JWT Authentication in Python APIs: A Practical Guide
Learn how to secure your Python APIs with JSON Web Tokens (JWT) using PyJWT and Flask. This guide covers token creation, endpoint protection, common mistakes, and production-ready patterns for FastAPI as well.
Locking Down Your Python APIs: A Practical Guide to JWT Authentication
If you've been building APIs with Python, you've probably realized that letting anyone access your endpoints is a recipe for disaster. That's where JSON Web Tokens (JWT) come in. They're not just some fancy tech buzzword—they're one of the most practical ways to secure your APIs without overcomplicating things.
When I first started working with APIs at PythonSkillset, I spent weeks trying to understand authentication methods. JWT turned out to be the sweet spot between security and simplicity. Let me show you how it actually works.
What Makes JWT Different?
Think of JWT as a digital ID card. When a user logs into your API, you create a token that contains their identity information. Instead of checking their password every single time they make a request, they just show you this token. You verify it's genuine, and that's it.
The beauty is in the structure. A JWT has three parts: - Header: Says what kind of token it is and how it's encrypted - Payload: The actual data (user ID, expiration time, etc.) - Signature: Digital proof that nobody tampered with the token
All these parts are base64-encoded and separated by dots, making something that looks like eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.xxxxxxxx
Getting Started with PyJWT
First, you need to install the library. I recommend PyJWT because it's actively maintained and works well with Flask and FastAPI:
pip install pyjwt
Now here's a common scenario from my work at PythonSkillset. We needed to protect a user dashboard API. Here's how we set up the token creation:
import jwt
import datetime
def create_jwt_token(user_id):
payload = {
'user_id': user_id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow()
}
token = jwt.encode(payload, 'your-secret-key', algorithm='HS256')
return token
The exp claim is crucial—it sets an expiration time. Never create tokens that last forever. One hour works well for most applications.
Protecting Your Endpoints
Here's where things get interesting. You need to verify tokens on every protected route. Let me show you how we do this at PythonSkillset with Flask:
from functools import wraps
from flask import request, jsonify
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token is missing'}), 401
try:
# Remove 'Bearer ' prefix
token = token.split(' ')[1] if 'Bearer' in token else token
data = jwt.decode(token, 'your-secret-key', algorithms=['HS256'])
current_user = data['user_id']
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
return f(current_user, *args, **kwargs)
return decorated
@app.route('/api/dashboard')
@token_required
def dashboard(current_user):
return jsonify({'user_id': current_user, 'data': 'sensitive info'})
Notice how we handle the "Bearer" prefix? Many developers forget this and wonder why their tokens don't work. The Authorization header usually comes as Bearer eyJhbGci..., and you need to strip that prefix before decoding.
Common Mistakes I've Seen
At PythonSkillset, we review a lot of API security implementations. Here are the three most common mistakes:
1. Storing secrets in code Never hardcode your JWT secret. Use environment variables or a secrets manager:
import os
SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
2. Ignoring token storage Where users store their tokens matters. Local storage in browsers is vulnerable to XSS. HTTP-only cookies are safer for web apps.
3. Not refreshing tokens Your tokens expire in an hour, but what if the user is actively using the app? Implement refresh tokens—short-lived access tokens paired with longer-lived refresh tokens.
Making It Production-Ready
For real-world applications, consider these additions:
- Use RS256 instead of HS256 for distributed systems. HS256 requires sharing a secret, while RS256 uses public/private key pairs.
- Add rate limiting to token endpoints. Otherwise, attackers can brute force your login.
- Log failed token verifications. You'll spot attack patterns early.
Here's a more complete example using FastAPI that I wish I had when I started:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
@app.get("/api/protected")
async def protected_route(user = Depends(verify_token)):
return {"message": "Access granted", "user": user}
The Depends(verify_token) pattern makes it incredibly easy to protect multiple routes without repeating code.
Testing Your Implementation
Don't skip this step. Use a tool like Postman or write simple tests:
import requests
# Get a token
login_response = requests.post('http://localhost:5000/login', json={
'username': 'testuser',
'password': 'testpass'
})
token = login_response.json()['token']
# Use the token
headers = {'Authorization': f'Bearer {token}'}
response = requests.get('http://localhost:5000/api/dashboard', headers=headers)
print(response.status_code) # Should be 200
The Bottom Line
JWT authentication doesn't have to be complicated. Start with the basics—create tokens, verify them, handle errors gracefully. As you get comfortable, add layers like refresh tokens and role-based access.
Remember, security is a journey, not a destination. The APIs I build today at PythonSkillset are far more secure than what I wrote six months ago, and that's exactly how it should be. Keep learning, keep testing, and your users' data will stay safe.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.