JSON Web Tokens (JWT) are the standard for stateless authentication in modern applications. But improper JWT implementation leads to security vulnerabilities. This guide covers JWT best practices for secure authentication.
Understanding JWT
JWT Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JWT Structure โ
โ โ
โ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 โ
โ . โ
โ eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0 โ
โ IjoxNTE2MjM5MDIyfQ โ
โ . โ
โ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c โ
โ โ
โ Header.Payload.Signature โ
โ โ
โ Header: Algorithm and token type โ
โ Payload: Claims (data) โ
โ Signature: Verifies integrity โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
JWT Components
import jwt
import base64
import json
# JWT Header
header = {
"alg": "HS256", # Algorithm
"typ": "JWT" # Type
}
# Encoded header
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
# JWT Payload (claims)
payload = {
# Registered claims
"iss": "my-app", # Issuer
"sub": "1234567890", # Subject (user ID)
"aud": "my-api", # Audience
"exp": 1516239022, # Expiration time
"nbf": 1516239022, # Not before
"iat": 1516239022, # Issued at
"jti": "unique-id", # JWT ID
# Custom claims
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"permissions": ["read", "write"]
}
# Signature
# HMAC-SHA256 of: base64UrlEncode(header) + "." + base64UrlEncode(payload)
# With secret key
JWT Types: JWS vs JWE
JWS (JSON Web Signature)
# JWS - Signed tokens (integrity only)
# Header for signed token
jws_header = {
"alg": "RS256", # RS256, HS256, ES256
"typ": "JWT"
}
# Creating JWS
token = jwt.encode(
payload,
private_key,
algorithm="RS256"
)
# Verifying JWS
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="my-api",
issuer="my-app"
)
JWE (JSON Web Encryption)
# JWE - Encrypted tokens (confidentiality + integrity)
# Header for encrypted token
jwe_header = {
"alg": "RSA-OAEP", # Key encryption
"enc": "A256GCM", # Content encryption
"typ": "JWT"
}
# Creating JWE (encrypted)
token = jwt.encode(
payload,
public_key,
algorithm="RSA-OAEP",
encryption="A256GCM"
)
# Decoding JWE
decoded = jwt.decode(
token,
private_key,
algorithms=["RSA-OAEP"],
encryption="A256GCM"
)
Secure JWT Implementation
Token Generation
import jwt
import time
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
# Generate key pair (in production, store securely!)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
def generate_token(user_id, email, role):
"""Generate secure JWT token"""
now = datetime.utcnow()
payload = {
# Registered claims
"iss": "my-app", # Your app
"sub": str(user_id), # User ID
"aud": "my-api", # Your API
"iat": int(now.timestamp()), # Issued at
"exp": int((now + timedelta(hours=1)).timestamp()), # 1 hour
"jti": generate_unique_id(), # Unique token ID
# Custom claims
"email": email,
"role": role,
"type": "access" # Access token
}
# Sign with RS256 (asymmetric)
token = jwt.encode(
payload,
private_key,
algorithm="RS256",
headers={"kid": "key-id-1"}
)
return token
Token Validation
def validate_token(token):
"""Validate JWT token securely"""
try:
# Decode with public key
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="my-api",
issuer="my-app",
options={
# Security options
"verify_signature": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iat": True,
"verify_aud": True,
"verify_iss": True,
"require": ["exp", "iss", "sub"] # Required claims
}
)
# Additional checks
if payload.get("type") != "access":
raise ValueError("Invalid token type")
# Check for token revocation (optional)
if is_token_revoked(payload.get("jti")):
raise ValueError("Token revoked")
return payload
except jwt.ExpiredSignatureError:
raise AuthenticationError("Token expired")
except jwt.InvalidTokenError as e:
raise AuthenticationError(f"Invalid token: {e}")
Refresh Tokens
def generate_token_pair(user_id, email):
"""Generate access and refresh tokens"""
now = datetime.utcnow()
# Access token (short-lived: 15 min - 1 hour)
access_payload = {
"sub": str(user_id),
"email": email,
"type": "access",
"iat": int(now.timestamp()),
"exp": int((now + timedelta(hours=1)).timestamp())
}
access_token = jwt.encode(
access_payload,
private_key,
algorithm="RS256"
)
# Refresh token (long-lived: 7-30 days)
refresh_payload = {
"sub": str(user_id),
"type": "refresh",
"iat": int(now.timestamp()),
"exp": int((now + timedelta(days=7)).timestamp()),
"jti": generate_unique_id() # For revocation
}
refresh_token = jwt.encode(
refresh_payload,
private_key,
algorithm="RS256"
)
# Store refresh token JTI for revocation
store_refresh_token(refresh_payload["jti"], user_id)
return access_token, refresh_token
def refresh_access_token(refresh_token):
"""Exchange refresh token for new access token"""
# Validate refresh token
payload = jwt.decode(
refresh_token,
public_key,
algorithms=["RS256"],
options={"require": ["type", "jti"]}
)
if payload.get("type") != "refresh":
raise AuthenticationError("Invalid token type")
# Check not revoked
if is_token_revoked(payload.get("jti")):
raise AuthenticationError("Token revoked")
# Get user info
user = get_user(payload.get("sub"))
# Generate new access token
return generate_token(user.id, user.email, user.role)
Security Best Practices
Algorithm Security
# Algorithm recommendations
secure_algorithms = {
"recommended": [
"RS256", # RSA with SHA-256 (asymmetric)
"ES256", # ECDSA with SHA-256 (asymmetric)
"EdDSA" # Edwards-curve DSA (asymmetric)
],
"acceptable": [
"HS256" # HMAC with SHA-256 (symmetric, needs strong key)
],
"never_use": [
"none" # ALGORITHM NONE ATTACK!
]
}
# NEVER trust algorithm from token!
# Always specify allowed algorithms
jwt.decode(token, key, algorithms=["RS256"])
Key Management
# Key management best practices
key_management = {
"rotation": {
"description": "Rotate keys regularly",
"practice": "Rotate every 3-6 months",
"implementation": "Use kid header for key ID"
},
"storage": {
"description": "Store keys securely",
"practice": "HS256: strong random key",
"practice": "RS256/ES256: key vault or HSM"
},
"key_ids": {
"description": "Identify which key was used",
"practice": "Use 'kid' header",
"example": '{"alg":"RS256","kid":"key-id-1"}'
}
}
# Multiple keys for rotation
def get_signing_key(kid):
keys = {
"key-id-1": current_private_key,
"key-id-2": previous_private_key
}
return keys.get(kid)
def get_verification_key(kid):
keys = {
"key-id-1": current_public_key,
"key-id-2": previous_public_key
}
return keys.get(kid)
Token Storage
# Token storage security
storage_recommendations:
access_token:
- "Memory (JavaScript variable)"
- "Avoid: localStorage (XSS vulnerable)"
- "Avoid: Cookies (CSRF vulnerable without protection)"
refresh_token:
- "HTTP-only cookie"
- "Server-side storage"
- "Encrypted if stored client-side"
security_headers:
- "Set Secure flag on cookies"
- "Set HttpOnly flag on cookies"
- "Set SameSite=strict or lax"
- "Use CSRF tokens with cookies"
Implementation Examples
Flask JWT Implementation
from flask import Flask, request, jsonify
import jwt
from functools import wraps
app = Flask(__name__)
# Configuration
app.config['JWT_SECRET'] = get_jwt_secret() # From secure vault
app.config['JWT_ALGORITHM'] = 'RS256'
def create_token(user):
"""Create JWT for user"""
now = datetime.utcnow()
payload = {
"sub": str(user.id),
"email": user.email,
"role": user.role,
"iat": int(now.timestamp()),
"exp": int((now + timedelta(hours=1)).timestamp())
}
return jwt.encode(payload, private_key, algorithm="RS256")
def token_required(f):
"""Decorator to require valid JWT"""
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Get token from header
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
if auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if not token:
return jsonify({'error': 'Token missing'}), 401
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="my-api",
issuer="my-app"
)
request.user = payload
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
@app.route('/protected')
@token_required
def protected():
return jsonify({
'user': request.user['sub'],
'email': request.user['email']
})
FastAPI JWT Implementation
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
"""Validate JWT and return user"""
token = credentials.credentials
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="my-api",
issuer="my-app"
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired"
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
@app.get("/protected")
async def protected_route(user: dict = Depends(get_current_user)):
return {"user_id": user["sub"], "email": user["email"]}
Common Vulnerabilities
Attacks and Mitigations
vulnerabilities = {
"algorithm_none": {
"attack": "Set alg: none in header",
"mitigation": "Always specify allowed algorithms"
},
"key_confusion": {
"attack": "Use RS token with HS and weak secret",
"mitigation": "Use different keys for different algos"
},
"jwt_brute_force": {
"attack": "Brute force weak HMAC secret",
"mitigation": "Use strong secret (256+ bits)"
},
"jku_header": {
"attack": "Use external key URL",
"mitigation": "Disable header injection"
},
"kid_header": {
"attack": "Path traversal in kid header",
"mitigation": "Validate and sanitize kid"
}
}
# Safe configuration
safe_options = {
"verify_signature": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iat": True,
"verify_aud": True,
"verify_iss": True,
"algorithms": ["RS256", "ES256"], # Explicit!
"require": ["exp", "iss", "sub"] # Required claims
}
Conclusion
JWT security essentials:
- Algorithm: Use RS256 or ES256, never “none”
- Expiration: Short-lived access tokens (1 hour max)
- Validation: Verify signature, expiration, issuer, audience
- Storage: Access in memory, refresh in HTTP-only cookies
- Key rotation: Regular key rotation with
kidheader - Revocation: Track revoked tokens for refresh tokens
Always use established libraries and keep dependencies updated.
Comments