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):

python
import 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:

python
from 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:

python
class 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

  1. Storing JWTs in localStorage: Vulnerable to XSS attacks. Use httpOnly cookies for web apps.

  2. No token revocation: JWTs are valid until expiry. Implement a token blacklist for logout.

  3. Long-lived access tokens: Keep access tokens short (15 min). Use refresh tokens for longer sessions.

Best Practices

  1. Use short-lived access tokens: 15 minutes is a good balance.

  2. Implement refresh token rotation: Issue new refresh token with each refresh.

  3. Store minimal data in payload: Only user ID, not sensitive information.

  4. 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

python
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'),
    }
✓ Completed