Vulnerability
Django

JWT Vulnerabilities in Django

Django REST Framework applications typically use djangorestframework-simplejwt for JWT authentication. While SimpleJWT handles many security details correctly by default, vulnerabilities arise from using Django's SECRET_KEY as the JWT signing key (which is often weak or committed to version control), overly long token lifetimes, and missing token blacklisting after password changes.

Scan Your Django App

How JWT Vulnerabilities Manifests in Django

The most common JWT vulnerability in Django is using the default SIGNING_KEY setting in SimpleJWT, which defaults to Django's SECRET_KEY. If SECRET_KEY is weak, short, or committed to Git, every JWT can be forged. SimpleJWT's default ACCESS_TOKEN_LIFETIME is 5 minutes and REFRESH_TOKEN_LIFETIME is 1 day, but developers frequently extend these to avoid implementing token refresh in the frontend. Access tokens with 24-hour lifetimes mean stolen tokens are valid for an entire day. Missing token blacklisting means that after a user changes their password or is deactivated, their existing tokens remain valid until expiration. SimpleJWT includes a blacklist app, but it must be explicitly enabled. Custom token claims added via get_token() overrides can leak sensitive information. Developers sometimes include email addresses, roles, or internal IDs in the JWT payload, which is base64-encoded (not encrypted) and readable by anyone who captures the token.

Real-World Impact

A Django REST Framework API used the default SimpleJWT configuration with Django's SECRET_KEY set to the auto-generated value from django-admin startproject. The SECRET_KEY was committed to the public GitHub repository. An attacker found it, forged admin JWT tokens, and accessed all API endpoints with full privileges. Another Django API had a 7-day access token lifetime and no blacklisting. A user reported their account compromised and changed their password, but the attacker's stolen access token remained valid for the remaining 6 days, during which they continued accessing the victim's data.

Step-by-Step Fix

1

Configure SimpleJWT with secure settings

Set a dedicated signing key, short token lifetimes, and enable token rotation.

# settings.py
from datetime import timedelta
import os

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'SIGNING_KEY': os.environ['JWT_SIGNING_KEY'],  # Dedicated key, not SECRET_KEY
    'ALGORITHM': 'HS256',
    'AUTH_HEADER_TYPES': ('Bearer',),
    'TOKEN_OBTAIN_SERIALIZER': 'myapp.serializers.MyTokenObtainPairSerializer',
}

# Enable the blacklist app
INSTALLED_APPS = [
    # ...
    'rest_framework_simplejwt.token_blacklist',
]
2

Blacklist tokens on password change

When a user changes their password, blacklist all their outstanding tokens.

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken

class ChangePasswordView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        user = request.user
        serializer = ChangePasswordSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        user.set_password(serializer.validated_data['new_password'])
        user.save()

        # Blacklist all outstanding tokens for this user
        tokens = OutstandingToken.objects.filter(user=user)
        for token in tokens:
            BlacklistedToken.objects.get_or_create(token=token)

        return Response({'message': 'Password changed. Please log in again.'})
3

Minimize JWT payload claims

Only include essential claims in the token. Fetch user details from the database instead of encoding them in the JWT.

# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        # Only add minimal claims
        # DO NOT add: token['email'], token['role'], token['phone']
        # The 'sub' (user_id) claim is sufficient
        return token

# In views, fetch user details from DB using the token's user_id
# instead of reading them from JWT claims
class UserProfileView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        # request.user is loaded from DB based on JWT sub claim
        return Response({
            'email': request.user.email,
            'name': request.user.get_full_name(),
        })

Prevention Best Practices

1. Set a dedicated SIGNING_KEY in SimpleJWT configuration — do not rely on Django's SECRET_KEY. 2. Keep ACCESS_TOKEN_LIFETIME short (5-15 minutes) and use refresh tokens. 3. Enable SimpleJWT's token blacklisting and blacklist tokens on password change. 4. Never commit SECRET_KEY or SIGNING_KEY to version control. 5. Minimize claims in the JWT payload — do not include sensitive data. 6. Use ROTATE_REFRESH_TOKENS = True to enable refresh token rotation.

How to Test

1. Check if SIGNING_KEY is set separately from SECRET_KEY in your SimpleJWT configuration. 2. Decode a JWT from your API at jwt.io and verify it does not contain sensitive claims. 3. Change a user's password and test if the old access token still works. 4. Check ACCESS_TOKEN_LIFETIME — anything over 30 minutes is risky. 5. Use Vibe App Scanner to detect JWT configuration issues in your Django application.

Frequently Asked Questions

Should I use SimpleJWT or Django's session authentication?

For traditional web apps with server-rendered templates, Django sessions are simpler and more secure. Use SimpleJWT for SPAs or mobile apps that communicate with a Django REST Framework API. Sessions can be revoked instantly and do not require blacklisting. JWTs require more careful configuration but work better for stateless APIs.

Is it safe to use Django SECRET_KEY as the JWT signing key?

It is technically functional but risky. Django's SECRET_KEY is used for many purposes (sessions, CSRF, password reset tokens), so exposing it compromises all of them. Use a dedicated SIGNING_KEY for JWTs stored in a separate environment variable. This limits the blast radius if either key is compromised.

Does SimpleJWT prevent algorithm confusion attacks?

Yes. SimpleJWT specifies the algorithm explicitly in its configuration (ALGORITHM setting, defaulting to HS256) and validates it during verification. Unlike the raw PyJWT library where developers might call jwt.decode() without specifying algorithms, SimpleJWT handles this correctly by default.

Is Your Django App Vulnerable to JWT Vulnerabilities?

VAS automatically scans for jwt vulnerabilities vulnerabilities in Django applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Django stack.