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
- JWT decoder → - paste a token to decode and verify it in your browser
- JWT Signing Algorithms → - HS256, RS256, ES256, EdDSA with key sizes and code
- Registered Claims Reference → - iss, sub, aud, exp, jti, and every RFC 7519 claim
- JWT Security Vulnerabilities → - alg:none, algorithm confusion, kid injection, and every fix
- Verify JWT in Node.js → - jsonwebtoken and jose code
- Verify JWT in Java → - Spring Security and jjwt, including the aud mismatch fix