How to Implement JWT Authentication with Custom Middleware Security

JSON Web Tokens (JWT) provide stateless, secure authentication for REST APIs. Combining JWT with custom middleware enables advanced security patterns—such as request validation, rate limiting, and role-based access control—at the middleware layer. This guide uses Django REST Framework (DRF) and djangorestframework-simplejwt to implement robust JWT authentication and middleware-based security.

1. Install Dependencies

pip install djangorestframework djangorestframework-simplejwt
  

Ensure rest_framework is in INSTALLED_APPS in settings.py.

2. Configure Simple JWT in Settings

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
}
    

Note: Rotation and blacklisting protect against token theft by invalidating old refresh tokens.

3. Create Token Views

# urls.py
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
    

Clients POST credentials to /api/token/ to receive access and refresh tokens.

See also  How to add to manytomany field in Django

4. Implement Custom Security Middleware

Middleware can enforce additional checks on each request—for example, rate limiting or auditing.

# middleware/security.py
import time
from django.core.cache import cache
from django.http import JsonResponse

class RateLimitMiddleware:
    """
    Simple per-user rate limiter using Django cache.
    Limits to 100 requests per 10-minute window.
    """
    RATE_LIMIT = 100
    WINDOW = 600  # seconds

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        user = request.user
        if user.is_authenticated:
            key = f"rl:{user.id}"
            count, first_ts = cache.get(key, (0, time.time()))
            now = time.time()
            if now - first_ts > self.WINDOW:
                count, first_ts = 0, now
            count += 1
            cache.set(key, (count, first_ts), timeout=self.WINDOW)
            if count > self.RATE_LIMIT:
                return JsonResponse(
                    {'detail': 'Rate limit exceeded.'}, status=429
                )
        return self.get_response(request)
    

Add the middleware to MIDDLEWARE in settings.py:

MIDDLEWARE = [
    # ...
    'middleware.security.RateLimitMiddleware',
    # ...
]
    

5. Role-Based Access Control Middleware

Example: restrict certain endpoints to users with an “admin” role stored in JWT claims.

# middleware/roles.py
from django.http import JsonResponse
from rest_framework_simplejwt.authentication import JWTAuthentication

class RoleRequiredMiddleware:
    """
    Ensure JWT contains a required role claim for protected paths.
    """
    def __init__(self, get_response):
        self.get_response = get_response
        self.auth = JWTAuthentication()

    def __call__(self, request):
        # Only protect API endpoints under /api/admin/
        if request.path.startswith('/api/admin/'):
            try:
                user, token = self.auth.authenticate(request)
            except Exception:
                return JsonResponse({'detail': 'Invalid or missing token.'}, status=401)
            role = token.payload.get('role')
            if role != 'admin':
                return JsonResponse({'detail': 'Admin role required.'}, status=403)
            request.user = user
        return self.get_response(request)
    

Register it after authentication middleware:

MIDDLEWARE = [
    # ...
    'rest_framework_simplejwt.authentication.JWTAuthentication',
    'middleware.roles.RoleRequiredMiddleware',
    # ...
]
    

6. Add Custom JWT Claims in Token Generation

Override TokenObtainPairView to include extra claims:

# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        # Add custom claims
        token['role'] = user.profile.role  # e.g., 'admin' or 'user'
        token['full_name'] = user.get_full_name()
        return token

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer
    

Update urls.py to use the custom view:

from .views import CustomTokenObtainPairView

urlpatterns = [
    path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
    # ...
]
    

7. Test Authentication & Middleware

  1. Obtain JWT:
    POST /api/token/ 
    {
      "username": "admin",
      "password": "password123"
    }
          
  2. Access protected endpoint:
    GET /api/admin/dashboard/ 
    Authorization: Bearer <access_token>
          
  3. Rate-limit test: send 101 rapid requests to any endpoint as an authenticated user—expect HTTP 429 on the 101st.
Tip: Use Django’s cache backend (Redis/Memcached) for rate limiting storage to handle multiple processes and distributed deployments.
See also  How to Use django-adaptors

8. Security Best Practices

  • Rotate and blacklist refresh tokens to limit replay attacks.
  • Use HTTPS to protect tokens in transit.
  • Set AUTH_TOKEN_CLASSES to include AccessToken only for sensitive endpoints.
  • Monitor authentication events and rate limit violations in logs or an external monitoring system.