HIGH jwt misconfigurationdjangomutual tls

Jwt Misconfiguration in Django with Mutual Tls

Jwt Misconfiguration in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability

JWT misconfiguration in Django becomes particularly risky when combined with Mutual TLS (mTLS), because mTLS enforces client certificate authentication at the transport layer while developers may assume this also secures the JWT layer. This false sense of security can lead to missing or weak JWT validation, such as not verifying signatures, skipping issuer/audience checks, or accepting unsigned tokens.

When mTLS is used, requests may already present a valid client certificate, and the application might incorrectly trust the JWT payload without proper verification. For example, if django-oauth-toolkit or a custom JWT middleware does not explicitly validate the alg header and rejects none algorithms, an attacker could present a valid mTLS client certificate and supply a JWT with alg: none to gain unauthorized access.

Another scenario involves mismatched token audiences. mTLS ensures the client is known to the server, but if the JWT audience (aud) claim is not checked against the intended API consumers, tokens issued for one service may be accepted by another backend behind the same mTLS gateway. This can lead to horizontal or vertical privilege escalation when roles or scopes are not enforced consistently.

The interplay also surfaces issues in token binding. mTLS provides channel-level identity, but if the application does not correlate the certificate subject or serial with the JWT subject or scopes, there is no guarantee that the token matches the authenticated client certificate. Without this binding, a stolen JWT could be used from a different mTLS-authenticated session if the token itself is not tightly scoped and validated.

Additionally, weak or missing token expiration checks combined with long-lived certificates in mTLS can keep compromised tokens valid for extended periods. Attackers may capture JWTs from logs or error messages, and if signature verification or iss/aud/exp/nbf validation is incomplete, they can reuse them across requests that are accepted due to the mTLS context.

Finally, improper integration between mTLS and JWT can lead to bypasses where optional token validation is allowed. If Django views or permission classes treat JWT as optional when a client certificate is present, attackers can omit the JWT entirely and rely solely on the certificate, or supply a malformed token and still proceed if the certificate check passes.

Mutual Tls-Specific Remediation in Django — concrete code fixes

Remediation centers on strict JWT validation regardless of mTLS presence, binding token claims to the certificate identity, and enforcing least privilege. Always validate signature, issuer, audience, and standard claims, and never skip token checks when a client certificate is provided.

Example 1: Strict JWT validation middleware with mTLS awareness

import jwt
from django.http import HttpResponseForbidden
from django.utils.deprecation import MiddlewareMixin

class JwtMtlsValidationMiddleware(MiddlewareMixin):
    # Public keys or JWKS endpoint for your issuer
    JWKS_URL = 'https://auth.example.com/.well-known/jwks.json'
    AUDIENCE = 'my-api.example.com'
    ISSUER = 'https://auth.example.com/'

    def process_request(self, request):
        # mTLS already verified client cert at gateway; ensure JWT is present and valid
        auth = request.META.get('HTTP_AUTHORIZATION', '')
        if not auth.lower().startswith('bearer '):
            return HttpResponseForbidden('Missing Bearer token')
        token = auth[7:].strip()
        if not token:
            return HttpResponseForbidden('Empty token')

        # Fetch signing keys (in practice cache JWKS)
        import jwt
        from jwt import PyJWKClient
        jwks_client = PyJWKClient(self.JWKS_URL)
        signing_key = jwks_client.get_signing_key_from_jwt(token)

        # Enforce expected claims and algorithms
        try:
            decoded = jwt.decode(
                token,
                signing_key.key,
                algorithms=['RS256'],
                audience=self.AUDIENCE,
                issuer=self.ISSUER,
                options={'require_exp': True, 'verify_exp': True}
            )
            # Bind identity: ensure JWT subject matches certificate subject if available
            # e.g., request.META.get('SSL_CLIENT_SUBJECT') or request.META.get('SSL_CLIENT_I_DN')
            request.user = self._map_to_user(decoded)
        except jwt.InvalidTokenError as e:
            return HttpResponseForbidden(f'Invalid token: {str(e)}')

    def _map_to_user(self, decoded):
        # Map claims to Django user, applying least privilege
        from django.contrib.auth.models import User
        username = decoded.get('sub') or decoded.get('preferred_username')
        if not username:
            raise ValueError('Missing subject in token')
        user, _ = User.objects.get_or_create(username=username)
        # Apply scopes/roles from token to Django permissions or groups
        return user

Example 2: View-level enforcement with scope/role checks

from django.http import JsonResponse
from django.views import View
from functools import wraps
from django.core.exceptions import PermissionDenied

def require_scope(required_scope):
    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            if not hasattr(request, 'user') or request.user.is_anonymous:
                raise PermissionDenied('Authentication required')
            # scopes may be mapped to groups or custom attributes
            token_scopes = getattr(request, 'jwt_scopes', set())
            if required_scope not in token_scopes:
                raise PermissionDenied(f'Missing scope: {required_scope}')
            return view_func(request, *args, **kwargs)
        return _wrapped_view
    return decorator

class SensitiveDataView(View):
    @require_scope('data:read:sensitive')
    def get(self, request):
        return JsonResponse({'data': 'protected information'})

Example 3: Enforce algorithm and reject none

import jwt
from django.conf import settings

def decode_token_hard(token):
    # Always specify allowed algorithms; never use 'none' or 'auto'
    algorithms = ['RS256', 'ES256']
    # If you use asymmetric keys, load the public key from a trusted source
    public_key = settings.JWT_PUBLIC_KEY  # PEM string or loaded from JWKS
    try:
        payload = jwt.decode(
            token,
            public_key,
            algorithms=algorithms,
            audience='my-api.example.com',
            issuer='https://auth.example.com/',
            options={'verify_signature': True, 'require_exp': True}
        )
        return payload
    except jwt.InvalidAlgorithmError:
        raise ValueError('Algorithm not allowed')
    except jwt.InvalidTokenError:
        raise ValueError('Invalid token')

Operational and deployment guidance

  • Configure your API gateway or mTLS frontend to pass the verified client certificate metadata (subject, issuer, serial) in headers for downstream validation, but never rely on them alone.
  • Rotate JWT signing keys and certificate materials regularly and map tokens to short lifetimes; mTLS long-lived certs should not imply long-lived tokens.
  • Use centralized JWKS and strict CORS and referrer policies; log validation failures and certificate binding mismatches for audit without exposing sensitive details.
  • Test the combination: simulate tokens with missing claims, wrong audience, and unsupported algorithms while mTLS is present to ensure validation fails securely.

Related CWEs: authentication

CWE IDNameSeverity
CWE-287Improper Authentication CRITICAL
CWE-306Missing Authentication for Critical Function CRITICAL
CWE-307Brute Force HIGH
CWE-308Single-Factor Authentication MEDIUM
CWE-309Use of Password System for Primary Authentication MEDIUM
CWE-347Improper Verification of Cryptographic Signature HIGH
CWE-384Session Fixation HIGH
CWE-521Weak Password Requirements MEDIUM
CWE-613Insufficient Session Expiration MEDIUM
CWE-640Weak Password Recovery HIGH

Frequently Asked Questions

Does mTLS alone replace the need for JWT validation in Django?
No. mTLS authenticates the client at the transport layer, but JWT validation is still required to enforce application-level identity, scopes, audience, and expiration. Always validate JWTs independently.
What should I do if my Django app receives JWTs with the 'none' algorithm when mTLS is used?
Reject such tokens. Explicitly configure your JWT library to allow only strong algorithms like RS256 or ES256 and never accept 'none'. Also verify issuer, audience, and expiration regardless of mTLS.