Skip to content
AB

Strict JWT Validation Without Algorithm Confusion

Validate JWT algorithms, signatures, issuers, audiences, and required claims deliberately before trusting user identity.

Ashish BhakhandPublished

A JWT is only useful after verification. Decoding its payload and reading sub proves nothing because anyone can construct an unsigned-looking token.

Start by extracting the bearer value strictly:

Code
def bearer_token(authorization: str) -> str:
    scheme, separator, token = authorization.partition(' ')
    if scheme.lower() != 'bearer' or not separator or not token.strip():
        raise HTTPException(status_code=401, detail='Invalid authorization header.')
    return token.strip()

Allow-list the algorithm#

Some identity providers migrate from a shared HS256 secret to asymmetric RS256 or ES256 keys. Read the unverified header only to select an explicitly allowed verification path:

Code
header = jwt.get_unverified_header(token)
algorithm = str(header.get('alg', '')).upper()

if algorithm == 'HS256':
    payload = decode_hs256(token, secret)
elif algorithm in {'RS256', 'ES256'}:
    payload = decode_with_jwks(token, jwks_client)
else:
    raise JWTValidationError('Unsupported signing algorithm')

Do not attempt one verifier and fall back to another after failure. Verification behavior should never be negotiated by trial and error.

Verify the complete identity contract#

Signature validation alone is insufficient. Require and validate:

  • exp: the token has not expired;
  • iat: it was issued at a reasonable time;
  • sub: the user identity exists;
  • aud: it targets this application;
  • iss: it came from the expected issuer.

A small clock-skew allowance can accommodate real infrastructure differences without disabling time validation.

Cache the JWKS client, but allow normal key rotation. Avoid logging the token or returning library exception details to the caller.

Convert claims into a small identity#

Code
@dataclass(frozen=True)
class AuthenticatedUser:
    id: str
    email: str | None = None

Downstream code needs a verified identity, not the entire raw claim dictionary. Authentication should attach that identity to request context for authorization and diagnostics.

Return the same safe 401 message for malformed, expired, and incorrectly signed tokens. Detailed failure reasons belong in protected diagnostics. A strict verifier accepts only the exact token contract the application intends to trust.

← Back to all writing