Guide 3 min read Updated

Verify JWT in Python (PyJWT, SimpleJWT, FastAPI)

Verify JWT signatures in Python with PyJWT and Django SimpleJWT. Sign and verify HS256 and RS256 tokens, validate iss/aud/exp, verify JWTs against a JWKS endpoint, and protect FastAPI routes with JWT bearer auth.

Python’s standard JWT library is PyJWT. Django REST Framework users typically use SimpleJWT (a batteries-included layer built on PyJWT). FastAPI users build a small dependency around PyJWT. This page covers all three: signing, verifying with claim validation, JWKS verification, and protecting a FastAPI route.

Install

# PyJWT with RS256/ES256/EdDSA support (needs the cryptography backend)
pip install "pyjwt[crypto]"

# Django SimpleJWT (if using DRF)
pip install djangorestframework-simplejwt

Sign and verify an HS256 token (PyJWT)

import jwt, time, os

secret = os.environ["JWT_SECRET"]  # 32+ bytes of random data

# Sign
payload = {
    "sub": "user_123",
    "role": "admin",
    "iss": "https://auth.example.com",
    "aud": "https://api.example.com",
    "exp": int(time.time()) + 15 * 60,
}
token = jwt.encode(payload, secret, algorithm="HS256")

# Verify — algorithms is mandatory since PyJWT 2.0
try:
    decoded = jwt.decode(
        token,
        secret,
        algorithms=["HS256"],
        issuer="https://auth.example.com",
        audience="https://api.example.com",
        leeway=60,  # 60s clock skew tolerance for exp/nbf
    )
except jwt.ExpiredSignatureError:
    print("expired")
except jwt.InvalidAudienceError:
    print("wrong aud")
except jwt.InvalidIssuerError:
    print("wrong iss")

Sign and verify an RS256 token (PyJWT)

import jwt
from cryptography.hazmat.primitives import serialization

with open("rsa-private.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)
with open("rsa-public.pem", "rb") as f:
    public_key = serialization.load_pem_public_key(f.read())

token = jwt.encode(
    {"sub": "user_123", "iss": "https://auth.example.com", "aud": "https://api.example.com", "exp": int(time.time()) + 900},
    private_key,
    algorithm="RS256",
    headers={"kid": "rsa-key-v1"},
)

decoded = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],
    issuer="https://auth.example.com",
    audience="https://api.example.com",
)

Verify a JWT against a JWKS endpoint (PyJWT ≥ 2.6)

import jwt
from jwt import PyJWKClient

jwks_client = PyJWKClient("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(token)

decoded = jwt.decode(
    token,
    signing_key.key,
    algorithms=["RS256"],
    audience="https://api.example.com",
    issuer="https://YOUR_DOMAIN.auth0.com/",
)

PyJWKClient fetches the JWKS once and caches it; it refetches only when the token’s kid is missing from the cache. Do not fetch the JWKS yourself on every request.

Protect a FastAPI route with JWT bearer auth

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from jwt import PyJWKClient

app = FastAPI()
bearer = HTTPBearer(auto_error=True)
jwks_client = PyJWKClient("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json")

def verify_jwt(credentials: HTTPAuthorizationCredentials = Depends(bearer)):
    token = credentials.credentials
    try:
        signing_key = jwks_client.get_signing_key_from_jwt(token)
        return jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience="https://api.example.com",
            issuer="https://YOUR_DOMAIN.auth0.com/",
        )
    except jwt.PyJWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token",
            headers={"WWW-Authenticate": "Bearer"},
        )

@app.get("/me")
def me(user = Depends(verify_jwt)):
    return {"sub": user["sub"]}

The HTTPBearer dependency parses the Authorization: Bearer <token> header and rejects requests missing it. verify_jwt does signature + claim validation and raises 401 on any failure. Every protected route adds Depends(verify_jwt) to its signature.

Django SimpleJWT configuration

In settings.py, configure the token lifetimes and the signing key:

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "ALGORITHM": "HS256",          # or "RS256" with SIGNING_KEY = private key
    "SIGNING_KEY": os.environ["JWT_SECRET"],
    "VERIFYING_KEY": None,         # public key for RS256
    "AUDIENCE": "https://api.example.com",
    "ISSUER": "https://auth.example.com",
    "AUTH_HEADER_TYPES": ("Bearer",),
}

SimpleJWT issues and verifies tokens tied to your user model via TokenObtainPairView, handles refresh-token rotation, and integrates with DRF permission classes. Set AUDIENCE and ISSUER — without them, SimpleJWT skips those claim checks.


Continue reading