Introduction
JSON Web Tokens (JWT) are self-contained tokens that encode user information. Unlike server-stored tokens, JWTs can be verified without database lookups, making them popular for distributed systems and microservices.
Key Concepts
JWT (JSON Web Token): A compact, URL-safe token format consisting of three parts: header, payload, and signature.
Access Token: Short-lived JWT used to access protected resources (typically 15 minutes to 1 hour).
Refresh Token: Longer-lived token used to obtain new access tokens without re-authentication.
Token Signature: Cryptographic signature that ensures the token hasn't been tampered with.
Real World Context
JWTs are ideal for:
- Microservices: Services can verify tokens without shared database access
- Cross-domain APIs: Tokens work across different domains
- Mobile apps: Tokens can be stored securely on device
- Stateless scaling: No server-side session storage needed
Deep Dive
JWT Structure
header.payload.signature
# Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDUzMjAwMDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Simple JWT Implementation
This module creates access and refresh tokens using the PyJWT library. Access tokens are short-lived (15 minutes) while refresh tokens last longer (7 days):
pythonimport jwt import json from datetime import datetime, timedelta, timezone from django.conf import settings from django.http import JsonResponse from django.contrib.auth import authenticate JWT_SECRET = settings.SECRET_KEY JWT_ALGORITHM = 'HS256' ACCESS_TOKEN_LIFETIME = timedelta(minutes=15) REFRESH_TOKEN_LIFETIME = timedelta(days=7) def generate_tokens(user): """Generate access and refresh tokens.""" # Note: datetime.utcnow() is deprecated since Python 3.12 # Use datetime.now(timezone.utc) instead now = datetime.now(timezone.utc) access_payload = { 'user_id': user.id, 'username': user.username, 'exp': now + ACCESS_TOKEN_LIFETIME, 'iat': now, 'type': 'access' } refresh_payload = { 'user_id': user.id, 'exp': now + REFRESH_TOKEN_LIFETIME, 'iat': now, 'type': 'refresh' } return { 'access': jwt.encode(access_payload, JWT_SECRET, JWT_ALGORITHM), 'refresh': jwt.encode(refresh_payload, JWT_SECRET, JWT_ALGORITHM), } def verify_token(token, token_type='access'): """Verify and decode a JWT.""" try: payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) if payload.get('type') != token_type: return None return payload except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None
The generate_tokens() function embeds a type field ('access' or 'refresh') in each payload so verify_token() can reject a refresh token used as an access token and vice versa.
Auth Endpoints
The token-obtain endpoint validates credentials and returns both tokens, while the refresh endpoint exchanges a valid refresh token for a new token pair:
pythonfrom django.views.decorators.csrf import csrf_exempt @csrf_exempt def api_token_obtain(request): """Login and get tokens.""" data = json.loads(request.body) user = authenticate( username=data.get('username'), password=data.get('password') ) if not user: return JsonResponse({'error': 'Invalid credentials'}, status=401) tokens = generate_tokens(user) return JsonResponse(tokens) @csrf_exempt def api_token_refresh(request): """Get new access token using refresh token.""" data = json.loads(request.body) refresh_token = data.get('refresh') payload = verify_token(refresh_token, 'refresh') if not payload: return JsonResponse({'error': 'Invalid refresh token'}, status=401) from django.contrib.auth import get_user_model User = get_user_model() try: user = User.objects.get(id=payload['user_id']) except User.DoesNotExist: return JsonResponse({'error': 'User not found'}, status=401) tokens = generate_tokens(user) return JsonResponse(tokens)
The refresh endpoint re-fetches the user from the database to ensure revoked or deleted accounts cannot obtain new tokens.
JWT Authentication Middleware
This middleware extracts the JWT from the Authorization header, verifies its signature and expiration, and attaches the user to the request:
pythonclass JWTAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): auth = request.headers.get('Authorization', '') if auth.startswith('Bearer '): token = auth[7:] payload = verify_token(token, 'access') if payload: from django.contrib.auth import get_user_model User = get_user_model() try: request.user = User.objects.get(id=payload['user_id']) except User.DoesNotExist: pass return self.get_response(request)
Unlike database-token middleware, JWT verification requires no database query -- the signature check alone confirms authenticity. The user lookup only happens when the token is valid.
Common Pitfalls
-
Storing JWTs in localStorage: Vulnerable to XSS attacks. Use httpOnly cookies for web apps.
-
No token revocation: JWTs are valid until expiry. Implement a token blacklist for logout.
-
Long-lived access tokens: Keep access tokens short (15 min). Use refresh tokens for longer sessions.
Best Practices
-
Use short-lived access tokens: 15 minutes is a good balance.
-
Implement refresh token rotation: Issue new refresh token with each refresh.
-
Store minimal data in payload: Only user ID, not sensitive information.
-
Use HTTPS always: Tokens are credentials—protect them in transit.
Summary
JWTs provide stateless authentication suitable for distributed systems. They consist of a header, payload, and signature. Implement short-lived access tokens with refresh tokens for security. Be aware of storage security—httpOnly cookies are safer than localStorage for web applications.
Code Examples
import jwt
from datetime import datetime, timedelta, timezone
from django.conf import settings
def generate_tokens(user):
now = datetime.now(timezone.utc)
access_payload = {
'user_id': user.id,
'exp': now + timedelta(minutes=15),
'iat': now,
'type': 'access'
}
refresh_payload = {
'user_id': user.id,
'exp': now + timedelta(days=7),
'iat': now,
'type': 'refresh'
}
return {
'access': jwt.encode(access_payload, settings.SECRET_KEY, 'HS256'),
'refresh': jwt.encode(refresh_payload, settings.SECRET_KEY, 'HS256'),
}