A comprehensive guide to understanding and implementing JWT authentication and authorization using FastAPI and JavaScript.
📚 Table of Contents
Navigation Guide:
🟢 Beginner | 🟡 Intermediate | 🟠 Advanced | 🔴 Expert
Part I: Fundamentals 🟢
Master the core concepts of JWT authentication
- Introduction to JWT – What is JWT and why use it?
- JWT Structure and Anatomy – Understanding the three parts
- Claims and Payload – Working with JWT data
- Cryptographic Algorithms – Security foundations
- Setting Up Development Environment – Get started
Part II: Basic Implementation 🟡
Build your first JWT authentication system
- Basic JWT Authentication with FastAPI – Backend implementation
- Frontend Integration with JavaScript – Client-side auth
- Token Storage and Management – Secure token handling
- Authorization and Role-Based Access Control – Permissions
- Error Handling and Validation – Robust error management
Part III: Advanced Security 🟠
Implement enterprise-grade security features
- JWT Encryption (JWE) – Encrypt sensitive data
- OAuth 2.0 Integration – Third-party authentication
- Key Management and Rotation – Advanced key strategies
- Token Revocation and Blacklisting – Invalidate tokens
- Security Best Practices – Comprehensive security guide
Part IV: Production Systems 🔴
Deploy production-ready applications
- Complete Full-Stack Application – End-to-end example
- Troubleshooting and FAQ – Common issues and solutions
- Production Deployment and Monitoring – Go live
- Testing and Quality Assurance – Comprehensive testing
Part V: Enterprise and Scale 🔴
Master microservices, performance, and future trends
- JWT in Microservices Architecture – Distributed systems
- Performance Optimization – Scale efficiently
- Migration Strategies – Legacy system transitions
- Future Standards and Trends – WebAuthn, DPoP, SD-JWT
- System Design and Architectural Patterns – Enterprise architecture
- Summary and Best Practices – Key takeaways
1. Introduction to JWT
What is JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.
Why Use JWT?
JWTs are particularly useful for:
- Stateless Authentication: No need to store session data on the server
- Scalability: Perfect for distributed systems and microservices
- Cross-Domain Authentication: Works across different domains and services
- Mobile Applications: Lightweight and efficient for mobile apps
- Single Sign-On (SSO): Enables seamless authentication across multiple applications
JWT vs Traditional Sessions
graph LR
subgraph "Traditional Session-Based Auth"
A[Client] -->|Login Request| B[Server]
B -->|Session ID| A
A -->|Session ID Cookie| C[Subsequent Requests]
C -->|Lookup Session| D[Session Store]
D -->|Session Data| B
end
subgraph "JWT-Based Auth"
E[Client] -->|Login Request| F[Server]
F -->|JWT Token| E
E -->|JWT in Header| G[Subsequent Requests]
G -->|Verify Signature| F
endWhen to Use JWT
Use JWT when:
- Building stateless APIs
- Implementing microservices architecture
- Need cross-domain authentication
- Building single-page applications (SPAs)
- Implementing mobile app authentication
Consider alternatives when:
- Building simple server-side rendered applications
- Need immediate token revocation
- Working with highly sensitive data requiring frequent validation
2. JWT Structure and Anatomy
JWT Structure Overview
A JWT consists of three parts separated by dots (.):
header.payload.signatureJSONgraph TB
A[JWT Token] --> B[Header]
A --> C[Payload]
A --> D[Signature]
B --> E["Algorithm (alg)Token Type (typ)"]
C --> F["ClaimsUser InfoExpiration"]
D --> G["SignatureVerification"]Header
The header typically consists of two parts:
- alg: The signing algorithm being used (e.g., HS256, RS256)
- typ: The type of token (JWT)
Example Header:
{
"alg": "HS256",
"typ": "JWT"
}JSONBase64URL Encoded:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9ANSIPayload
The payload contains the claims – statements about an entity (typically, the user) and additional data.
Standard Claims:
- iss (issuer): Who issued the token
- sub (subject): Who the token is about
- aud (audience): Who the token is intended for
- exp (expiration time): When the token expires
- nbf (not before): When the token becomes valid
- iat (issued at): When the token was issued
- jti (JWT ID): Unique identifier for the token
Example Payload:
{
"sub": "user123",
"name": "John Doe",
"role": "admin",
"iat": 1697644800,
"exp": 1697648400
}JSONBase64URL Encoded:
eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjk3NjQ0ODAwLCJleHAiOjE2OTc2NDg0MDB9ANSISignature
The signature is created by taking the encoded header, encoded payload, a secret, and signing it using the algorithm specified in the header.
For HMAC SHA256:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)ANSIComplete JWT Example
Raw JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjk3NjQ0ODAwLCJleHAiOjE2OTc2NDg0MDB9.5mUL-WOmOy-P5n6K8T0H3pPz4yM9QJF2k8N7V6x2A1MANSIJWT Validation Flow
sequenceDiagram
participant C as Client
participant S as Server
participant V as JWT Validator
C->>S: Send JWT Token
S->>V: Extract Header & Payload
V->>V: Decode Base64URL
V->>V: Verify Signature
V->>V: Check Expiration
V->>V: Validate Claims
V->>S: Validation Result
S->>C: Access Granted/Denied3. Claims and Payload
Understanding Claims
Claims are statements about an entity (typically, the user) and additional data. There are three types of claims:
- Registered Claims: Predefined claims that are not mandatory but recommended
- Public Claims: Defined by those using JWTs (should be registered to avoid collisions)
- Private Claims: Custom claims created to share information between parties
Registered Claims in Detail
Essential Time-Based Claims
{
"iat": 1697644800, // Issued At: October 18, 2023 10:00:00 UTC
"exp": 1697648400, // Expires: October 18, 2023 11:00:00 UTC
"nbf": 1697644800 // Not Before: October 18, 2023 10:00:00 UTC
}JSONIdentity and Audience Claims
{
"iss": "https://auth.example.com", // Issuer
"sub": "user_123456", // Subject (User ID)
"aud": ["api.example.com", "mobile-app"], // Audience
"jti": "abc123-def456-ghi789" // JWT ID (Unique identifier)
}JSONCustom Claims Best Practices
User Information Claims
{
"user_id": "123456",
"username": "johndoe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"avatar_url": "https://example.com/avatars/john.jpg"
}JSONAuthorization Claims
{
"role": "admin",
"permissions": ["read", "write", "delete"],
"scopes": ["user:read", "user:write", "admin:all"],
"groups": ["administrators", "developers"],
"tenant_id": "company_abc"
}JSONApplication-Specific Claims
{
"subscription_type": "premium",
"last_login": 1697644800,
"login_count": 45,
"preferences": {
"theme": "dark",
"language": "en-US",
"timezone": "America/New_York"
}
}JSONClaim Validation Strategies
graph TD
A[Incoming JWT] --> B[Extract Claims]
B --> C{Check exp}
C -->|Expired| D[Reject Token]
C -->|Valid| E{Check nbf}
E -->|Too Early| D
E -->|Valid| F{Check aud}
F -->|Wrong Audience| D
F -->|Valid| G{Check iss}
G -->|Wrong Issuer| D
G -->|Valid| H[Token Valid]Payload Size Considerations
Keep payload minimal:
// Good: Essential information only
{
"sub": "user123",
"role": "admin",
"exp": 1697648400
}JSON// Avoid: Large payloads
{
"sub": "user123",
"role": "admin",
"exp": 1697648400,
"profile": {
"bio": "Very long biography text...",
"preferences": { /* Large object */ },
"history": [ /* Array of many items */ ]
}
}JSON4. Cryptographic Algorithms
Symmetric vs Asymmetric Algorithms
graph LR
subgraph "Symmetric (HMAC)"
A[Secret Key] --> B[Sign Token]
A --> C[Verify Token]
B --> D[JWT]
D --> C
end
subgraph "Asymmetric (RSA/ECDSA)"
E[Private Key] --> F[Sign Token]
G[Public Key] --> H[Verify Token]
F --> I[JWT]
I --> H
endHMAC Algorithms (Symmetric)
HS256 (HMAC using SHA-256)
Characteristics:
- Single secret key for signing and verification
- Fastest algorithm
- Best for single-service applications
- Secret must be kept secure on all services
Use Case Example:
# FastAPI with HS256
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwtPythonHS384 and HS512
HS384: Uses SHA-384 hash function HS512: Uses SHA-512 hash function
# Different HMAC algorithms
algorithms_comparison = {
"HS256": {"hash_size": 256, "speed": "fastest", "security": "good"},
"HS384": {"hash_size": 384, "speed": "medium", "security": "better"},
"HS512": {"hash_size": 512, "speed": "slower", "security": "best"}
}PythonRSA Algorithms (Asymmetric)
RS256 (RSA using SHA-256)
Characteristics:
- Uses RSA key pairs (private/public)
- Private key for signing, public key for verification
- Perfect for microservices and distributed systems
- Public key can be shared safely
Key Generation:
# Generate RSA private key
openssl genrsa -out private_key.pem 2048
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pemBashImplementation:
# FastAPI with RS256
from cryptography.hazmat.primitives import serialization
def load_private_key():
with open("private_key.pem", "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
)
return private_key
def load_public_key():
with open("public_key.pem", "rb") as key_file:
public_key = serialization.load_pem_public_key(key_file.read())
return public_key
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
private_key = load_private_key()
encoded_jwt = jwt.encode(to_encode, private_key, algorithm="RS256")
return encoded_jwtPythonECDSA Algorithms (Asymmetric)
ES256 (ECDSA using P-256 and SHA-256)
Characteristics:
- Elliptic Curve Digital Signature Algorithm
- Smaller key sizes than RSA
- Better performance than RSA
- Same security level with smaller keys
Key Generation:
# Generate ECDSA private key
openssl ecparam -genkey -name prime256v1 -noout -out ec_private_key.pem
# Extract public key
openssl ec -in ec_private_key.pem -pubout -out ec_public_key.pemBashAlgorithm Security Comparison
| Algorithm | Type | Key Size | Security Level | Performance | Use Case |
|---|---|---|---|---|---|
| HS256 | HMAC | 256-bit | Good | Fastest | Single service |
| HS384 | HMAC | 384-bit | Better | Fast | Single service |
| HS512 | HMAC | 512-bit | Best | Fast | Single service |
| RS256 | RSA | 2048-bit | Good | Medium | Microservices |
| RS384 | RSA | 2048-bit | Better | Medium | Microservices |
| RS512 | RSA | 2048-bit | Best | Slower | High security |
| ES256 | ECDSA | 256-bit | Good | Fast | Modern apps |
| ES384 | ECDSA | 384-bit | Better | Fast | Modern apps |
| ES512 | ECDSA | 521-bit | Best | Fast | High security |
Algorithm Selection Guide
flowchart TD
A[Choose Algorithm] --> B{Single Service?}
B -->|Yes| C[Use HMAC]
B -->|No| D[Use Asymmetric]
C --> E{Security Level?}
E -->|Standard| F[HS256]
E -->|High| G[HS512]
D --> H{Key Size Preference?}
H -->|Smaller Keys| I[ECDSA]
H -->|Standard| J[RSA]
I --> K{Security Level?}
K -->|Standard| L[ES256]
K -->|High| M[ES512]
J --> N{Security Level?}
N -->|Standard| O[RS256]
N -->|High| P[RS512]Dangerous Algorithms to Avoid
The “none” Algorithm
Never use the “none” algorithm:
{
"alg": "none",
"typ": "JWT"
}JSONThis bypasses signature verification entirely and should never be accepted in production.
Weak Algorithms
Avoid deprecated or weak algorithms:
- MD5-based algorithms
- SHA-1-based algorithms
- Weak RSA key sizes (<2048 bits)
5. Setting Up Development Environment
Prerequisites
Before we start building JWT applications, let’s set up our development environment with all necessary tools and dependencies.
System Requirements
- Python 3.8+ for FastAPI backend
- Node.js 16+ for frontend tooling (optional)
- Git for version control
- VS Code or your preferred IDE
- Postman or similar API testing tool
Project Structure
Let’s create a well-organized project structure:
jwt-guide-project/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── auth/
│ │ │ ├── __init__.py
│ │ │ ├── jwt_handler.py
│ │ │ └── models.py
│ │ ├── routers/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ └── users.py
│ │ └── database/
│ │ ├── __init__.py
│ │ └── database.py
│ ├── requirements.txt
│ └── .env
├── frontend/
│ ├── index.html
│ ├── css/
│ │ └── styles.css
│ ├── js/
│ │ ├── auth.js
│ │ ├── api.js
│ │ └── main.js
│ └── assets/
└── README.mdBashBackend Setup (FastAPI)
Installing Dependencies
Create backend/requirements.txt:
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
python-decouple==3.8
sqlalchemy==2.0.23
databases[sqlite]==0.8.0
aiosqlite==0.19.0TOMLEnvironment Configuration
Create backend/.env:
# JWT Configuration
SECRET_KEY=your-super-secret-key-here-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
# Database Configuration
DATABASE_URL=sqlite:///./app.db
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://127.0.0.1:5500TOMLBasic FastAPI Application
Create backend/app/main.py:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from decouple import config
import uvicorn
# Import our modules (we'll create these)
from app.auth.jwt_handler import JWTHandler
from app.routers import auth, users
# Initialize FastAPI app
app = FastAPI(
title="JWT Authentication API",
description="A comprehensive JWT authentication system",
version="1.0.0"
)
# CORS Configuration
origins = config("ALLOWED_ORIGINS", default="http://localhost:3000").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Security scheme
security = HTTPBearer()
# JWT Handler instance
jwt_handler = JWTHandler()
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/users", tags=["Users"])
@app.get("/")
async def root():
return {"message": "JWT Authentication API is running!"}
@app.get("/protected")
async def protected_route(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Example protected route"""
token = credentials.credentials
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
return {"message": f"Hello {payload.get('sub')}!", "data": payload}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)PythonFrontend Setup
Basic HTML Structure
Create frontend/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JWT Authentication Demo</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<header>
<h1>JWT Authentication Demo</h1>
<nav id="navigation">
<!-- Navigation will be populated by JavaScript -->
</nav>
</header>
<main id="main-content">
<!-- Login Form -->
<div id="login-section" class="auth-section">
<h2>Login</h2>
<form id="login-form">
<div class="form-group">
<label for="login-email">Email:</label>
<input type="email" id="login-email" required>
</div>
<div class="form-group">
<label for="login-password">Password:</label>
<input type="password" id="login-password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
<!-- Registration Form -->
<div id="register-section" class="auth-section" style="display: none;">
<h2>Register</h2>
<form id="register-form">
<div class="form-group">
<label for="register-name">Full Name:</label>
<input type="text" id="register-name" required>
</div>
<div class="form-group">
<label for="register-email">Email:</label>
<input type="email" id="register-email" required>
</div>
<div class="form-group">
<label for="register-password">Password:</label>
<input type="password" id="register-password" required>
</div>
<button type="submit">Register</button>
</form>
</div>
<!-- Dashboard -->
<div id="dashboard-section" class="dashboard" style="display: none;">
<h2>Dashboard</h2>
<div id="user-info"></div>
<div id="protected-content"></div>
</div>
</main>
<footer>
<div id="status"></div>
</footer>
</div>
<script src="js/api.js"></script>
<script src="js/auth.js"></script>
<script src="js/main.js"></script>
</body>
</html>HTMLCSS Styling
Create frontend/css/styles.css:
/* Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f4f4;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Header Styles */
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
header h1 {
text-align: center;
margin-bottom: 1rem;
font-size: 2.5rem;
}
nav {
text-align: center;
}
nav button {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 10px 20px;
margin: 0 10px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
nav button:hover {
background: rgba(255, 255, 255, 0.3);
}
nav button.active {
background: rgba(255, 255, 255, 0.4);
}
/* Form Styles */
.auth-section {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 500px;
margin: 0 auto;
}
.auth-section h2 {
text-align: center;
margin-bottom: 1.5rem;
color: #667eea;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #555;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(0);
}
/* Dashboard Styles */
.dashboard {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.dashboard h2 {
color: #667eea;
margin-bottom: 1.5rem;
text-align: center;
}
#user-info {
background: #f8f9fa;
padding: 1rem;
border-radius: 5px;
margin-bottom: 1rem;
}
#protected-content {
background: #e7f3ff;
padding: 1rem;
border-radius: 5px;
border-left: 4px solid #667eea;
}
/* Status Messages */
#status {
margin-top: 1rem;
padding: 1rem;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
header h1 {
font-size: 2rem;
}
nav button {
display: block;
width: 100%;
margin: 5px 0;
}
.auth-section {
padding: 1rem;
}
}
/* Loading Animation */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Token Display */
.token-display {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 1rem;
margin: 1rem 0;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.token-header {
color: #28a745;
font-weight: bold;
}
.token-payload {
color: #007bff;
font-weight: bold;
}
.token-signature {
color: #dc3545;
font-weight: bold;
}CSSDevelopment Tools Setup
VS Code Extensions
Recommended extensions for JWT development:
- Python – Python language support
- Pylance – Python language server
- REST Client – API testing within VS Code
- Thunder Client – Another excellent API testing tool
- Live Server – Live reload for frontend development
- JWT Debugger – JWT token inspection
Postman Collection
Create a Postman collection for testing our API endpoints:
{
"info": {
"name": "JWT Authentication API",
"description": "Collection for testing JWT authentication endpoints",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Authentication",
"item": [
{
"name": "Register User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"password123\",\n \"full_name\": \"John Doe\"\n}"
},
"url": {
"raw": "{{base_url}}/auth/register",
"host": ["{{base_url}}"],
"path": ["auth", "register"]
}
}
},
{
"name": "Login User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "user@example.com"
},
{
"key": "password",
"value": "password123"
}
]
},
"url": {
"raw": "{{base_url}}/auth/login",
"host": ["{{base_url}}"],
"path": ["auth", "login"]
}
}
}
]
}
],
"variable": [
{
"key": "base_url",
"value": "http://localhost:8000"
},
{
"key": "access_token",
"value": ""
}
]
}JSONTesting Environment
Unit Testing Setup
Create backend/tests/test_jwt.py:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.auth.jwt_handler import JWTHandler
client = TestClient(app)
jwt_handler = JWTHandler()
def test_create_and_verify_token():
"""Test JWT token creation and verification"""
payload = {"sub": "test_user", "role": "user"}
token = jwt_handler.create_access_token(payload)
assert token is not None
assert isinstance(token, str)
decoded_payload = jwt_handler.verify_token(token)
assert decoded_payload is not None
assert decoded_payload["sub"] == "test_user"
assert decoded_payload["role"] == "user"
def test_expired_token():
"""Test expired token handling"""
from datetime import datetime, timedelta
import jwt
from decouple import config
# Create an expired token
payload = {
"sub": "test_user",
"exp": datetime.utcnow() - timedelta(minutes=1)
}
expired_token = jwt.encode(
payload,
config("SECRET_KEY"),
algorithm=config("ALGORITHM")
)
decoded_payload = jwt_handler.verify_token(expired_token)
assert decoded_payload is None
def test_invalid_token():
"""Test invalid token handling"""
invalid_token = "invalid.token.here"
decoded_payload = jwt_handler.verify_token(invalid_token)
assert decoded_payload is NonePythonRun tests with:
pytest backend/tests/ -vBash6. Basic JWT Authentication with FastAPI
Now let’s implement a complete JWT authentication system using FastAPI. This chapter covers user registration, login, token creation, and basic protection mechanisms.
JWT Handler Implementation
Create backend/app/auth/jwt_handler.py:
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
import jwt
from decouple import config
from passlib.context import CryptContext
class JWTHandler:
def __init__(self):
self.secret_key = config("SECRET_KEY")
self.algorithm = config("ALGORITHM", default="HS256")
self.access_token_expire_minutes = int(config("ACCESS_TOKEN_EXPIRE_MINUTES", default=30))
self.refresh_token_expire_days = int(config("REFRESH_TOKEN_EXPIRE_DAYS", default=7))
# Password hashing
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(self, data: Dict[str, Any]) -> str:
"""Create a new access token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access"
})
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
def create_refresh_token(self, data: Dict[str, Any]) -> str:
"""Create a new refresh token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "refresh"
})
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Verify and decode a JWT token"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.JWTError:
return None
def hash_password(self, password: str) -> str:
"""Hash a password"""
return self.pwd_context.hash(password)
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return self.pwd_context.verify(plain_password, hashed_password)
def refresh_access_token(self, refresh_token: str) -> Optional[str]:
"""Create a new access token from a refresh token"""
payload = self.verify_token(refresh_token)
if not payload or payload.get("type") != "refresh":
return None
# Create new access token with same user data
new_token_data = {
"sub": payload.get("sub"),
"email": payload.get("email"),
"role": payload.get("role")
}
return self.create_access_token(new_token_data)PythonUser Models
Create backend/app/auth/models.py:
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
password: str
full_name: str
class UserResponse(BaseModel):
id: int
email: str
full_name: str
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class UserLogin(BaseModel):
username: str # We'll use email as username
password: str
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenPayload(BaseModel):
sub: Optional[str] = None
exp: Optional[int] = None
iat: Optional[int] = None
type: Optional[str] = None
class RefreshTokenRequest(BaseModel):
refresh_token: strPythonDatabase Setup
Create backend/app/database/database.py:
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from databases import Database
from decouple import config
DATABASE_URL = config("DATABASE_URL", default="sqlite:///./app.db")
# Database instance
database = Database(DATABASE_URL)
# SQLAlchemy metadata
metadata = MetaData()
# Users table
users_table = Table(
"users",
metadata,
Column("id", Integer, primary_key=True, index=True),
Column("email", String, unique=True, index=True, nullable=False),
Column("full_name", String, nullable=False),
Column("hashed_password", String, nullable=False),
Column("is_active", Boolean, default=True),
Column("created_at", DateTime(timezone=True), server_default=func.now()),
Column("updated_at", DateTime(timezone=True), onupdate=func.now()),
)
# Create engine
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
# Create tables
metadata.create_all(bind=engine)
async def connect_db():
"""Connect to database"""
await database.connect()
async def disconnect_db():
"""Disconnect from database"""
await database.disconnect()PythonAuthentication Router
Create backend/app/routers/auth.py:
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import select
from typing import Dict, Any
from app.auth.jwt_handler import JWTHandler
from app.auth.models import UserCreate, UserResponse, Token, RefreshTokenRequest
from app.database.database import database, users_table
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
jwt_handler = JWTHandler()
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register_user(user: UserCreate):
"""Register a new user"""
# Check if user already exists
query = select(users_table).where(users_table.c.email == user.email)
existing_user = await database.fetch_one(query)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Hash password
hashed_password = jwt_handler.hash_password(user.password)
# Insert new user
query = users_table.insert().values(
email=user.email,
full_name=user.full_name,
hashed_password=hashed_password
)
user_id = await database.execute(query)
# Fetch and return the created user
query = select(users_table).where(users_table.c.id == user_id)
created_user = await database.fetch_one(query)
return UserResponse(**created_user)
@router.post("/login", response_model=Token)
async def login_user(form_data: OAuth2PasswordRequestForm = Depends()):
"""Authenticate user and return tokens"""
# Find user by email
query = select(users_table).where(users_table.c.email == form_data.username)
user = await database.fetch_one(query)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Verify password
if not jwt_handler.verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Check if user is active
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Account is disabled"
)
# Create tokens
token_data = {
"sub": str(user.id),
"email": user.email,
"full_name": user.full_name,
"role": "user" # Default role
}
access_token = jwt_handler.create_access_token(token_data)
refresh_token = jwt_handler.create_refresh_token({"sub": str(user.id)})
return Token(
access_token=access_token,
refresh_token=refresh_token
)
@router.post("/refresh", response_model=Dict[str, str])
async def refresh_token(refresh_request: RefreshTokenRequest):
"""Refresh access token using refresh token"""
new_access_token = jwt_handler.refresh_access_token(refresh_request.refresh_token)
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
return {"access_token": new_access_token, "token_type": "bearer"}
@router.get("/me", response_model=UserResponse)
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""Get current user information"""
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
# Fetch user from database
query = select(users_table).where(users_table.c.id == int(user_id))
user = await database.fetch_one(query)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(**user)
@router.post("/logout")
async def logout_user(token: str = Depends(oauth2_scheme)):
"""Logout user (in a real application, you might want to blacklist the token)"""
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# In a production application, you would add the token to a blacklist
# For now, we'll just return a success message
return {"message": "Successfully logged out"}PythonProtected Routes
Create backend/app/routers/users.py:
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import select
from typing import List
from app.auth.jwt_handler import JWTHandler
from app.auth.models import UserResponse
from app.database.database import database, users_table
router = APIRouter()
security = HTTPBearer()
jwt_handler = JWTHandler()
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Dependency to get current user from JWT token"""
token = credentials.credentials
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
# Fetch user from database
query = select(users_table).where(users_table.c.id == int(user_id))
user = await database.fetch_one(query)
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found or inactive"
)
return user
async def require_admin(current_user = Depends(get_current_user)):
"""Dependency to require admin role"""
# In a real application, you would check the user's role
# For this example, we'll assume all users can access admin endpoints
# You would typically have a role field in your user model
return current_user
@router.get("/profile", response_model=UserResponse)
async def get_user_profile(current_user = Depends(get_current_user)):
"""Get current user's profile"""
return UserResponse(**current_user)
@router.get("/all", response_model=List[UserResponse])
async def get_all_users(current_user = Depends(require_admin)):
"""Get all users (admin only)"""
query = select(users_table)
users = await database.fetch_all(query)
return [UserResponse(**user) for user in users]
@router.get("/protected-data")
async def get_protected_data(current_user = Depends(get_current_user)):
"""Example endpoint that requires authentication"""
return {
"message": f"Hello {current_user.full_name}!",
"data": {
"user_id": current_user.id,
"email": current_user.email,
"timestamp": "2023-10-18T10:00:00Z",
"secret_data": "This data is only available to authenticated users"
}
}PythonUpdated Main Application
Update backend/app/main.py to include database connection:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from decouple import config
import uvicorn
# Import our modules
from app.auth.jwt_handler import JWTHandler
from app.routers import auth, users
from app.database.database import connect_db, disconnect_db
# Initialize FastAPI app
app = FastAPI(
title="JWT Authentication API",
description="A comprehensive JWT authentication system",
version="1.0.0"
)
# CORS Configuration
origins = config("ALLOWED_ORIGINS", default="http://localhost:3000").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Security scheme
security = HTTPBearer()
# JWT Handler instance
jwt_handler = JWTHandler()
# Database event handlers
@app.on_event("startup")
async def startup():
await connect_db()
print("Database connected")
@app.on_event("shutdown")
async def shutdown():
await disconnect_db()
print("Database disconnected")
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/users", tags=["Users"])
@app.get("/")
async def root():
return {
"message": "JWT Authentication API is running!",
"docs": "/docs",
"redoc": "/redoc"
}
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": "2023-10-18T10:00:00Z"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)PythonAuthentication Flow Diagram
sequenceDiagram
participant C as Client
participant API as FastAPI
participant DB as Database
participant JWT as JWT Handler
Note over C,JWT: User Registration
C->>API: POST /auth/register
API->>DB: Check if user exists
DB->>API: User not found
API->>JWT: Hash password
JWT->>API: Hashed password
API->>DB: Create user
DB->>API: User created
API->>C: User response
Note over C,JWT: User Login
C->>API: POST /auth/login
API->>DB: Find user by email
DB->>API: User data
API->>JWT: Verify password
JWT->>API: Password valid
API->>JWT: Create tokens
JWT->>API: Access & refresh tokens
API->>C: Token response
Note over C,JWT: Protected Request
C->>API: GET /users/profile (with token)
API->>JWT: Verify token
JWT->>API: Token valid + payload
API->>DB: Get user details
DB->>API: User data
API->>C: Protected dataTesting the Authentication API
Manual Testing with curl
# Register a new user
curl -X POST "http://localhost:8000/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123",
"full_name": "John Doe"
}'
# Login
curl -X POST "http://localhost:8000/auth/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=john@example.com&password=password123"
# Access protected route (replace TOKEN with actual token)
curl -X GET "http://localhost:8000/users/profile" \
-H "Authorization: Bearer TOKEN"
# Refresh token
curl -X POST "http://localhost:8000/auth/refresh" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "REFRESH_TOKEN"}'BashAutomated Testing
Create backend/tests/test_auth.py:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database.database import database, users_table
client = TestClient(app)
@pytest.fixture(scope="module")
async def setup_db():
await database.connect()
yield
# Cleanup
await database.execute(users_table.delete())
await database.disconnect()
def test_register_user(setup_db):
"""Test user registration"""
response = client.post(
"/auth/register",
json={
"email": "test@example.com",
"password": "testpassword123",
"full_name": "Test User"
}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert data["full_name"] == "Test User"
assert "id" in data
def test_register_duplicate_user():
"""Test registration with duplicate email"""
# First registration
client.post(
"/auth/register",
json={
"email": "duplicate@example.com",
"password": "password123",
"full_name": "First User"
}
)
# Duplicate registration
response = client.post(
"/auth/register",
json={
"email": "duplicate@example.com",
"password": "password123",
"full_name": "Second User"
}
)
assert response.status_code == 400
assert "already registered" in response.json()["detail"]
def test_login_success():
"""Test successful login"""
# First register a user
client.post(
"/auth/register",
json={
"email": "login@example.com",
"password": "loginpassword",
"full_name": "Login User"
}
)
# Then login
response = client.post(
"/auth/login",
data={
"username": "login@example.com",
"password": "loginpassword"
}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
def test_login_invalid_credentials():
"""Test login with invalid credentials"""
response = client.post(
"/auth/login",
data={
"username": "nonexistent@example.com",
"password": "wrongpassword"
}
)
assert response.status_code == 401
assert "Invalid email or password" in response.json()["detail"]
def test_protected_route_with_valid_token():
"""Test accessing protected route with valid token"""
# Register and login to get token
client.post(
"/auth/register",
json={
"email": "protected@example.com",
"password": "password123",
"full_name": "Protected User"
}
)
login_response = client.post(
"/auth/login",
data={
"username": "protected@example.com",
"password": "password123"
}
)
token = login_response.json()["access_token"]
# Access protected route
response = client.get(
"/users/profile",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "protected@example.com"
def test_protected_route_without_token():
"""Test accessing protected route without token"""
response = client.get("/users/profile")
assert response.status_code == 403 # FastAPI returns 403 for missing auth
def test_protected_route_with_invalid_token():
"""Test accessing protected route with invalid token"""
response = client.get(
"/users/profile",
headers={"Authorization": "Bearer invalid_token"}
)
assert response.status_code == 401Python7. Frontend Integration with JavaScript
Now let’s build a complete frontend that integrates with our FastAPI JWT authentication system. This chapter covers client-side token management, API communication, and user interface components.
API Communication Module
Create frontend/js/api.js:
/**
* API Configuration and HTTP Client
*/
class ApiClient {
constructor(baseUrl = 'http://localhost:8000') {
this.baseUrl = baseUrl;
this.token = this.getStoredToken();
}
/**
* Get stored token from localStorage
*/
getStoredToken() {
return localStorage.getItem('access_token');
}
/**
* Store token in localStorage
*/
storeToken(token) {
localStorage.setItem('access_token', token);
this.token = token;
}
/**
* Remove token from localStorage
*/
removeToken() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
this.token = null;
}
/**
* Get default headers for API requests
*/
getHeaders(includeAuth = true) {
const headers = {
'Content-Type': 'application/json',
};
if (includeAuth && this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers;
}
/**
* Generic HTTP request method
*/
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: this.getHeaders(options.includeAuth !== false),
...options,
};
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || `HTTP error! status: ${response.status}`);
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
/**
* GET request
*/
async get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
/**
* POST request
*/
async post(endpoint, data = null, options = {}) {
const config = {
...options,
method: 'POST',
};
if (data) {
config.body = JSON.stringify(data);
}
return this.request(endpoint, config);
}
/**
* Register a new user
*/
async register(userData) {
return this.post('/auth/register', userData, { includeAuth: false });
}
/**
* Login user
*/
async login(email, password) {
const formData = new FormData();
formData.append('username', email);
formData.append('password', password);
const response = await fetch(`${this.baseUrl}/auth/login`, {
method: 'POST',
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Login failed');
}
// Store tokens
this.storeToken(data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return data;
}
/**
* Refresh access token
*/
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await this.post('/auth/refresh', {
refresh_token: refreshToken
}, { includeAuth: false });
this.storeToken(response.access_token);
return response;
} catch (error) {
// Refresh failed, remove all tokens
this.removeToken();
throw error;
}
}
/**
* Get current user profile
*/
async getCurrentUser() {
try {
return await this.get('/auth/me');
} catch (error) {
// If token is invalid, try to refresh
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
try {
await this.refreshToken();
return await this.get('/auth/me');
} catch (refreshError) {
throw new Error('Authentication failed');
}
}
throw error;
}
}
/**
* Get user profile
*/
async getUserProfile() {
return this.get('/users/profile');
}
/**
* Get protected data
*/
async getProtectedData() {
return this.get('/users/protected-data');
}
/**
* Logout user
*/
async logout() {
try {
await this.post('/auth/logout');
} catch (error) {
console.warn('Logout request failed:', error);
} finally {
this.removeToken();
}
}
/**
* Check if user is authenticated
*/
isAuthenticated() {
return !!this.token;
}
}
// Create global API client instance
const api = new ApiClient();JavaScriptAuthentication Management
Create frontend/js/auth.js:
/**
* Authentication Management Class
*/
class AuthManager {
constructor(apiClient) {
this.api = apiClient;
this.currentUser = null;
this.authCallbacks = [];
}
/**
* Add callback for authentication state changes
*/
onAuthStateChange(callback) {
this.authCallbacks.push(callback);
}
/**
* Notify all callbacks of authentication state change
*/
notifyAuthStateChange(isAuthenticated, user = null) {
this.authCallbacks.forEach(callback => {
callback(isAuthenticated, user);
});
}
/**
* Initialize authentication state
*/
async init() {
if (this.api.isAuthenticated()) {
try {
this.currentUser = await this.api.getCurrentUser();
this.notifyAuthStateChange(true, this.currentUser);
return true;
} catch (error) {
console.error('Failed to get current user:', error);
this.api.removeToken();
this.notifyAuthStateChange(false);
return false;
}
} else {
this.notifyAuthStateChange(false);
return false;
}
}
/**
* Register a new user
*/
async register(userData) {
try {
const user = await this.api.register(userData);
this.showStatus('Registration successful! Please log in.', 'success');
return user;
} catch (error) {
this.showStatus(`Registration failed: ${error.message}`, 'error');
throw error;
}
}
/**
* Login user
*/
async login(email, password) {
try {
const tokens = await this.api.login(email, password);
this.currentUser = await this.api.getCurrentUser();
this.notifyAuthStateChange(true, this.currentUser);
this.showStatus('Login successful!', 'success');
return tokens;
} catch (error) {
this.showStatus(`Login failed: ${error.message}`, 'error');
throw error;
}
}
/**
* Logout user
*/
async logout() {
try {
await this.api.logout();
this.currentUser = null;
this.notifyAuthStateChange(false);
this.showStatus('Logged out successfully', 'info');
} catch (error) {
console.error('Logout error:', error);
// Still clear local state even if API call fails
this.currentUser = null;
this.notifyAuthStateChange(false);
}
}
/**
* Get current user
*/
getCurrentUser() {
return this.currentUser;
}
/**
* Check if user is authenticated
*/
isAuthenticated() {
return this.api.isAuthenticated() && this.currentUser !== null;
}
/**
* Show status message to user
*/
showStatus(message, type = 'info') {
const statusElement = document.getElementById('status');
if (statusElement) {
statusElement.textContent = message;
statusElement.className = `status-${type}`;
// Clear status after 5 seconds
setTimeout(() => {
statusElement.textContent = '';
statusElement.className = '';
}, 5000);
}
}
/**
* Decode JWT token payload (client-side inspection only)
*/
decodeToken(token) {
try {
const payload = token.split('.')[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
}
/**
* Get token information for debugging
*/
getTokenInfo() {
const token = this.api.getStoredToken();
if (!token) return null;
const payload = this.decodeToken(token);
if (!payload) return null;
return {
token: token,
payload: payload,
isExpired: payload.exp < Date.now() / 1000,
expiresAt: new Date(payload.exp * 1000),
issuedAt: new Date(payload.iat * 1000)
};
}
}
// Create global auth manager instance
const auth = new AuthManager(api);JavaScriptMain Application Logic
Create frontend/js/main.js:
/**
* Main Application Class
*/
class App {
constructor() {
this.currentView = 'login';
this.init();
}
/**
* Initialize the application
*/
async init() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
}
/**
* Setup the application
*/
async setup() {
console.log('Initializing JWT Demo App...');
// Initialize authentication
const isAuthenticated = await auth.init();
// Setup authentication state change listener
auth.onAuthStateChange((authenticated, user) => {
this.handleAuthStateChange(authenticated, user);
});
// Setup event listeners
this.setupEventListeners();
// Show appropriate view
if (isAuthenticated) {
this.showDashboard();
} else {
this.showLogin();
}
console.log('App initialized successfully');
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Login form
const loginForm = document.getElementById('login-form');
if (loginForm) {
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
}
// Register form
const registerForm = document.getElementById('register-form');
if (registerForm) {
registerForm.addEventListener('submit', (e) => this.handleRegister(e));
}
// Navigation buttons (will be created dynamically)
document.addEventListener('click', (e) => {
if (e.target.matches('[data-action]')) {
this.handleNavigation(e.target.dataset.action);
}
});
}
/**
* Handle authentication state changes
*/
handleAuthStateChange(authenticated, user) {
console.log('Auth state changed:', { authenticated, user });
this.updateNavigation(authenticated);
if (authenticated) {
this.showDashboard();
} else {
this.showLogin();
}
}
/**
* Update navigation based on authentication state
*/
updateNavigation(authenticated) {
const nav = document.getElementById('navigation');
if (authenticated) {
nav.innerHTML = `
<button data-action="dashboard" class="nav-btn">Dashboard</button>
<button data-action="profile" class="nav-btn">Profile</button>
<button data-action="logout" class="nav-btn">Logout</button>
`;
} else {
nav.innerHTML = `
<button data-action="login" class="nav-btn">Login</button>
<button data-action="register" class="nav-btn">Register</button>
`;
}
// Update active button
this.updateActiveNavButton();
}
/**
* Update active navigation button
*/
updateActiveNavButton() {
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.action === this.currentView) {
btn.classList.add('active');
}
});
}
/**
* Handle navigation actions
*/
async handleNavigation(action) {
switch (action) {
case 'login':
this.showLogin();
break;
case 'register':
this.showRegister();
break;
case 'dashboard':
this.showDashboard();
break;
case 'profile':
this.showProfile();
break;
case 'logout':
await this.handleLogout();
break;
default:
console.warn('Unknown navigation action:', action);
}
}
/**
* Handle login form submission
*/
async handleLogin(event) {
event.preventDefault();
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
if (!email || !password) {
auth.showStatus('Please fill in all fields', 'error');
return;
}
try {
this.setLoading(true);
await auth.login(email, password);
// Clear form
document.getElementById('login-form').reset();
} catch (error) {
console.error('Login error:', error);
} finally {
this.setLoading(false);
}
}
/**
* Handle register form submission
*/
async handleRegister(event) {
event.preventDefault();
const name = document.getElementById('register-name').value;
const email = document.getElementById('register-email').value;
const password = document.getElementById('register-password').value;
if (!name || !email || !password) {
auth.showStatus('Please fill in all fields', 'error');
return;
}
if (password.length < 6) {
auth.showStatus('Password must be at least 6 characters', 'error');
return;
}
try {
this.setLoading(true);
await auth.register({
full_name: name,
email: email,
password: password
});
// Clear form and switch to login
document.getElementById('register-form').reset();
this.showLogin();
} catch (error) {
console.error('Registration error:', error);
} finally {
this.setLoading(false);
}
}
/**
* Handle logout
*/
async handleLogout() {
try {
this.setLoading(true);
await auth.logout();
} catch (error) {
console.error('Logout error:', error);
} finally {
this.setLoading(false);
}
}
/**
* Show login view
*/
showLogin() {
this.currentView = 'login';
this.hideAllSections();
document.getElementById('login-section').style.display = 'block';
this.updateActiveNavButton();
}
/**
* Show register view
*/
showRegister() {
this.currentView = 'register';
this.hideAllSections();
document.getElementById('register-section').style.display = 'block';
this.updateActiveNavButton();
}
/**
* Show dashboard view
*/
async showDashboard() {
this.currentView = 'dashboard';
this.hideAllSections();
document.getElementById('dashboard-section').style.display = 'block';
this.updateActiveNavButton();
try {
await this.loadDashboardData();
} catch (error) {
console.error('Failed to load dashboard data:', error);
auth.showStatus('Failed to load dashboard data', 'error');
}
}
/**
* Show profile view
*/
async showProfile() {
this.currentView = 'profile';
await this.showDashboard(); // Reuse dashboard for now
}
/**
* Load dashboard data
*/
async loadDashboardData() {
const userInfoElement = document.getElementById('user-info');
const protectedContentElement = document.getElementById('protected-content');
try {
// Load user profile
const userProfile = await api.getUserProfile();
userInfoElement.innerHTML = `
<h3>User Information</h3>
<p><strong>Name:</strong> ${userProfile.full_name}</p>
<p><strong>Email:</strong> ${userProfile.email}</p>
<p><strong>ID:</strong> ${userProfile.id}</p>
<p><strong>Active:</strong> ${userProfile.is_active ? 'Yes' : 'No'}</p>
<p><strong>Member since:</strong> ${new Date(userProfile.created_at).toLocaleDateString()}</p>
`;
// Load protected data
const protectedData = await api.getProtectedData();
protectedContentElement.innerHTML = `
<h3>Protected Content</h3>
<p><strong>Message:</strong> ${protectedData.message}</p>
<p><strong>Secret Data:</strong> ${protectedData.data.secret_data}</p>
<p><strong>Timestamp:</strong> ${protectedData.data.timestamp}</p>
`;
// Show token information (for debugging)
this.showTokenInfo();
} catch (error) {
userInfoElement.innerHTML = '<p class="error">Failed to load user information</p>';
protectedContentElement.innerHTML = '<p class="error">Failed to load protected content</p>';
throw error;
}
}
/**
* Show token information for debugging
*/
showTokenInfo() {
const tokenInfo = auth.getTokenInfo();
if (!tokenInfo) return;
const tokenDisplay = document.createElement('div');
tokenDisplay.className = 'token-display';
tokenDisplay.innerHTML = `
<h4>Token Information (Debug)</h4>
<p><strong>Expires:</strong> ${tokenInfo.expiresAt.toLocaleString()}</p>
<p><strong>Issued:</strong> ${tokenInfo.issuedAt.toLocaleString()}</p>
<p><strong>Expired:</strong> ${tokenInfo.isExpired ? 'Yes' : 'No'}</p>
<details>
<summary>Token Payload</summary>
<pre>${JSON.stringify(tokenInfo.payload, null, 2)}</pre>
</details>
`;
const protectedContentElement = document.getElementById('protected-content');
protectedContentElement.appendChild(tokenDisplay);
}
/**
* Hide all sections
*/
hideAllSections() {
document.querySelectorAll('.auth-section, .dashboard').forEach(section => {
section.style.display = 'none';
});
}
/**
* Set loading state
*/
setLoading(loading) {
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
if (loading) {
button.disabled = true;
if (button.type === 'submit') {
button.innerHTML = '<span class="loading"></span> Loading...';
}
} else {
button.disabled = false;
if (button.type === 'submit') {
button.textContent = button.textContent.includes('Login') ? 'Login' : 'Register';
}
}
});
}
}
// Initialize the application
const app = new App();JavaScriptToken Visualization Component
Let’s add a token visualization component to help understand JWT structure:
/**
* JWT Token Visualizer
*/
class TokenVisualizer {
constructor() {
this.createVisualizerHTML();
}
/**
* Create visualizer HTML structure
*/
createVisualizerHTML() {
const container = document.createElement('div');
container.id = 'token-visualizer';
container.className = 'token-visualizer';
container.innerHTML = `
<h3>JWT Token Visualizer</h3>
<div class="token-input-section">
<textarea id="token-input" placeholder="Paste JWT token here..."></textarea>
<button onclick="tokenVisualizer.visualizeToken()">Analyze Token</button>
</div>
<div id="token-output" class="token-output"></div>
`;
// Add to dashboard
const dashboard = document.getElementById('dashboard-section');
if (dashboard) {
dashboard.appendChild(container);
}
}
/**
* Visualize JWT token structure
*/
visualizeToken() {
const tokenInput = document.getElementById('token-input');
const tokenOutput = document.getElementById('token-output');
const token = tokenInput.value.trim();
if (!token) {
tokenOutput.innerHTML = '<p class="error">Please enter a JWT token</p>';
return;
}
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format - must have 3 parts');
}
const [headerB64, payloadB64, signature] = parts;
// Decode header and payload
const header = JSON.parse(atob(headerB64.replace(/-/g, '+').replace(/_/g, '/')));
const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
// Format output
tokenOutput.innerHTML = `
<div class="token-part">
<h4 class="token-header">Header</h4>
<pre>${JSON.stringify(header, null, 2)}</pre>
<p><strong>Base64:</strong> <code>${headerB64}</code></p>
</div>
<div class="token-part">
<h4 class="token-payload">Payload</h4>
<pre>${JSON.stringify(payload, null, 2)}</pre>
<p><strong>Base64:</strong> <code>${payloadB64}</code></p>
${this.formatPayloadInfo(payload)}
</div>
<div class="token-part">
<h4 class="token-signature">Signature</h4>
<code>${signature}</code>
<p><em>Signature verification requires the secret key</em></p>
</div>
`;
} catch (error) {
tokenOutput.innerHTML = `<p class="error">Error parsing token: ${error.message}</p>`;
}
}
/**
* Format payload information
*/
formatPayloadInfo(payload) {
let info = '<div class="payload-info"><h5>Claim Information:</h5><ul>';
if (payload.exp) {
const expDate = new Date(payload.exp * 1000);
const isExpired = expDate < new Date();
info += `<li><strong>Expires:</strong> ${expDate.toLocaleString()} ${isExpired ? '(EXPIRED)' : ''}</li>`;
}
if (payload.iat) {
const iatDate = new Date(payload.iat * 1000);
info += `<li><strong>Issued At:</strong> ${iatDate.toLocaleString()}</li>`;
}
if (payload.sub) {
info += `<li><strong>Subject:</strong> ${payload.sub}</li>`;
}
if (payload.iss) {
info += `<li><strong>Issuer:</strong> ${payload.iss}</li>`;
}
if (payload.aud) {
info += `<li><strong>Audience:</strong> ${Array.isArray(payload.aud) ? payload.aud.join(', ') : payload.aud}</li>`;
}
info += '</ul></div>';
return info;
}
}
// Initialize token visualizer
const tokenVisualizer = new TokenVisualizer();JavaScriptFrontend Flow Diagram
flowchart TD
A[Page Load] --> B[Initialize App]
B --> C[Check Stored Token]
C --> D{Token Exists?}
D -->|Yes| E[Validate Token with API]
D -->|No| F[Show Login Form]
E --> G{Token Valid?}
G -->|Yes| H[Show Dashboard]
G -->|No| I[Try Refresh Token]
I --> J{Refresh Success?}
J -->|Yes| H
J -->|No| F
F --> K[User Login/Register]
K --> L[API Authentication]
L --> M{Auth Success?}
M -->|Yes| N[Store Tokens]
M -->|No| O[Show Error]
N --> H
O --> F
H --> P[Load Protected Data]
P --> Q[Display User Interface]This frontend implementation provides:
- Token Management: Automatic storage, retrieval, and refresh of JWT tokens
- API Communication: Centralized HTTP client with authentication handling
- User Interface: Clean, responsive interface for authentication and dashboard
- Error Handling: Comprehensive error handling and user feedback
- Token Visualization: Debug tool to inspect JWT token structure
- Automatic Refresh: Transparent token refresh when needed
- Responsive Design: Mobile-friendly CSS layout
The frontend seamlessly integrates with our FastAPI backend and provides a complete user experience for JWT authentication.
8. Token Storage and Management
Proper token storage is crucial for security. This chapter covers different storage options, their security implications, and best practices for token management.
Storage Options Comparison
graph TB
A[JWT Storage Options] --> B[Browser Storage]
A --> C[HTTP Cookies]
A --> D[Memory Only]
B --> E[localStorage]
B --> F[sessionStorage]
B --> G[IndexedDB]
C --> H[HttpOnly Cookies]
C --> I[Secure Cookies]
C --> J[SameSite Cookies]
D --> K[JavaScript Variables]
D --> L[Web Workers]localStorage Implementation
Pros:
- Persists across browser sessions
- Large storage capacity
- Easy to implement
Cons:
- Vulnerable to XSS attacks
- Accessible via JavaScript
- No automatic expiration
class LocalStorageTokenManager {
constructor() {
this.ACCESS_TOKEN_KEY = 'jwt_access_token';
this.REFRESH_TOKEN_KEY = 'jwt_refresh_token';
this.USER_KEY = 'jwt_user_data';
}
/**
* Store tokens in localStorage
*/
storeTokens(accessToken, refreshToken, userData = null) {
try {
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
if (userData) {
localStorage.setItem(this.USER_KEY, JSON.stringify(userData));
}
// Set expiration timestamp
const tokenPayload = this.decodeToken(accessToken);
if (tokenPayload && tokenPayload.exp) {
localStorage.setItem(
'jwt_expires_at',
(tokenPayload.exp * 1000).toString()
);
}
return true;
} catch (error) {
console.error('Failed to store tokens:', error);
return false;
}
}
/**
* Get access token
*/
getAccessToken() {
try {
const token = localStorage.getItem(this.ACCESS_TOKEN_KEY);
// Check if token is expired
if (token && this.isTokenExpired(token)) {
this.clearTokens();
return null;
}
return token;
} catch (error) {
console.error('Failed to get access token:', error);
return null;
}
}
/**
* Get refresh token
*/
getRefreshToken() {
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
}
/**
* Get stored user data
*/
getUserData() {
try {
const userData = localStorage.getItem(this.USER_KEY);
return userData ? JSON.parse(userData) : null;
} catch (error) {
console.error('Failed to get user data:', error);
return null;
}
}
/**
* Clear all stored tokens and data
*/
clearTokens() {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
localStorage.removeItem(this.USER_KEY);
localStorage.removeItem('jwt_expires_at');
}
/**
* Check if token is expired
*/
isTokenExpired(token) {
try {
const payload = this.decodeToken(token);
if (!payload || !payload.exp) return true;
return Date.now() >= payload.exp * 1000;
} catch (error) {
return true;
}
}
/**
* Decode JWT token payload
*/
decodeToken(token) {
try {
const base64Payload = token.split('.')[1];
const payload = atob(base64Payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(payload);
} catch (error) {
return null;
}
}
/**
* Get time until token expiration
*/
getTimeUntilExpiration() {
const token = this.getAccessToken();
if (!token) return 0;
const payload = this.decodeToken(token);
if (!payload || !payload.exp) return 0;
const expirationTime = payload.exp * 1000;
const currentTime = Date.now();
return Math.max(0, expirationTime - currentTime);
}
}JavaScriptSecure Cookie Implementation
Pros:
- HttpOnly cookies prevent XSS access
- Automatic inclusion in requests
- Built-in expiration
Cons:
- Vulnerable to CSRF attacks
- Size limitations
- Complex SameSite configuration
class CookieTokenManager {
constructor() {
this.COOKIE_NAME = 'jwt_token';
this.REFRESH_COOKIE_NAME = 'jwt_refresh';
}
/**
* Set secure HTTP-only cookies (requires server cooperation)
*/
setCookieTokens(accessToken, refreshToken) {
// This would typically be done server-side
// Client-side cookies are less secure
const accessTokenExpiry = this.getTokenExpiration(accessToken);
const refreshTokenExpiry = this.getTokenExpiration(refreshToken);
this.setCookie(this.COOKIE_NAME, accessToken, accessTokenExpiry, {
httpOnly: false, // Can't set HttpOnly from client-side
secure: window.location.protocol === 'https:',
sameSite: 'Strict'
});
this.setCookie(this.REFRESH_COOKIE_NAME, refreshToken, refreshTokenExpiry, {
httpOnly: false,
secure: window.location.protocol === 'https:',
sameSite: 'Strict'
});
}
/**
* Set cookie with options
*/
setCookie(name, value, expiration, options = {}) {
let cookieString = `${name}=${value}`;
if (expiration) {
cookieString += `; expires=${expiration.toUTCString()}`;
}
if (options.secure) {
cookieString += '; Secure';
}
if (options.sameSite) {
cookieString += `; SameSite=${options.sameSite}`;
}
if (options.path) {
cookieString += `; Path=${options.path}`;
} else {
cookieString += '; Path=/';
}
document.cookie = cookieString;
}
/**
* Get cookie value
*/
getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (cookieName === name) {
return cookieValue;
}
}
return null;
}
/**
* Get access token from cookie
*/
getAccessToken() {
return this.getCookie(this.COOKIE_NAME);
}
/**
* Get refresh token from cookie
*/
getRefreshToken() {
return this.getCookie(this.REFRESH_COOKIE_NAME);
}
/**
* Clear cookies
*/
clearTokens() {
this.setCookie(this.COOKIE_NAME, '', new Date(0));
this.setCookie(this.REFRESH_COOKIE_NAME, '', new Date(0));
}
/**
* Get token expiration date
*/
getTokenExpiration(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return new Date(payload.exp * 1000);
} catch (error) {
return new Date(Date.now() + 60 * 60 * 1000); // 1 hour default
}
}
}JavaScriptMemory-Only Storage (Most Secure)
Pros:
- No persistence after page reload
- Not accessible via XSS or storage APIs
- Automatically cleared on navigation
Cons:
- Lost on page refresh
- Complex state management
- Poor user experience
class MemoryTokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.userData = null;
this.expirationTimer = null;
}
/**
* Store tokens in memory
*/
storeTokens(accessToken, refreshToken, userData = null) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.userData = userData;
// Set up automatic token refresh
this.setupTokenRefresh();
return true;
}
/**
* Get access token
*/
getAccessToken() {
if (this.accessToken && this.isTokenExpired(this.accessToken)) {
this.clearTokens();
return null;
}
return this.accessToken;
}
/**
* Get refresh token
*/
getRefreshToken() {
return this.refreshToken;
}
/**
* Get user data
*/
getUserData() {
return this.userData;
}
/**
* Clear tokens from memory
*/
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.userData = null;
if (this.expirationTimer) {
clearTimeout(this.expirationTimer);
this.expirationTimer = null;
}
}
/**
* Check if token is expired
*/
isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= payload.exp * 1000;
} catch (error) {
return true;
}
}
/**
* Setup automatic token refresh
*/
setupTokenRefresh() {
if (this.expirationTimer) {
clearTimeout(this.expirationTimer);
}
if (!this.accessToken) return;
try {
const payload = JSON.parse(atob(this.accessToken.split('.')[1]));
const expirationTime = payload.exp * 1000;
const currentTime = Date.now();
// Refresh 5 minutes before expiration
const refreshTime = expirationTime - currentTime - (5 * 60 * 1000);
if (refreshTime > 0) {
this.expirationTimer = setTimeout(() => {
this.attemptTokenRefresh();
}, refreshTime);
}
} catch (error) {
console.error('Failed to setup token refresh:', error);
}
}
/**
* Attempt to refresh token
*/
async attemptTokenRefresh() {
if (!this.refreshToken) return;
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: this.refreshToken
})
});
if (response.ok) {
const data = await response.json();
this.accessToken = data.access_token;
this.setupTokenRefresh();
// Notify application of token refresh
window.dispatchEvent(new CustomEvent('tokenRefreshed', {
detail: { accessToken: this.accessToken }
}));
} else {
// Refresh failed, clear tokens
this.clearTokens();
window.dispatchEvent(new CustomEvent('tokenExpired'));
}
} catch (error) {
console.error('Token refresh failed:', error);
this.clearTokens();
window.dispatchEvent(new CustomEvent('tokenExpired'));
}
}
}JavaScriptHybrid Storage Strategy
Combine multiple storage methods for optimal security and user experience:
class HybridTokenManager {
constructor(options = {}) {
this.useSecureCookies = options.useSecureCookies || false;
this.fallbackToMemory = options.fallbackToMemory || false;
// Initialize storage managers
this.localStorage = new LocalStorageTokenManager();
this.cookieStorage = new CookieTokenManager();
this.memoryStorage = new MemoryTokenManager();
// Choose primary storage method
this.primaryStorage = this.choosePrimaryStorage();
}
/**
* Choose the best storage method based on environment
*/
choosePrimaryStorage() {
// Check for HTTPS
const isSecure = window.location.protocol === 'https:';
// Check for storage availability
const hasLocalStorage = this.isStorageAvailable('localStorage');
if (this.useSecureCookies && isSecure) {
return this.cookieStorage;
} else if (hasLocalStorage && !this.fallbackToMemory) {
return this.localStorage;
} else {
return this.memoryStorage;
}
}
/**
* Check if storage type is available
*/
isStorageAvailable(type) {
try {
const storage = window[type];
const test = '__storage_test__';
storage.setItem(test, test);
storage.removeItem(test);
return true;
} catch (error) {
return false;
}
}
/**
* Store tokens using primary storage
*/
storeTokens(accessToken, refreshToken, userData = null) {
return this.primaryStorage.storeTokens(accessToken, refreshToken, userData);
}
/**
* Get access token
*/
getAccessToken() {
return this.primaryStorage.getAccessToken();
}
/**
* Get refresh token
*/
getRefreshToken() {
return this.primaryStorage.getRefreshToken();
}
/**
* Get user data
*/
getUserData() {
return this.primaryStorage.getUserData();
}
/**
* Clear all tokens
*/
clearTokens() {
// Clear from all storage methods to be safe
this.localStorage.clearTokens();
this.cookieStorage.clearTokens();
this.memoryStorage.clearTokens();
}
/**
* Get storage type being used
*/
getStorageType() {
if (this.primaryStorage === this.cookieStorage) return 'cookies';
if (this.primaryStorage === this.memoryStorage) return 'memory';
return 'localStorage';
}
}JavaScriptToken Refresh Strategy
class TokenRefreshManager {
constructor(tokenManager, apiClient) {
this.tokenManager = tokenManager;
this.apiClient = apiClient;
this.refreshPromise = null;
this.refreshBuffer = 5 * 60 * 1000; // 5 minutes
}
/**
* Check if token needs refresh
*/
needsRefresh(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const expirationTime = payload.exp * 1000;
const currentTime = Date.now();
return (expirationTime - currentTime) < this.refreshBuffer;
} catch (error) {
return true;
}
}
/**
* Refresh token if needed
*/
async refreshIfNeeded() {
const accessToken = this.tokenManager.getAccessToken();
if (!accessToken || !this.needsRefresh(accessToken)) {
return accessToken;
}
// If refresh is already in progress, wait for it
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.performRefresh();
try {
const result = await this.refreshPromise;
return result;
} finally {
this.refreshPromise = null;
}
}
/**
* Perform token refresh
*/
async performRefresh() {
const refreshToken = this.tokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await this.apiClient.post('/auth/refresh', {
refresh_token: refreshToken
});
const newAccessToken = response.access_token;
// Update stored token
this.tokenManager.storeTokens(
newAccessToken,
refreshToken,
this.tokenManager.getUserData()
);
return newAccessToken;
} catch (error) {
// Refresh failed, clear all tokens
this.tokenManager.clearTokens();
throw new Error('Token refresh failed');
}
}
/**
* Setup automatic refresh
*/
setupAutoRefresh() {
setInterval(async () => {
try {
await this.refreshIfNeeded();
} catch (error) {
console.warn('Auto refresh failed:', error);
// Emit event for application to handle
window.dispatchEvent(new CustomEvent('autoRefreshFailed', {
detail: { error }
}));
}
}, 60000); // Check every minute
}
}JavaScriptSecurity Comparison Table
| Storage Method | XSS Resistance | CSRF Resistance | Persistence | Performance | Complexity |
|---|---|---|---|---|---|
| localStorage | ❌ Low | ✅ High | ✅ High | ✅ High | ✅ Low |
| sessionStorage | ❌ Low | ✅ High | ⚠️ Medium | ✅ High | ✅ Low |
| HttpOnly Cookies | ✅ High | ❌ Low* | ✅ High | ✅ High | ⚠️ Medium |
| Memory Only | ✅ High | ✅ High | ❌ None | ✅ High | ❌ High |
| Hybrid Approach | ⚠️ Medium | ⚠️ Medium | ✅ High | ✅ High | ❌ High |
*Can be mitigated with CSRF tokens and SameSite cookies
Best Practices for Token Storage
- Use HTTPS Always: Never transmit tokens over HTTP
- Implement Token Rotation: Regularly refresh access tokens
- Set Appropriate Expiration: Short-lived access tokens (15-30 minutes)
- Sanitize All Inputs: Prevent XSS attacks
- Implement CSRF Protection: When using cookies
- Monitor for Suspicious Activity: Log and alert on unusual patterns
- Have a Revocation Strategy: Ability to invalidate tokens immediately
9. Authorization and Role-Based Access Control
Authorization determines what authenticated users can do. This chapter covers implementing role-based access control (RBAC) and fine-grained permissions with JWT.
RBAC Architecture
graph TB
A[User] --> B[Assigned Roles]
B --> C[Role 1: Admin]
B --> D[Role 2: Manager]
B --> E[Role 3: User]
C --> F[Admin Permissions]
D --> G[Manager Permissions]
E --> H[User Permissions]
F --> I[Create Users]
F --> J[Delete Users]
F --> K[System Config]
G --> L[Manage Team]
G --> M[View Reports]
G --> N[Approve Requests]
H --> O[View Profile]
H --> P[Update Profile]
H --> Q[Submit Requests]Enhanced User Model with Roles
Update backend/app/database/database.py:
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.sql import func
from databases import Database
from decouple import config
DATABASE_URL = config("DATABASE_URL", default="sqlite:///./app.db")
# Database instance
database = Database(DATABASE_URL)
# SQLAlchemy metadata
metadata = MetaData()
# Roles table
roles_table = Table(
"roles",
metadata,
Column("id", Integer, primary_key=True, index=True),
Column("name", String(50), unique=True, nullable=False),
Column("description", Text),
Column("created_at", DateTime(timezone=True), server_default=func.now()),
)
# Permissions table
permissions_table = Table(
"permissions",
metadata,
Column("id", Integer, primary_key=True, index=True),
Column("name", String(100), unique=True, nullable=False),
Column("description", Text),
Column("resource", String(50), nullable=False),
Column("action", String(50), nullable=False),
Column("created_at", DateTime(timezone=True), server_default=func.now()),
)
# Role-Permission mapping table
role_permissions_table = Table(
"role_permissions",
metadata,
Column("role_id", Integer, ForeignKey("roles.id"), primary_key=True),
Column("permission_id", Integer, ForeignKey("permissions.id"), primary_key=True),
)
# Updated Users table
users_table = Table(
"users",
metadata,
Column("id", Integer, primary_key=True, index=True),
Column("email", String, unique=True, index=True, nullable=False),
Column("full_name", String, nullable=False),
Column("hashed_password", String, nullable=False),
Column("role_id", Integer, ForeignKey("roles.id"), default=1), # Default to 'user' role
Column("is_active", Boolean, default=True),
Column("created_at", DateTime(timezone=True), server_default=func.now()),
Column("updated_at", DateTime(timezone=True), onupdate=func.now()),
)
# Create engine
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
# Create tables
metadata.create_all(bind=engine)
async def connect_db():
"""Connect to database"""
await database.connect()
async def disconnect_db():
"""Disconnect from database"""
await database.disconnect()
async def create_default_roles_and_permissions():
"""Create default roles and permissions"""
# Check if roles already exist
existing_roles = await database.fetch_all("SELECT * FROM roles")
if existing_roles:
return
# Create default roles
admin_role_id = await database.execute(
roles_table.insert().values(
name="admin",
description="System administrator with full access"
)
)
manager_role_id = await database.execute(
roles_table.insert().values(
name="manager",
description="Manager with team oversight capabilities"
)
)
user_role_id = await database.execute(
roles_table.insert().values(
name="user",
description="Regular user with basic access"
)
)
# Create default permissions
permissions = [
# User management
{"name": "user:create", "description": "Create new users", "resource": "user", "action": "create"},
{"name": "user:read", "description": "View user information", "resource": "user", "action": "read"},
{"name": "user:update", "description": "Update user information", "resource": "user", "action": "update"},
{"name": "user:delete", "description": "Delete users", "resource": "user", "action": "delete"},
# Profile management
{"name": "profile:read", "description": "View own profile", "resource": "profile", "action": "read"},
{"name": "profile:update", "description": "Update own profile", "resource": "profile", "action": "update"},
# Reports
{"name": "report:read", "description": "View reports", "resource": "report", "action": "read"},
{"name": "report:create", "description": "Create reports", "resource": "report", "action": "create"},
# System administration
{"name": "system:config", "description": "Configure system settings", "resource": "system", "action": "config"},
{"name": "system:monitor", "description": "Monitor system health", "resource": "system", "action": "monitor"},
]
permission_ids = {}
for perm in permissions:
perm_id = await database.execute(permissions_table.insert().values(**perm))
permission_ids[perm["name"]] = perm_id
# Assign permissions to roles
# Admin gets all permissions
admin_permissions = list(permission_ids.values())
for perm_id in admin_permissions:
await database.execute(
role_permissions_table.insert().values(
role_id=admin_role_id,
permission_id=perm_id
)
)
# Manager permissions
manager_permissions = [
"user:read", "user:update", "profile:read", "profile:update",
"report:read", "report:create", "system:monitor"
]
for perm_name in manager_permissions:
if perm_name in permission_ids:
await database.execute(
role_permissions_table.insert().values(
role_id=manager_role_id,
permission_id=permission_ids[perm_name]
)
)
# User permissions
user_permissions = ["profile:read", "profile:update"]
for perm_name in user_permissions:
if perm_name in permission_ids:
await database.execute(
role_permissions_table.insert().values(
role_id=user_role_id,
permission_id=permission_ids[perm_name]
)
)PythonEnhanced JWT Handler with Permissions
Update backend/app/auth/jwt_handler.py:
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import jwt
from decouple import config
from passlib.context import CryptContext
from sqlalchemy import select
from app.database.database import database, roles_table, permissions_table, role_permissions_table
class JWTHandler:
def __init__(self):
self.secret_key = config("SECRET_KEY")
self.algorithm = config("ALGORITHM", default="HS256")
self.access_token_expire_minutes = int(config("ACCESS_TOKEN_EXPIRE_MINUTES", default=30))
self.refresh_token_expire_days = int(config("REFRESH_TOKEN_EXPIRE_DAYS", default=7))
# Password hashing
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def get_user_permissions(self, user_id: int) -> List[str]:
"""Get user permissions based on their role"""
query = """
SELECT p.name
FROM users u
JOIN roles r ON u.role_id = r.id
JOIN role_permissions rp ON r.id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = :user_id AND u.is_active = true
"""
result = await database.fetch_all(query, {"user_id": user_id})
return [row["name"] for row in result]
async def get_user_role(self, user_id: int) -> Optional[str]:
"""Get user role name"""
query = """
SELECT r.name
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.id = :user_id AND u.is_active = true
"""
result = await database.fetch_one(query, {"user_id": user_id})
return result["name"] if result else None
async def create_access_token(self, user_data: Dict[str, Any]) -> str:
"""Create a new access token with user permissions"""
user_id = user_data.get("id") or user_data.get("sub")
# Get user permissions and role
permissions = await self.get_user_permissions(user_id) if user_id else []
role = await self.get_user_role(user_id) if user_id else "user"
to_encode = {
"sub": str(user_id),
"email": user_data.get("email"),
"full_name": user_data.get("full_name"),
"role": role,
"permissions": permissions,
"exp": datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes),
"iat": datetime.utcnow(),
"type": "access"
}
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
def create_refresh_token(self, data: Dict[str, Any]) -> str:
"""Create a new refresh token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "refresh"
})
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Verify and decode a JWT token"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.JWTError:
return None
def has_permission(self, token_payload: Dict[str, Any], required_permission: str) -> bool:
"""Check if user has required permission"""
user_permissions = token_payload.get("permissions", [])
user_role = token_payload.get("role", "")
# Admin has all permissions
if user_role == "admin":
return True
# Check specific permission
return required_permission in user_permissions
def has_role(self, token_payload: Dict[str, Any], required_role: str) -> bool:
"""Check if user has required role"""
user_role = token_payload.get("role", "")
return user_role == required_role
def hash_password(self, password: str) -> str:
"""Hash a password"""
return self.pwd_context.hash(password)
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return self.pwd_context.verify(plain_password, hashed_password)
async def refresh_access_token(self, refresh_token: str) -> Optional[str]:
"""Create a new access token from a refresh token"""
payload = self.verify_token(refresh_token)
if not payload or payload.get("type") != "refresh":
return None
# Get updated user data
user_data = {
"id": payload.get("sub"),
"email": payload.get("email"),
"full_name": payload.get("full_name")
}
return await self.create_access_token(user_data)PythonPermission Decorators and Dependencies
Create backend/app/auth/permissions.py:
from functools import wraps
from typing import List, Union
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.auth.jwt_handler import JWTHandler
security = HTTPBearer()
jwt_handler = JWTHandler()
class PermissionChecker:
def __init__(self, required_permissions: Union[str, List[str]], require_all: bool = True):
"""
Initialize permission checker
Args:
required_permissions: Single permission or list of permissions
require_all: If True, user must have ALL permissions. If False, ANY permission is sufficient.
"""
if isinstance(required_permissions, str):
self.required_permissions = [required_permissions]
else:
self.required_permissions = required_permissions
self.require_all = require_all
async def __call__(self, credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Check if user has required permissions"""
token = credentials.credentials
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Check permissions
if self.require_all:
# User must have ALL required permissions
for permission in self.required_permissions:
if not jwt_handler.has_permission(payload, permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required permission: {permission}"
)
else:
# User must have ANY of the required permissions
has_any_permission = any(
jwt_handler.has_permission(payload, permission)
for permission in self.required_permissions
)
if not has_any_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing any of required permissions: {', '.join(self.required_permissions)}"
)
return payload
class RoleChecker:
def __init__(self, required_roles: Union[str, List[str]]):
"""
Initialize role checker
Args:
required_roles: Single role or list of roles
"""
if isinstance(required_roles, str):
self.required_roles = [required_roles]
else:
self.required_roles = required_roles
async def __call__(self, credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Check if user has required role"""
token = credentials.credentials
payload = jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
user_role = payload.get("role", "")
if user_role not in self.required_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Required role: {' or '.join(self.required_roles)}"
)
return payload
# Convenience functions for common permission checks
RequireAdmin = RoleChecker("admin")
RequireManager = RoleChecker(["admin", "manager"])
# Common permission dependencies
RequireUserRead = PermissionChecker("user:read")
RequireUserWrite = PermissionChecker(["user:create", "user:update"], require_all=False)
RequireUserDelete = PermissionChecker("user:delete")
RequireProfileAccess = PermissionChecker(["profile:read", "profile:update"], require_all=False)
RequireReportAccess = PermissionChecker("report:read")
RequireReportCreate = PermissionChecker("report:create")
RequireSystemConfig = PermissionChecker("system:config")
RequireSystemMonitor = PermissionChecker("system:monitor")PythonProtected Routes with RBAC
Update backend/app/routers/users.py:
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from typing import List
from app.auth.jwt_handler import JWTHandler
from app.auth.permissions import (
RequireAdmin, RequireManager, RequireUserRead, RequireUserWrite,
RequireUserDelete, RequireProfileAccess, PermissionChecker
)
from app.auth.models import UserResponse, UserCreate
from app.database.database import database, users_table, roles_table
router = APIRouter()
jwt_handler = JWTHandler()
@router.get("/profile", response_model=UserResponse)
async def get_user_profile(current_user = Depends(RequireProfileAccess)):
"""Get current user's profile"""
user_id = current_user.get("sub")
query = select(users_table).where(users_table.c.id == int(user_id))
user = await database.fetch_one(query)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(**user)
@router.get("/all", response_model=List[UserResponse])
async def get_all_users(current_user = Depends(RequireUserRead)):
"""Get all users (requires user:read permission)"""
query = """
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
"""
users = await database.fetch_all(query)
return [UserResponse(**user) for user in users]
@router.post("/create", response_model=UserResponse)
async def create_user(
user_data: UserCreate,
current_user = Depends(PermissionChecker("user:create"))
):
"""Create a new user (admin only)"""
# Check if user already exists
query = select(users_table).where(users_table.c.email == user_data.email)
existing_user = await database.fetch_one(query)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Hash password
hashed_password = jwt_handler.hash_password(user_data.password)
# Insert new user
query = users_table.insert().values(
email=user_data.email,
full_name=user_data.full_name,
hashed_password=hashed_password,
role_id=1 # Default to user role
)
user_id = await database.execute(query)
# Fetch and return the created user
query = select(users_table).where(users_table.c.id == user_id)
created_user = await database.fetch_one(query)
return UserResponse(**created_user)
@router.delete("/{user_id}")
async def delete_user(
user_id: int,
current_user = Depends(RequireUserDelete)
):
"""Delete a user (admin only)"""
# Prevent admin from deleting themselves
current_user_id = int(current_user.get("sub"))
if current_user_id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account"
)
# Check if user exists
query = select(users_table).where(users_table.c.id == user_id)
user = await database.fetch_one(query)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Delete user (or mark as inactive)
delete_query = users_table.update().where(
users_table.c.id == user_id
).values(is_active=False)
await database.execute(delete_query)
return {"message": "User deleted successfully"}
@router.put("/{user_id}/role")
async def update_user_role(
user_id: int,
role_name: str,
current_user = Depends(RequireAdmin)
):
"""Update user role (admin only)"""
# Get role ID
role_query = select(roles_table).where(roles_table.c.name == role_name)
role = await database.fetch_one(role_query)
if not role:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Role not found"
)
# Update user role
update_query = users_table.update().where(
users_table.c.id == user_id
).values(role_id=role.id)
result = await database.execute(update_query)
if result == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"message": f"User role updated to {role_name}"}
@router.get("/admin/stats")
async def get_admin_stats(current_user = Depends(RequireAdmin)):
"""Get admin statistics"""
# Get user count by role
stats_query = """
SELECT r.name as role, COUNT(u.id) as count
FROM roles r
LEFT JOIN users u ON r.id = u.role_id AND u.is_active = true
GROUP BY r.id, r.name
ORDER BY r.name
"""
role_stats = await database.fetch_all(stats_query)
# Get total active users
total_users = await database.fetch_val(
"SELECT COUNT(*) FROM users WHERE is_active = true"
)
return {
"total_users": total_users,
"role_distribution": [
{"role": stat["role"], "count": stat["count"]}
for stat in role_stats
]
}
@router.get("/manager/team")
async def get_team_overview(current_user = Depends(RequireManager)):
"""Get team overview (manager and admin only)"""
team_query = """
SELECT u.id, u.full_name, u.email, r.name as role, u.created_at
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.is_active = true
ORDER BY u.created_at DESC
"""
team_members = await database.fetch_all(team_query)
return {
"team_members": [
{
"id": member["id"],
"name": member["full_name"],
"email": member["email"],
"role": member["role"],
"joined": member["created_at"].isoformat()
}
for member in team_members
]
}PythonFrontend Permission Handling
Create frontend/js/permissions.js:
/**
* Permission Management for Frontend
*/
class PermissionManager {
constructor(authManager) {
this.auth = authManager;
this.userPermissions = [];
this.userRole = '';
// Listen for auth state changes
this.auth.onAuthStateChange((authenticated, user) => {
if (authenticated) {
this.updatePermissions();
} else {
this.clearPermissions();
}
});
}
/**
* Update permissions from current token
*/
updatePermissions() {
const tokenInfo = this.auth.getTokenInfo();
if (tokenInfo && tokenInfo.payload) {
this.userPermissions = tokenInfo.payload.permissions || [];
this.userRole = tokenInfo.payload.role || '';
}
}
/**
* Clear permissions
*/
clearPermissions() {
this.userPermissions = [];
this.userRole = '';
}
/**
* Check if user has specific permission
*/
hasPermission(permission) {
// Admin has all permissions
if (this.userRole === 'admin') {
return true;
}
return this.userPermissions.includes(permission);
}
/**
* Check if user has any of the specified permissions
*/
hasAnyPermission(permissions) {
if (this.userRole === 'admin') {
return true;
}
return permissions.some(permission => this.userPermissions.includes(permission));
}
/**
* Check if user has all of the specified permissions
*/
hasAllPermissions(permissions) {
if (this.userRole === 'admin') {
return true;
}
return permissions.every(permission => this.userPermissions.includes(permission));
}
/**
* Check if user has specific role
*/
hasRole(role) {
return this.userRole === role;
}
/**
* Check if user has any of the specified roles
*/
hasAnyRole(roles) {
return roles.includes(this.userRole);
}
/**
* Get user's current role
*/
getRole() {
return this.userRole;
}
/**
* Get user's permissions
*/
getPermissions() {
return [...this.userPermissions];
}
/**
* Show/hide elements based on permissions
*/
applyPermissionVisibility() {
// Handle data-permission attributes
document.querySelectorAll('[data-permission]').forEach(element => {
const requiredPermission = element.dataset.permission;
if (this.hasPermission(requiredPermission)) {
element.style.display = '';
element.removeAttribute('disabled');
} else {
element.style.display = 'none';
element.setAttribute('disabled', 'true');
}
});
// Handle data-role attributes
document.querySelectorAll('[data-role]').forEach(element => {
const requiredRoles = element.dataset.role.split(',').map(r => r.trim());
if (this.hasAnyRole(requiredRoles)) {
element.style.display = '';
element.removeAttribute('disabled');
} else {
element.style.display = 'none';
element.setAttribute('disabled', 'true');
}
});
// Handle data-any-permission attributes
document.querySelectorAll('[data-any-permission]').forEach(element => {
const requiredPermissions = element.dataset.anyPermission.split(',').map(p => p.trim());
if (this.hasAnyPermission(requiredPermissions)) {
element.style.display = '';
element.removeAttribute('disabled');
} else {
element.style.display = 'none';
element.setAttribute('disabled', 'true');
}
});
// Handle data-all-permissions attributes
document.querySelectorAll('[data-all-permissions]').forEach(element => {
const requiredPermissions = element.dataset.allPermissions.split(',').map(p => p.trim());
if (this.hasAllPermissions(requiredPermissions)) {
element.style.display = '';
element.removeAttribute('disabled');
} else {
element.style.display = 'none';
element.setAttribute('disabled', 'true');
}
});
}
/**
* Create permission-aware button
*/
createPermissionButton(text, permission, clickHandler, className = '') {
const button = document.createElement('button');
button.textContent = text;
button.className = className;
if (this.hasPermission(permission)) {
button.addEventListener('click', clickHandler);
} else {
button.disabled = true;
button.title = `Requires ${permission} permission`;
}
return button;
}
/**
* Create role-aware navigation
*/
createRoleNavigation() {
const navItems = [];
// Basic navigation for all authenticated users
navItems.push({
text: 'Profile',
permission: 'profile:read',
action: () => app.showProfile()
});
// Manager navigation
if (this.hasAnyRole(['manager', 'admin'])) {
navItems.push({
text: 'Team',
permission: 'user:read',
action: () => app.showTeamManagement()
});
navItems.push({
text: 'Reports',
permission: 'report:read',
action: () => app.showReports()
});
}
// Admin navigation
if (this.hasRole('admin')) {
navItems.push({
text: 'User Management',
permission: 'user:create',
action: () => app.showUserManagement()
});
navItems.push({
text: 'System Config',
permission: 'system:config',
action: () => app.showSystemConfig()
});
}
return navItems;
}
}
// Create global permission manager
const permissions = new PermissionManager(auth);JavaScriptUpdated HTML with Permission Attributes
Update frontend/index.html to include permission-based elements:
<!-- Add to dashboard section -->
<div id="dashboard-section" class="dashboard" style="display: none;">
<h2>Dashboard</h2>
<div id="user-info"></div>
<!-- Role-based navigation -->
<div class="role-navigation">
<button data-permission="profile:read" onclick="showUserProfile()">My Profile</button>
<button data-any-role="manager,admin" onclick="showTeamManagement()">Team Management</button>
<button data-permission="report:read" onclick="showReports()">Reports</button>
<button data-role="admin" onclick="showUserManagement()">User Management</button>
<button data-permission="system:config" onclick="showSystemConfig()">System Settings</button>
</div>
<!-- Permission-specific content -->
<div id="user-management" data-permission="user:create" style="display: none;">
<h3>User Management</h3>
<button data-permission="user:create" onclick="createNewUser()">Create User</button>
<div id="user-list"></div>
</div>
<div id="team-management" data-any-role="manager,admin" style="display: none;">
<h3>Team Overview</h3>
<div id="team-list"></div>
</div>
<div id="protected-content"></div>
</div>HTMLThis comprehensive RBAC implementation provides:
- Database Schema: Roles, permissions, and role-permission mappings
- JWT Enhancement: Include permissions and roles in JWT payload
- Backend Protection: Route-level permission and role checking
- Frontend Integration: Permission-aware UI components
- Flexible Authorization: Support for both role-based and permission-based access control
- Scalable Design: Easy to add new roles and permissions
The system allows for fine-grained access control while maintaining simplicity for common use cases.
10. Error Handling and Validation
Robust error handling and input validation are essential for secure JWT implementations. This chapter covers comprehensive error handling strategies and security validation.
JWT Error Types and Handling
graph TB
A[JWT Error Types] --> B[Token Errors]
A --> C[Validation Errors]
A --> D[Security Errors]
B --> E[Expired Token]
B --> F[Invalid Signature]
B --> G[Malformed Token]
C --> H[Missing Claims]
C --> I[Invalid Claims]
C --> J[Audience Mismatch]
D --> K[Algorithm Confusion]
D --> L[Key Compromise]
D --> M[Replay Attacks]Backend Error Handling
Create backend/app/auth/exceptions.py:
from fastapi import HTTPException, status
from typing import Optional, Dict, Any
class JWTError(Exception):
"""Base JWT exception"""
pass
class TokenExpiredError(JWTError):
"""Token has expired"""
pass
class InvalidTokenError(JWTError):
"""Token is invalid"""
pass
class InvalidSignatureError(JWTError):
"""Token signature is invalid"""
pass
class MissingClaimError(JWTError):
"""Required claim is missing"""
pass
class InvalidClaimError(JWTError):
"""Claim value is invalid"""
pass
class InsufficientPermissionsError(JWTError):
"""User lacks required permissions"""
pass
class SecurityError(JWTError):
"""Security-related error"""
pass
def create_http_exception(
error_type: str,
message: str,
status_code: int = status.HTTP_401_UNAUTHORIZED,
headers: Optional[Dict[str, str]] = None
) -> HTTPException:
"""Create standardized HTTP exception"""
detail = {
"error": error_type,
"message": message,
"code": status_code
}
return HTTPException(
status_code=status_code,
detail=detail,
headers=headers or {}
)
# Predefined HTTP exceptions
class AuthenticationExceptions:
INVALID_TOKEN = create_http_exception(
"INVALID_TOKEN",
"The provided token is invalid or malformed",
status.HTTP_401_UNAUTHORIZED,
{"WWW-Authenticate": "Bearer"}
)
EXPIRED_TOKEN = create_http_exception(
"EXPIRED_TOKEN",
"The token has expired",
status.HTTP_401_UNAUTHORIZED,
{"WWW-Authenticate": "Bearer"}
)
MISSING_TOKEN = create_http_exception(
"MISSING_TOKEN",
"Authentication token is required",
status.HTTP_401_UNAUTHORIZED,
{"WWW-Authenticate": "Bearer"}
)
INSUFFICIENT_PERMISSIONS = create_http_exception(
"INSUFFICIENT_PERMISSIONS",
"Insufficient permissions to access this resource",
status.HTTP_403_FORBIDDEN
)
INVALID_CREDENTIALS = create_http_exception(
"INVALID_CREDENTIALS",
"Invalid username or password",
status.HTTP_401_UNAUTHORIZED
)
ACCOUNT_DISABLED = create_http_exception(
"ACCOUNT_DISABLED",
"User account is disabled",
status.HTTP_401_UNAUTHORIZED
)
RATE_LIMITED = create_http_exception(
"RATE_LIMITED",
"Too many requests. Please try again later.",
status.HTTP_429_TOO_MANY_REQUESTS
)PythonEnhanced JWT Handler with Validation
Update backend/app/auth/jwt_handler.py to include comprehensive validation:
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import jwt
from decouple import config
from passlib.context import CryptContext
from sqlalchemy import select
import re
import ipaddress
import hashlib
from app.database.database import database, roles_table, permissions_table, role_permissions_table
from app.auth.exceptions import (
TokenExpiredError, InvalidTokenError, InvalidSignatureError,
MissingClaimError, InvalidClaimError, SecurityError
)
class EnhancedJWTHandler:
def __init__(self):
self.secret_key = config("SECRET_KEY")
self.algorithm = config("ALGORITHM", default="HS256")
self.access_token_expire_minutes = int(config("ACCESS_TOKEN_EXPIRE_MINUTES", default=30))
self.refresh_token_expire_days = int(config("REFRESH_TOKEN_EXPIRE_DAYS", default=7))
self.issuer = config("JWT_ISSUER", default="jwt-auth-api")
self.audience = config("JWT_AUDIENCE", default="jwt-auth-client")
# Security settings
self.max_token_age_days = int(config("MAX_TOKEN_AGE_DAYS", default=30))
self.require_issued_at = config("REQUIRE_ISSUED_AT", default=True, cast=bool)
self.clock_skew_seconds = int(config("CLOCK_SKEW_SECONDS", default=60))
# Password hashing
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Token blacklist (in production, use Redis or database)
self.token_blacklist = set()
def validate_token_structure(self, token: str) -> bool:
"""Validate JWT token structure"""
if not token:
return False
# Check basic structure
parts = token.split('.')
if len(parts) != 3:
return False
# Validate each part is valid base64
try:
for part in parts:
# Add padding if needed
padding = 4 - len(part) % 4
if padding != 4:
part += '=' * padding
jwt.utils.base64url_decode(part)
except Exception:
return False
return True
def validate_algorithm(self, token: str) -> bool:
"""Validate token uses allowed algorithm"""
try:
header = jwt.get_unverified_header(token)
algorithm = header.get('alg')
# Never allow 'none' algorithm
if algorithm == 'none':
raise SecurityError("Algorithm 'none' is not allowed")
# Check against allowed algorithms
allowed_algorithms = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512']
if algorithm not in allowed_algorithms:
raise SecurityError(f"Algorithm '{algorithm}' is not allowed")
return True
except Exception as e:
raise SecurityError(f"Invalid token header: {str(e)}")
def validate_claims(self, payload: Dict[str, Any], client_ip: Optional[str] = None) -> bool:
"""Validate JWT claims"""
current_time = datetime.utcnow()
# Validate expiration (exp)
if 'exp' not in payload:
raise MissingClaimError("Missing 'exp' claim")
exp_time = datetime.fromtimestamp(payload['exp'])
if current_time > exp_time:
raise TokenExpiredError("Token has expired")
# Validate issued at (iat)
if self.require_issued_at:
if 'iat' not in payload:
raise MissingClaimError("Missing 'iat' claim")
iat_time = datetime.fromtimestamp(payload['iat'])
if iat_time > current_time + timedelta(seconds=self.clock_skew_seconds):
raise InvalidClaimError("Token issued in the future")
# Check maximum token age
max_age = timedelta(days=self.max_token_age_days)
if current_time - iat_time > max_age:
raise InvalidClaimError("Token is too old")
# Validate not before (nbf)
if 'nbf' in payload:
nbf_time = datetime.fromtimestamp(payload['nbf'])
if current_time < nbf_time - timedelta(seconds=self.clock_skew_seconds):
raise InvalidClaimError("Token not yet valid")
# Validate issuer (iss)
if 'iss' in payload:
if payload['iss'] != self.issuer:
raise InvalidClaimError(f"Invalid issuer: {payload['iss']}")
# Validate audience (aud)
if 'aud' in payload:
audience = payload['aud']
if isinstance(audience, list):
if self.audience not in audience:
raise InvalidClaimError("Invalid audience")
else:
if audience != self.audience:
raise InvalidClaimError("Invalid audience")
# Validate subject (sub)
if 'sub' not in payload:
raise MissingClaimError("Missing 'sub' claim")
# Validate subject format (should be user ID)
try:
int(payload['sub'])
except ValueError:
raise InvalidClaimError("Invalid subject format")
# Validate JWT ID (jti) if present
if 'jti' in payload:
jti = payload['jti']
if jti in self.token_blacklist:
raise SecurityError("Token has been revoked")
# Validate IP binding if enabled
if 'ip' in payload and client_ip:
token_ip = payload['ip']
if not self.validate_ip_binding(token_ip, client_ip):
raise SecurityError("IP address mismatch")
return True
def validate_ip_binding(self, token_ip: str, client_ip: str) -> bool:
"""Validate IP address binding"""
try:
token_addr = ipaddress.ip_address(token_ip)
client_addr = ipaddress.ip_address(client_ip)
# For IPv4, allow same /24 subnet
if token_addr.version == 4 and client_addr.version == 4:
token_network = ipaddress.IPv4Network(f"{token_ip}/24", strict=False)
return client_addr in token_network
# For IPv6, exact match required
return token_addr == client_addr
except ValueError:
return False
def validate_user_session(self, payload: Dict[str, Any]) -> bool:
"""Validate user session is still active"""
user_id = payload.get('sub')
if not user_id:
return False
# Check if user is still active in database
# This would typically involve checking a sessions table
# For this example, we'll just check if user exists and is active
return True
def create_secure_token(
self,
user_data: Dict[str, Any],
client_ip: Optional[str] = None,
bind_ip: bool = False
) -> str:
"""Create a secure JWT token with comprehensive validation"""
user_id = user_data.get("id") or user_data.get("sub")
current_time = datetime.utcnow()
# Get user permissions and role
permissions = await self.get_user_permissions(user_id) if user_id else []
role = await self.get_user_role(user_id) if user_id else "user"
# Generate unique token ID
jti = self.generate_jti(user_id, current_time)
payload = {
"sub": str(user_id),
"email": user_data.get("email"),
"full_name": user_data.get("full_name"),
"role": role,
"permissions": permissions,
"iss": self.issuer,
"aud": self.audience,
"exp": current_time + timedelta(minutes=self.access_token_expire_minutes),
"iat": current_time,
"jti": jti,
"type": "access"
}
# Add IP binding if requested
if bind_ip and client_ip:
payload["ip"] = client_ip
# Add custom security claims
payload["sec"] = {
"v": "1.0", # Security version
"alg_verified": True,
"claims_verified": True
}
try:
encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
except Exception as e:
raise SecurityError(f"Failed to create token: {str(e)}")
def verify_secure_token(
self,
token: str,
client_ip: Optional[str] = None,
verify_session: bool = True
) -> Optional[Dict[str, Any]]:
"""Verify JWT token with comprehensive security checks"""
if not token:
raise InvalidTokenError("Token is required")
# Step 1: Validate token structure
if not self.validate_token_structure(token):
raise InvalidTokenError("Invalid token structure")
# Step 2: Validate algorithm
self.validate_algorithm(token)
# Step 3: Verify signature and decode
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm],
options={
"verify_signature": True,
"verify_exp": False, # We'll do custom validation
"verify_nbf": False,
"verify_iat": False,
"verify_aud": False,
"verify_iss": False,
}
)
except jwt.ExpiredSignatureError:
raise TokenExpiredError("Token has expired")
except jwt.InvalidSignatureError:
raise InvalidSignatureError("Invalid token signature")
except jwt.DecodeError:
raise InvalidTokenError("Token decode error")
except Exception as e:
raise InvalidTokenError(f"Token verification failed: {str(e)}")
# Step 4: Validate claims
self.validate_claims(payload, client_ip)
# Step 5: Validate user session
if verify_session and not self.validate_user_session(payload):
raise SecurityError("User session is no longer valid")
return payload
def generate_jti(self, user_id: Any, timestamp: datetime) -> str:
"""Generate unique JWT ID"""
data = f"{user_id}:{timestamp.isoformat()}:{self.secret_key}"
return hashlib.sha256(data.encode()).hexdigest()[:16]
def revoke_token(self, token: str) -> bool:
"""Add token to blacklist"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm],
options={"verify_exp": False}
)
jti = payload.get('jti')
if jti:
self.token_blacklist.add(jti)
return True
except Exception:
pass
return False
def is_token_revoked(self, token: str) -> bool:
"""Check if token is revoked"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm],
options={"verify_exp": False}
)
jti = payload.get('jti')
return jti in self.token_blacklist if jti else False
except Exception:
return True
# ... (include other methods from previous JWTHandler implementation)PythonInput Validation
Create backend/app/auth/validators.py:
import re
from typing import Optional, Dict, Any
from pydantic import BaseModel, validator, EmailStr
from fastapi import HTTPException, status
class StrictUserCreate(BaseModel):
email: EmailStr
password: str
full_name: str
@validator('password')
def validate_password(cls, v):
"""Validate password strength"""
if len(v) < 8:
raise ValueError('Password must be at least 8 characters long')
if len(v) > 128:
raise ValueError('Password must be less than 128 characters')
# Check for required character types
if not re.search(r'[A-Z]', v):
raise ValueError('Password must contain at least one uppercase letter')
if not re.search(r'[a-z]', v):
raise ValueError('Password must contain at least one lowercase letter')
if not re.search(r'\d', v):
raise ValueError('Password must contain at least one digit')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', v):
raise ValueError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty', 'abc123',
'password123', 'admin', 'user', 'test'
]
if v.lower() in weak_passwords:
raise ValueError('Password is too weak')
return v
@validator('full_name')
def validate_full_name(cls, v):
"""Validate full name"""
if len(v.strip()) < 2:
raise ValueError('Full name must be at least 2 characters')
if len(v) > 100:
raise ValueError('Full name must be less than 100 characters')
# Allow only letters, spaces, hyphens, and apostrophes
if not re.match(r"^[a-zA-Z\s\-']+$", v):
raise ValueError('Full name contains invalid characters')
return v.strip()
@validator('email')
def validate_email_format(cls, v):
"""Additional email validation"""
# Check email length
if len(v) > 254:
raise ValueError('Email address is too long')
# Check for suspicious patterns
suspicious_patterns = [
r'\.{2,}', # Multiple consecutive dots
r'^\.|\.$', # Starting or ending with dot
r'@.*@', # Multiple @ symbols
]
for pattern in suspicious_patterns:
if re.search(pattern, v):
raise ValueError('Invalid email format')
return v.lower()
class SecureLoginRequest(BaseModel):
username: EmailStr
password: str
remember_me: Optional[bool] = False
client_info: Optional[Dict[str, Any]] = None
@validator('password')
def validate_password_not_empty(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Password cannot be empty')
if len(v) > 128:
raise ValueError('Password is too long')
return v
@validator('client_info')
def validate_client_info(cls, v):
"""Validate client information for security logging"""
if v is None:
return v
allowed_fields = ['user_agent', 'ip_address', 'browser', 'os', 'device_type']
# Remove any fields not in allowed list
filtered_info = {k: v[k] for k in v if k in allowed_fields}
# Validate field lengths
for field, value in filtered_info.items():
if isinstance(value, str) and len(value) > 500:
filtered_info[field] = value[:500]
return filtered_info
class TokenRefreshRequest(BaseModel):
refresh_token: str
@validator('refresh_token')
def validate_refresh_token(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Refresh token cannot be empty')
# Basic JWT structure validation
parts = v.split('.')
if len(parts) != 3:
raise ValueError('Invalid token format')
return v.strip()
def validate_request_rate_limit(client_ip: str, action: str) -> bool:
"""
Rate limiting validation
In production, use Redis or similar for distributed rate limiting
"""
# This is a simplified example
# You would implement proper rate limiting logic here
return True
def validate_request_origin(request_headers: Dict[str, str]) -> bool:
"""Validate request origin for CSRF protection"""
origin = request_headers.get('origin')
referer = request_headers.get('referer')
if not origin and not referer:
return False
# Check against allowed origins
allowed_origins = [
'http://localhost:3000',
'http://localhost:8080',
'https://yourdomain.com'
]
if origin and origin not in allowed_origins:
return False
return True
class SecurityValidator:
"""Comprehensive security validation class"""
@staticmethod
def validate_jwt_claims(payload: Dict[str, Any]) -> bool:
"""Validate JWT payload structure"""
required_claims = ['sub', 'exp', 'iat', 'type']
for claim in required_claims:
if claim not in payload:
raise ValueError(f"Missing required claim: {claim}")
# Validate claim types
if not isinstance(payload['sub'], str):
raise ValueError("Subject must be a string")
if not isinstance(payload['exp'], (int, float)):
raise ValueError("Expiration must be a number")
if not isinstance(payload['iat'], (int, float)):
raise ValueError("Issued at must be a number")
return True
@staticmethod
def validate_user_input(data: Dict[str, Any], max_length: int = 1000) -> Dict[str, Any]:
"""Sanitize and validate user input"""
cleaned_data = {}
for key, value in data.items():
if isinstance(value, str):
# Remove potentially dangerous characters
value = re.sub(r'[<>"\']', '', value)
# Limit length
if len(value) > max_length:
value = value[:max_length]
# Strip whitespace
value = value.strip()
cleaned_data[key] = value
return cleaned_data
@staticmethod
def validate_file_upload(file_data: bytes, allowed_types: list = None) -> bool:
"""Validate file uploads"""
if not file_data:
return False
# Check file size (10MB limit)
if len(file_data) > 10 * 1024 * 1024:
return False
# Check file signatures if types specified
if allowed_types:
# This is a simplified example
# In production, use python-magic or similar for proper MIME detection
return True
return TruePythonFrontend Error Handling
Create frontend/js/errorHandler.js:
/**
* Comprehensive Error Handling for JWT Authentication
*/
class ErrorHandler {
constructor() {
this.errorLog = [];
this.maxLogSize = 100;
this.setupGlobalErrorHandling();
}
/**
* Setup global error handling
*/
setupGlobalErrorHandling() {
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.logError('Unhandled Promise Rejection', event.reason);
event.preventDefault();
});
// Handle global JavaScript errors
window.addEventListener('error', (event) => {
this.logError('JavaScript Error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
});
}
/**
* Handle API errors with specific JWT error types
*/
handleApiError(error, context = '') {
let errorType = 'UNKNOWN_ERROR';
let message = 'An unexpected error occurred';
let shouldRetry = false;
let shouldLogout = false;
if (error.response) {
// Server responded with error status
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 401:
errorType = data?.error || 'AUTHENTICATION_ERROR';
shouldLogout = true;
if (errorType === 'EXPIRED_TOKEN') {
message = 'Your session has expired. Please log in again.';
shouldRetry = true;
} else if (errorType === 'INVALID_TOKEN') {
message = 'Invalid authentication token. Please log in again.';
} else {
message = 'Authentication failed. Please check your credentials.';
}
break;
case 403:
errorType = 'INSUFFICIENT_PERMISSIONS';
message = 'You do not have permission to access this resource.';
break;
case 422:
errorType = 'VALIDATION_ERROR';
message = this.formatValidationErrors(data?.detail || []);
break;
case 429:
errorType = 'RATE_LIMITED';
message = 'Too many requests. Please try again later.';
shouldRetry = true;
break;
case 500:
errorType = 'SERVER_ERROR';
message = 'Server error occurred. Please try again later.';
shouldRetry = true;
break;
default:
message = data?.message || `HTTP ${status} error occurred`;
}
} else if (error.request) {
// Network error
errorType = 'NETWORK_ERROR';
message = 'Network error. Please check your connection and try again.';
shouldRetry = true;
} else {
// Client-side error
errorType = 'CLIENT_ERROR';
message = error.message || 'An unexpected error occurred';
}
const errorInfo = {
type: errorType,
message: message,
context: context,
timestamp: new Date().toISOString(),
shouldRetry: shouldRetry,
shouldLogout: shouldLogout,
originalError: error
};
this.logError(errorType, errorInfo);
return errorInfo;
}
/**
* Format validation errors from FastAPI
*/
formatValidationErrors(details) {
if (!Array.isArray(details)) {
return 'Validation error occurred';
}
const errors = details.map(detail => {
const field = detail.loc ? detail.loc.join('.') : 'unknown';
return `${field}: ${detail.msg}`;
});
return errors.join(', ');
}
/**
* Handle JWT specific errors
*/
handleJWTError(error) {
const jwtErrors = {
'TOKEN_EXPIRED': {
message: 'Your session has expired',
action: 'refresh_token',
severity: 'warning'
},
'INVALID_TOKEN': {
message: 'Invalid authentication token',
action: 'logout',
severity: 'error'
},
'MISSING_TOKEN': {
message: 'Authentication required',
action: 'login',
severity: 'info'
},
'INSUFFICIENT_PERMISSIONS': {
message: 'Access denied - insufficient permissions',
action: 'show_error',
severity: 'error'
},
'INVALID_SIGNATURE': {
message: 'Token signature verification failed',
action: 'logout',
severity: 'error'
},
'ALGORITHM_CONFUSION': {
message: 'Security error - please contact support',
action: 'logout',
severity: 'critical'
}
};
const errorInfo = jwtErrors[error.type] || {
message: 'Authentication error occurred',
action: 'logout',
severity: 'error'
};
this.showUserError(errorInfo.message, errorInfo.severity);
switch (errorInfo.action) {
case 'refresh_token':
return this.attemptTokenRefresh();
case 'logout':
return auth.logout();
case 'login':
return app.showLogin();
case 'show_error':
return Promise.resolve();
}
}
/**
* Attempt automatic token refresh
*/
async attemptTokenRefresh() {
try {
await auth.api.refreshToken();
this.showUserError('Session refreshed successfully', 'success');
return true;
} catch (error) {
this.showUserError('Failed to refresh session. Please log in again.', 'error');
await auth.logout();
return false;
}
}
/**
* Show error to user
*/
showUserError(message, severity = 'error') {
const statusElement = document.getElementById('status');
if (statusElement) {
statusElement.textContent = message;
statusElement.className = `status-${severity}`;
// Auto-clear after appropriate time based on severity
const clearTime = severity === 'critical' ? 10000 : 5000;
setTimeout(() => {
statusElement.textContent = '';
statusElement.className = '';
}, clearTime);
}
// Also log to console for debugging
console[severity === 'critical' ? 'error' : severity](message);
}
/**
* Log error for debugging and monitoring
*/
logError(type, details) {
const logEntry = {
type: type,
details: details,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
this.errorLog.push(logEntry);
// Keep log size manageable
if (this.errorLog.length > this.maxLogSize) {
this.errorLog.shift();
}
// In production, send critical errors to monitoring service
if (type === 'CRITICAL_ERROR') {
this.sendToMonitoring(logEntry);
}
console.error('Error logged:', logEntry);
}
/**
* Send error to monitoring service
*/
async sendToMonitoring(logEntry) {
try {
// Replace with your monitoring service endpoint
await fetch('/api/monitoring/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logEntry)
});
} catch (error) {
console.error('Failed to send error to monitoring service:', error);
}
}
/**
* Validate input on client side
*/
validateInput(value, type = 'text', options = {}) {
const errors = [];
if (options.required && (!value || value.toString().trim().length === 0)) {
errors.push('This field is required');
return errors;
}
if (!value) return errors;
switch (type) {
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
errors.push('Invalid email format');
}
break;
case 'password':
if (value.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (!/[A-Z]/.test(value)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(value)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/\d/.test(value)) {
errors.push('Password must contain at least one number');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
errors.push('Password must contain at least one special character');
}
break;
case 'name':
if (value.length < 2) {
errors.push('Name must be at least 2 characters long');
}
if (!/^[a-zA-Z\s\-']+$/.test(value)) {
errors.push('Name contains invalid characters');
}
break;
case 'text':
if (options.maxLength && value.length > options.maxLength) {
errors.push(`Text must be less than ${options.maxLength} characters`);
}
break;
}
return errors;
}
/**
* Get error log for debugging
*/
getErrorLog() {
return [...this.errorLog];
}
/**
* Clear error log
*/
clearErrorLog() {
this.errorLog = [];
}
/**
* Export error log for support
*/
exportErrorLog() {
const logData = {
errors: this.errorLog,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
const blob = new Blob([JSON.stringify(logData, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `error-log-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
}
// Create global error handler instance
const errorHandler = new ErrorHandler();
// Enhance API client with error handling
if (typeof api !== 'undefined') {
// Wrap API methods to include error handling
const originalRequest = api.request.bind(api);
api.request = async function(endpoint, options = {}) {
try {
return await originalRequest(endpoint, options);
} catch (error) {
const errorInfo = errorHandler.handleApiError(error, endpoint);
// Handle JWT specific errors
if (errorInfo.type.includes('TOKEN') || errorInfo.type === 'INSUFFICIENT_PERMISSIONS') {
await errorHandler.handleJWTError(errorInfo);
}
throw errorInfo;
}
};
}JavaScriptThis comprehensive error handling and validation implementation provides:
- Backend Validation: Strict input validation, JWT claim validation, and security checks
- Structured Error Handling: Consistent error types and responses
- Frontend Error Management: User-friendly error messages and automatic recovery
- Security Validation: Protection against common attacks and vulnerabilities
- Monitoring Integration: Error logging and reporting capabilities
- Client-side Validation: Real-time input validation for better UX
The system ensures robust security while providing clear feedback to users and developers for debugging and monitoring purposes.
11. JWE (JSON Web Encryption)
JSON Web Encryption (JWE) provides confidentiality by encrypting the JWT payload, ensuring that sensitive information cannot be read even if the token is intercepted.
JWE vs JWS Comparison
graph TB
A[JWT Security Options] --> B[JWS - Signed Only]
A --> C[JWE - Encrypted]
A --> D[JWS + JWE - Signed & Encrypted]
B --> E[Integrity ✓Authenticity ✓Confidentiality ✗]
C --> F[Integrity ✓Authenticity ✗Confidentiality ✓]
D --> G[Integrity ✓Authenticity ✓Confidentiality ✓]JWE Structure
JWE tokens have 5 parts separated by dots:
header.encrypted_key.initialization_vector.ciphertext.authentication_tagJSONJWE Implementation with FastAPI
Create backend/app/auth/jwe_handler.py:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os
import json
import base64
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
class JWEHandler:
def __init__(self):
self.content_encryption_algorithm = "A256GCM"
self.key_encryption_algorithm = "RSA-OAEP"
# Generate or load RSA key pair for key encryption
self.private_key, self.public_key = self.generate_rsa_keypair()
def generate_rsa_keypair(self):
"""Generate RSA key pair for JWE"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
return private_key, public_key
def create_jwe_token(self, payload: Dict[str, Any], recipient_public_key: Optional[rsa.RSAPublicKey] = None) -> str:
"""Create JWE token with encrypted payload"""
# Use provided public key or default
pub_key = recipient_public_key or self.public_key
# Step 1: Create JWE Header
header = {
"alg": self.key_encryption_algorithm,
"enc": self.content_encryption_algorithm,
"typ": "JWT"
}
# Step 2: Generate Content Encryption Key (CEK)
cek = AESGCM.generate_key(bit_length=256)
# Step 3: Encrypt CEK with recipient's public key
encrypted_key = pub_key.encrypt(
cek,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Step 4: Generate Initialization Vector
iv = os.urandom(12) # 96 bits for GCM
# Step 5: Create Additional Authenticated Data (AAD)
header_b64 = base64.urlsafe_b64encode(
json.dumps(header, separators=(',', ':')).encode()
).decode().rstrip('=')
aad = header_b64.encode()
# Step 6: Encrypt payload
aesgcm = AESGCM(cek)
payload_json = json.dumps(payload, separators=(',', ':')).encode()
ciphertext_and_tag = aesgcm.encrypt(iv, payload_json, aad)
ciphertext = ciphertext_and_tag[:-16] # All but last 16 bytes
auth_tag = ciphertext_and_tag[-16:] # Last 16 bytes
# Step 7: Base64URL encode all components
header_b64 = base64.urlsafe_b64encode(
json.dumps(header, separators=(',', ':')).encode()
).decode().rstrip('=')
encrypted_key_b64 = base64.urlsafe_b64encode(encrypted_key).decode().rstrip('=')
iv_b64 = base64.urlsafe_b64encode(iv).decode().rstrip('=')
ciphertext_b64 = base64.urlsafe_b64encode(ciphertext).decode().rstrip('=')
auth_tag_b64 = base64.urlsafe_b64encode(auth_tag).decode().rstrip('=')
# Step 8: Concatenate to form JWE
jwe_token = f"{header_b64}.{encrypted_key_b64}.{iv_b64}.{ciphertext_b64}.{auth_tag_b64}"
return jwe_token
def decrypt_jwe_token(self, jwe_token: str, recipient_private_key: Optional[rsa.RSAPrivateKey] = None) -> Optional[Dict[str, Any]]:
"""Decrypt JWE token and return payload"""
try:
# Use provided private key or default
priv_key = recipient_private_key or self.private_key
# Split JWE into components
parts = jwe_token.split('.')
if len(parts) != 5:
raise ValueError("Invalid JWE format")
header_b64, encrypted_key_b64, iv_b64, ciphertext_b64, auth_tag_b64 = parts
# Decode components
header = json.loads(self._base64url_decode(header_b64))
encrypted_key = self._base64url_decode(encrypted_key_b64)
iv = self._base64url_decode(iv_b64)
ciphertext = self._base64url_decode(ciphertext_b64)
auth_tag = self._base64url_decode(auth_tag_b64)
# Verify algorithms
if header.get('alg') != self.key_encryption_algorithm:
raise ValueError("Unsupported key encryption algorithm")
if header.get('enc') != self.content_encryption_algorithm:
raise ValueError("Unsupported content encryption algorithm")
# Decrypt CEK
cek = priv_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Prepare AAD
aad = header_b64.encode()
# Decrypt payload
aesgcm = AESGCM(cek)
ciphertext_with_tag = ciphertext + auth_tag
payload_json = aesgcm.decrypt(iv, ciphertext_with_tag, aad)
payload = json.loads(payload_json.decode())
return payload
except Exception as e:
print(f"JWE decryption failed: {e}")
return None
def _base64url_decode(self, data: str) -> bytes:
"""Decode base64url encoded data"""
# Add padding if needed
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
return base64.urlsafe_b64decode(data)
def create_nested_jwt(self, payload: Dict[str, Any], sign_key: str, encrypt_key: Optional[rsa.RSAPublicKey] = None) -> str:
"""Create nested JWT (JWS inside JWE)"""
# First, create a signed JWT
from app.auth.jwt_handler import JWTHandler
jwt_handler = JWTHandler()
# Create signed token
signed_token = jwt_handler.create_access_token(payload)
# Then encrypt the signed token
nested_payload = {"jwt": signed_token}
encrypted_token = self.create_jwe_token(nested_payload, encrypt_key)
return encrypted_token
def verify_nested_jwt(self, nested_token: str, decrypt_key: Optional[rsa.RSAPrivateKey] = None) -> Optional[Dict[str, Any]]:
"""Verify nested JWT (decrypt then verify signature)"""
# First decrypt
decrypted_payload = self.decrypt_jwe_token(nested_token, decrypt_key)
if not decrypted_payload or 'jwt' not in decrypted_payload:
return None
# Then verify signature
from app.auth.jwt_handler import JWTHandler
jwt_handler = JWTHandler()
signed_token = decrypted_payload['jwt']
return jwt_handler.verify_token(signed_token)
# Example usage in FastAPI
class SecureJWTHandler:
def __init__(self):
self.jwe_handler = JWEHandler()
from app.auth.jwt_handler import JWTHandler
self.jwt_handler = JWTHandler()
async def create_secure_token(self, user_data: Dict[str, Any], encryption_required: bool = False) -> str:
"""Create secure token with optional encryption"""
if encryption_required:
# Create encrypted token for sensitive data
sensitive_payload = {
"sub": str(user_data.get("id")),
"email": user_data.get("email"),
"full_name": user_data.get("full_name"),
"sensitive_data": {
"ssn": user_data.get("ssn"), # Example sensitive data
"bank_account": user_data.get("bank_account"),
"medical_info": user_data.get("medical_info")
},
"exp": (datetime.utcnow() + timedelta(minutes=30)).timestamp(),
"iat": datetime.utcnow().timestamp(),
"type": "secure_access"
}
return self.jwe_handler.create_jwe_token(sensitive_payload)
else:
# Create regular signed token
return await self.jwt_handler.create_access_token(user_data)
def verify_secure_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Verify secure token (try JWE first, then JWS)"""
# Try to decrypt as JWE first
payload = self.jwe_handler.decrypt_jwe_token(token)
if payload:
# Validate expiration for JWE tokens
exp = payload.get('exp')
if exp and datetime.utcnow().timestamp() > exp:
return None
return payload
# Fall back to JWS verification
return self.jwt_handler.verify_token(token)PythonJWE Route Implementation
Create backend/app/routers/secure.py:
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.auth.jwe_handler import SecureJWTHandler
from app.auth.models import UserResponse
from typing import Dict, Any
router = APIRouter()
security = HTTPBearer()
secure_jwt = SecureJWTHandler()
@router.post("/secure-login")
async def secure_login(request: Request, credentials: dict):
"""Login with JWE token for sensitive applications"""
# Validate credentials (implementation similar to regular login)
# ... credential validation logic ...
# Create encrypted token for sensitive data
user_data = {
"id": 123,
"email": credentials.get("email"),
"full_name": "John Doe",
"ssn": "123-45-6789", # This will be encrypted
"bank_account": "12345678",
"medical_info": {"condition": "example"}
}
encrypted_token = await secure_jwt.create_secure_token(user_data, encryption_required=True)
return {
"access_token": encrypted_token,
"token_type": "JWE",
"message": "Secure token created with encrypted payload"
}
@router.get("/secure-profile")
async def get_secure_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Get user profile from encrypted token"""
token = credentials.credentials
payload = secure_jwt.verify_secure_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired secure token"
)
# Remove sensitive data for response
response_data = {
"user_id": payload.get("sub"),
"email": payload.get("email"),
"full_name": payload.get("full_name"),
"has_sensitive_data": "sensitive_data" in payload
}
return response_data
@router.get("/sensitive-data")
async def get_sensitive_data(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Access sensitive data from encrypted token"""
token = credentials.credentials
payload = secure_jwt.verify_secure_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired secure token"
)
sensitive_data = payload.get("sensitive_data")
if not sensitive_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No sensitive data available"
)
return {
"message": "Sensitive data accessed",
"data": sensitive_data,
"notice": "This data was encrypted in the JWT token"
}JSON12. OAuth 2.0 Integration
OAuth 2.0 provides a framework for authorization that works seamlessly with JWT tokens. This chapter covers implementing OAuth 2.0 flows with JWT.
OAuth 2.0 + JWT Architecture
sequenceDiagram
participant C as Client App
participant AS as Authorization Server
participant RS as Resource Server
participant U as User
Note over C,U: Authorization Code Flow with JWT
C->>U: Redirect to Authorization Server
U->>AS: Login & Consent
AS->>C: Authorization Code
C->>AS: Exchange Code for Tokens
AS->>C: JWT Access Token + Refresh Token
Note over C,RS: API Access with JWT
C->>RS: API Request + JWT Token
RS->>RS: Verify JWT Signature
RS->>RS: Validate Claims
RS->>C: Protected ResourceOAuth 2.0 Server Implementation
Create backend/app/oauth/oauth_server.py:
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.security import HTTPBearer
import secrets
import hashlib
import base64
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from urllib.parse import urlencode, parse_qs
import json
from app.auth.jwt_handler import JWTHandler
from app.database.database import database
class OAuth2Server:
def __init__(self):
self.jwt_handler = JWTHandler()
# OAuth 2.0 configuration
self.supported_grant_types = [
"authorization_code",
"refresh_token",
"client_credentials"
]
self.supported_response_types = ["code"]
self.supported_scopes = [
"read", "write", "admin",
"profile", "email", "openid"
]
# In production, store these in database
self.clients = {
"web_app_client": {
"client_secret": "web_app_secret_key",
"redirect_uris": [
"http://localhost:3000/callback",
"http://localhost:8080/callback"
],
"scopes": ["read", "write", "profile", "email"],
"grant_types": ["authorization_code", "refresh_token"]
},
"mobile_app": {
"client_secret": None, # Public client
"redirect_uris": ["com.example.app://callback"],
"scopes": ["read", "profile"],
"grant_types": ["authorization_code"]
}
}
# Temporary storage (use Redis in production)
self.authorization_codes = {}
self.access_tokens = {}
def generate_authorization_code(self, client_id: str, user_id: str, scopes: List[str], redirect_uri: str) -> str:
"""Generate authorization code"""
code = secrets.token_urlsafe(32)
self.authorization_codes[code] = {
"client_id": client_id,
"user_id": user_id,
"scopes": scopes,
"redirect_uri": redirect_uri,
"expires_at": datetime.utcnow() + timedelta(minutes=10),
"used": False
}
return code
def validate_client(self, client_id: str, client_secret: Optional[str] = None) -> bool:
"""Validate OAuth client"""
if client_id not in self.clients:
return False
client = self.clients[client_id]
# Public clients don't have secrets
if client["client_secret"] is None:
return client_secret is None
return client["client_secret"] == client_secret
def validate_redirect_uri(self, client_id: str, redirect_uri: str) -> bool:
"""Validate redirect URI"""
if client_id not in self.clients:
return False
return redirect_uri in self.clients[client_id]["redirect_uris"]
def validate_scope(self, client_id: str, requested_scopes: List[str]) -> bool:
"""Validate requested scopes"""
if client_id not in self.clients:
return False
client_scopes = self.clients[client_id]["scopes"]
return all(scope in client_scopes for scope in requested_scopes)
async def create_oauth_tokens(self, user_id: str, client_id: str, scopes: List[str]) -> Dict[str, Any]:
"""Create OAuth tokens with JWT"""
# Get user data
user_query = """
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = :user_id AND u.is_active = true
"""
user = await database.fetch_one(user_query, {"user_id": user_id})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Create JWT access token with OAuth scopes
token_data = {
"sub": str(user["id"]),
"email": user["email"],
"full_name": user["full_name"],
"role": user["role_name"] or "user",
"client_id": client_id,
"scope": " ".join(scopes),
"aud": client_id,
"iss": "oauth2-server"
}
access_token = await self.jwt_handler.create_access_token(token_data)
# Create refresh token
refresh_token_data = {
"sub": str(user["id"]),
"client_id": client_id,
"scope": " ".join(scopes),
"token_type": "refresh"
}
refresh_token = self.jwt_handler.create_refresh_token(refresh_token_data)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": self.jwt_handler.access_token_expire_minutes * 60,
"scope": " ".join(scopes)
}
# OAuth 2.0 Router
oauth_router = APIRouter(prefix="/oauth")
oauth_server = OAuth2Server()
@oauth_router.get("/authorize")
async def authorize(
request: Request,
response_type: str,
client_id: str,
redirect_uri: str,
scope: str = "",
state: Optional[str] = None
):
"""OAuth 2.0 Authorization endpoint"""
# Validate parameters
if response_type != "code":
error_params = {
"error": "unsupported_response_type",
"error_description": "Only 'code' response type is supported"
}
if state:
error_params["state"] = state
error_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=error_url)
# Validate client
if not oauth_server.validate_client(client_id):
raise HTTPException(status_code=400, detail="Invalid client_id")
# Validate redirect URI
if not oauth_server.validate_redirect_uri(client_id, redirect_uri):
raise HTTPException(status_code=400, detail="Invalid redirect_uri")
# Parse and validate scopes
requested_scopes = scope.split() if scope else ["read"]
if not oauth_server.validate_scope(client_id, requested_scopes):
error_params = {
"error": "invalid_scope",
"error_description": "Invalid or unauthorized scopes requested"
}
if state:
error_params["state"] = state
error_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=error_url)
# In a real implementation, check if user is authenticated
# For this example, we'll assume user_id = "123"
user_id = "123" # This would come from session/authentication
# Generate authorization code
auth_code = oauth_server.generate_authorization_code(
client_id, user_id, requested_scopes, redirect_uri
)
# Redirect back to client with authorization code
success_params = {"code": auth_code}
if state:
success_params["state"] = state
callback_url = f"{redirect_uri}?{urlencode(success_params)}"
return RedirectResponse(url=callback_url)
@oauth_router.post("/token")
async def token(
grant_type: str = Form(...),
client_id: str = Form(...),
client_secret: Optional[str] = Form(None),
code: Optional[str] = Form(None),
redirect_uri: Optional[str] = Form(None),
refresh_token: Optional[str] = Form(None),
scope: Optional[str] = Form(None)
):
"""OAuth 2.0 Token endpoint"""
# Validate client credentials
if not oauth_server.validate_client(client_id, client_secret):
raise HTTPException(
status_code=401,
detail="Invalid client credentials",
headers={"WWW-Authenticate": "Basic"}
)
if grant_type == "authorization_code":
# Authorization Code Grant
if not code or not redirect_uri:
raise HTTPException(
status_code=400,
detail="Missing required parameters for authorization_code grant"
)
# Validate authorization code
if code not in oauth_server.authorization_codes:
raise HTTPException(status_code=400, detail="Invalid authorization code")
auth_data = oauth_server.authorization_codes[code]
# Check if code is expired or used
if auth_data["used"] or datetime.utcnow() > auth_data["expires_at"]:
raise HTTPException(status_code=400, detail="Authorization code expired or used")
# Validate client and redirect URI
if auth_data["client_id"] != client_id or auth_data["redirect_uri"] != redirect_uri:
raise HTTPException(status_code=400, detail="Client or redirect URI mismatch")
# Mark code as used
oauth_server.authorization_codes[code]["used"] = True
# Create tokens
tokens = await oauth_server.create_oauth_tokens(
auth_data["user_id"],
client_id,
auth_data["scopes"]
)
return tokens
elif grant_type == "refresh_token":
# Refresh Token Grant
if not refresh_token:
raise HTTPException(
status_code=400,
detail="Missing refresh_token parameter"
)
# Verify refresh token
payload = oauth_server.jwt_handler.verify_token(refresh_token)
if not payload or payload.get("token_type") != "refresh":
raise HTTPException(status_code=400, detail="Invalid refresh token")
# Validate client
if payload.get("client_id") != client_id:
raise HTTPException(status_code=400, detail="Client mismatch")
# Create new access token
scopes = payload.get("scope", "").split()
tokens = await oauth_server.create_oauth_tokens(
payload["sub"],
client_id,
scopes
)
# Don't return new refresh token unless scope changed
if not scope or scope == payload.get("scope"):
del tokens["refresh_token"]
return tokens
else:
raise HTTPException(
status_code=400,
detail="Unsupported grant type"
)
@oauth_router.get("/.well-known/oauth-authorization-server")
async def oauth_metadata():
"""OAuth 2.0 Authorization Server Metadata"""
return {
"issuer": "https://your-auth-server.com",
"authorization_endpoint": "/oauth/authorize",
"token_endpoint": "/oauth/token",
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials"
],
"scopes_supported": [
"read", "write", "admin",
"profile", "email", "openid"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
]
}PythonOpenID Connect Extension
Create backend/app/oauth/openid_connect.py:
from typing import Dict, Any, Optional
import hashlib
import base64
from app.oauth.oauth_server import OAuth2Server
class OpenIDConnectServer(OAuth2Server):
def __init__(self):
super().__init__()
# Add OpenID Connect scopes
self.supported_scopes.extend(["openid", "profile", "email", "address", "phone"])
async def create_id_token(self, user_id: str, client_id: str, scopes: List[str], access_token: str) -> str:
"""Create OpenID Connect ID Token"""
# Get user data
user_query = """
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = :user_id AND u.is_active = true
"""
user = await database.fetch_one(user_query, {"user_id": user_id})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Create ID token claims
id_token_claims = {
"iss": "https://your-auth-server.com",
"sub": str(user["id"]),
"aud": client_id,
"exp": datetime.utcnow() + timedelta(hours=1),
"iat": datetime.utcnow(),
"auth_time": datetime.utcnow(),
"nonce": self.generate_nonce() # Should come from auth request
}
# Add claims based on requested scopes
if "profile" in scopes:
id_token_claims.update({
"name": user["full_name"],
"given_name": user["full_name"].split()[0] if user["full_name"] else "",
"family_name": " ".join(user["full_name"].split()[1:]) if len(user["full_name"].split()) > 1 else "",
"preferred_username": user["email"],
"updated_at": user["updated_at"].timestamp() if user["updated_at"] else None
})
if "email" in scopes:
id_token_claims.update({
"email": user["email"],
"email_verified": True # Assume emails are verified
})
# Create access token hash for at_hash claim
if access_token:
at_hash = self.create_at_hash(access_token)
id_token_claims["at_hash"] = at_hash
# Create and return ID token
return self.jwt_handler.create_access_token(id_token_claims)
def create_at_hash(self, access_token: str) -> str:
"""Create access token hash for ID token"""
# Hash the access token with SHA256
hash_bytes = hashlib.sha256(access_token.encode()).digest()
# Take the left-most half of the hash
half_hash = hash_bytes[:len(hash_bytes)//2]
# Base64url encode
return base64.urlsafe_b64encode(half_hash).decode().rstrip('=')
def generate_nonce(self) -> str:
"""Generate nonce for ID token"""
return secrets.token_urlsafe(16)
async def create_oauth_tokens_with_id_token(self, user_id: str, client_id: str, scopes: List[str]) -> Dict[str, Any]:
"""Create OAuth tokens including ID token for OpenID Connect"""
# Create standard OAuth tokens
tokens = await self.create_oauth_tokens(user_id, client_id, scopes)
# Add ID token if OpenID Connect scope is requested
if "openid" in scopes:
id_token = await self.create_id_token(
user_id,
client_id,
scopes,
tokens["access_token"]
)
tokens["id_token"] = id_token
return tokens
# Update token endpoint to support OpenID Connect
oidc_server = OpenIDConnectServer()
@oauth_router.post("/token")
async def enhanced_token(
grant_type: str = Form(...),
client_id: str = Form(...),
client_secret: Optional[str] = Form(None),
code: Optional[str] = Form(None),
redirect_uri: Optional[str] = Form(None),
refresh_token: Optional[str] = Form(None),
scope: Optional[str] = Form(None)
):
"""Enhanced OAuth 2.0 Token endpoint with OpenID Connect support"""
# ... (same validation logic as before) ...
if grant_type == "authorization_code":
# Create tokens with ID token if OpenID Connect
tokens = await oidc_server.create_oauth_tokens_with_id_token(
auth_data["user_id"],
client_id,
auth_data["scopes"]
)
return tokens
# ... (rest of the implementation) ...
@oauth_router.get("/.well-known/openid_configuration")
async def openid_configuration():
"""OpenID Connect Discovery Document"""
return {
"issuer": "https://your-auth-server.com",
"authorization_endpoint": "/oauth/authorize",
"token_endpoint": "/oauth/token",
"userinfo_endpoint": "/oauth/userinfo",
"jwks_uri": "/oauth/jwks",
"response_types_supported": ["code", "id_token", "code id_token"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"implicit"
],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256", "HS256"],
"scopes_supported": [
"openid", "profile", "email", "address", "phone",
"read", "write", "admin"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_supported": [
"sub", "name", "given_name", "family_name", "preferred_username",
"email", "email_verified", "picture", "updated_at"
]
}
@oauth_router.get("/userinfo")
async def userinfo(credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
"""OpenID Connect UserInfo endpoint"""
token = credentials.credentials
payload = oidc_server.jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=401,
detail="Invalid access token"
)
# Check if token has appropriate scope
token_scopes = payload.get("scope", "").split()
if "openid" not in token_scopes:
raise HTTPException(
status_code=403,
detail="Token does not have openid scope"
)
# Get user information
user_id = payload.get("sub")
user_query = """
SELECT * FROM users WHERE id = :user_id AND is_active = true
"""
user = await database.fetch_one(user_query, {"user_id": user_id})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Build response based on scopes
userinfo = {"sub": str(user["id"])}
if "profile" in token_scopes:
userinfo.update({
"name": user["full_name"],
"preferred_username": user["email"],
"updated_at": user["updated_at"].timestamp() if user["updated_at"] else None
})
if "email" in token_scopes:
userinfo.update({
"email": user["email"],
"email_verified": True
})
return userinfoPython13. Key Management and Rotation
Proper key management is crucial for JWT security. This chapter covers key rotation strategies, JWKS endpoints, and automated key management.
Key Rotation Strategy
graph TB
A[Key Rotation Process] --> B[Generate New Key]
B --> C[Add to JWKS]
C --> D[Start Using for Signing]
D --> E[Keep Old Key for Verification]
E --> F[Remove Old Key After Grace Period]
G[JWKS Endpoint] --> H[Current Signing Key]
G --> I[Previous Key for Verification]
G --> J[Next Key for Preparation]Key Management Implementation
Create backend/app/auth/key_manager.py:
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
import json
import base64
import hashlib
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import os
from dataclasses import dataclass, asdict
@dataclass
class JWTKey:
"""JWT Key information"""
kid: str # Key ID
use: str # Key use (sig for signing)
kty: str # Key type (RSA, EC)
alg: str # Algorithm
n: str # RSA modulus (for RSA keys)
e: str # RSA exponent (for RSA keys)
created_at: datetime
expires_at: Optional[datetime] = None
private_key: Optional[str] = None # PEM encoded private key
def to_jwk(self) -> Dict:
"""Convert to JWK format"""
jwk = {
"kid": self.kid,
"use": self.use,
"kty": self.kty,
"alg": self.alg,
"n": self.n,
"e": self.e
}
return jwk
class KeyManager:
def __init__(self, key_directory: str = "keys"):
self.key_directory = key_directory
self.keys: Dict[str, JWTKey] = {}
self.current_signing_key: Optional[str] = None
# Key rotation settings
self.key_rotation_interval = timedelta(days=30)
self.key_grace_period = timedelta(days=7)
# Ensure key directory exists
os.makedirs(key_directory, exist_ok=True)
# Load existing keys
self.load_keys()
# Ensure we have at least one key
if not self.keys:
self.generate_initial_key()
def generate_key_id(self) -> str:
"""Generate a unique key ID"""
timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
random_part = hashlib.sha256(os.urandom(32)).hexdigest()[:8]
return f"{timestamp}_{random_part}"
def generate_rsa_key_pair(self) -> Tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey]:
"""Generate RSA key pair"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
return private_key, public_key
def rsa_key_to_jwk_components(self, public_key: rsa.RSAPublicKey) -> Tuple[str, str]:
"""Extract JWK components from RSA public key"""
public_numbers = public_key.public_key().public_numbers()
# Convert to base64url-encoded format
n = self.int_to_base64url(public_numbers.n)
e = self.int_to_base64url(public_numbers.e)
return n, e
def int_to_base64url(self, value: int) -> str:
"""Convert integer to base64url string"""
# Calculate byte length needed
byte_length = (value.bit_length() + 7) // 8
# Convert to bytes
value_bytes = value.to_bytes(byte_length, byteorder='big')
# Base64url encode
return base64.urlsafe_b64encode(value_bytes).decode().rstrip('=')
def create_jwt_key(self, algorithm: str = "RS256") -> JWTKey:
"""Create a new JWT key"""
kid = self.generate_key_id()
private_key, public_key = self.generate_rsa_key_pair()
# Get JWK components
n, e = self.rsa_key_to_jwk_components(public_key)
# Serialize private key
private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
).decode()
jwt_key = JWTKey(
kid=kid,
use="sig",
kty="RSA",
alg=algorithm,
n=n,
e=e,
created_at=datetime.utcnow(),
private_key=private_key_pem
)
return jwt_key
def save_key(self, jwt_key: JWTKey):
"""Save key to disk"""
key_file = os.path.join(self.key_directory, f"{jwt_key.kid}.json")
key_data = asdict(jwt_key)
key_data['created_at'] = jwt_key.created_at.isoformat()
if jwt_key.expires_at:
key_data['expires_at'] = jwt_key.expires_at.isoformat()
with open(key_file, 'w') as f:
json.dump(key_data, f, indent=2)
def load_keys(self):
"""Load keys from disk"""
if not os.path.exists(self.key_directory):
return
for filename in os.listdir(self.key_directory):
if filename.endswith('.json'):
try:
key_file = os.path.join(self.key_directory, filename)
with open(key_file, 'r') as f:
key_data = json.load(f)
# Parse datetime fields
key_data['created_at'] = datetime.fromisoformat(key_data['created_at'])
if key_data.get('expires_at'):
key_data['expires_at'] = datetime.fromisoformat(key_data['expires_at'])
jwt_key = JWTKey(**key_data)
self.keys[jwt_key.kid] = jwt_key
# Set as current signing key if none set
if not self.current_signing_key:
self.current_signing_key = jwt_key.kid
except Exception as e:
print(f"Failed to load key {filename}: {e}")
def generate_initial_key(self):
"""Generate initial key if none exist"""
jwt_key = self.create_jwt_key()
self.keys[jwt_key.kid] = jwt_key
self.current_signing_key = jwt_key.kid
self.save_key(jwt_key)
def rotate_keys(self):
"""Rotate signing keys"""
if not self.current_signing_key:
self.generate_initial_key()
return
current_key = self.keys[self.current_signing_key]
# Check if rotation is needed
if datetime.utcnow() - current_key.created_at < self.key_rotation_interval:
return
# Generate new key
new_key = self.create_jwt_key()
self.keys[new_key.kid] = new_key
self.save_key(new_key)
# Set expiration for old key
current_key.expires_at = datetime.utcnow() + self.key_grace_period
self.save_key(current_key)
# Update current signing key
self.current_signing_key = new_key.kid
print(f"Key rotation completed. New signing key: {new_key.kid}")
def cleanup_expired_keys(self):
"""Remove expired keys"""
expired_keys = []
for kid, jwt_key in self.keys.items():
if jwt_key.expires_at and datetime.utcnow() > jwt_key.expires_at:
expired_keys.append(kid)
for kid in expired_keys:
# Remove from memory
del self.keys[kid]
# Remove from disk
key_file = os.path.join(self.key_directory, f"{kid}.json")
if os.path.exists(key_file):
os.remove(key_file)
print(f"Expired key removed: {kid}")
def get_signing_key(self) -> Optional[JWTKey]:
"""Get current signing key"""
if self.current_signing_key and self.current_signing_key in self.keys:
return self.keys[self.current_signing_key]
return None
def get_verification_key(self, kid: str) -> Optional[JWTKey]:
"""Get key for verification by kid"""
return self.keys.get(kid)
def get_jwks(self) -> Dict:
"""Get JWKS (JSON Web Key Set) for public distribution"""
keys = []
for jwt_key in self.keys.values():
# Only include non-expired keys
if not jwt_key.expires_at or datetime.utcnow() <= jwt_key.expires_at:
keys.append(jwt_key.to_jwk())
return {"keys": keys}
def get_private_key(self, kid: str) -> Optional[rsa.RSAPrivateKey]:
"""Get private key for signing"""
jwt_key = self.keys.get(kid)
if not jwt_key or not jwt_key.private_key:
return None
try:
private_key = load_pem_private_key(
jwt_key.private_key.encode(),
password=None
)
return private_key
except Exception as e:
print(f"Failed to load private key {kid}: {e}")
return None
def perform_maintenance(self):
"""Perform regular maintenance tasks"""
self.rotate_keys()
self.cleanup_expired_keys()
# Global key manager instance
key_manager = KeyManager()PythonEnhanced JWT Handler with Key Rotation
Update the JWT Handler to use the key manager:
# In jwt_handler.py
import jwt
from app.auth.key_manager import key_manager
class RotatingJWTHandler:
def __init__(self):
self.key_manager = key_manager
# ... other initialization ...
def create_access_token(self, data: Dict[str, Any]) -> str:
"""Create access token with current signing key"""
# Get current signing key
signing_key_info = self.key_manager.get_signing_key()
if not signing_key_info:
raise Exception("No signing key available")
private_key = self.key_manager.get_private_key(signing_key_info.kid)
if not private_key:
raise Exception("Failed to load signing key")
# Prepare payload
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access"
})
# Create token with key ID in header
encoded_jwt = jwt.encode(
to_encode,
private_key,
algorithm=signing_key_info.alg,
headers={"kid": signing_key_info.kid}
)
return encoded_jwt
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Verify token using appropriate key"""
try:
# Get key ID from header
header = jwt.get_unverified_header(token)
kid = header.get("kid")
if not kid:
raise jwt.InvalidTokenError("Missing key ID in token header")
# Get verification key
key_info = self.key_manager.get_verification_key(kid)
if not key_info:
raise jwt.InvalidTokenError(f"Unknown key ID: {kid}")
# Get private key (we'll extract public key from it)
private_key = self.key_manager.get_private_key(kid)
if not private_key:
raise jwt.InvalidTokenError(f"Key not available: {kid}")
public_key = private_key.public_key()
# Verify token
payload = jwt.decode(
token,
public_key,
algorithms=[key_info.alg]
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
except Exception as e:
print(f"Token verification failed: {e}")
return NonePythonJWKS Endpoint
Create backend/app/routers/jwks.py:
from fastapi import APIRouter
from app.auth.key_manager import key_manager
router = APIRouter()
@router.get("/.well-known/jwks.json")
async def jwks():
"""JSON Web Key Set endpoint for public key distribution"""
return key_manager.get_jwks()
@router.get("/jwks")
async def jwks_alt():
"""Alternative JWKS endpoint"""
return key_manager.get_jwks()
@router.post("/admin/rotate-keys")
async def rotate_keys():
"""Manual key rotation endpoint (admin only)"""
# In production, add proper authentication and authorization
key_manager.rotate_keys()
return {"message": "Key rotation completed"}
@router.get("/admin/key-status")
async def key_status():
"""Get key rotation status (admin only)"""
signing_key = key_manager.get_signing_key()
return {
"current_signing_key": signing_key.kid if signing_key else None,
"total_keys": len(key_manager.keys),
"keys": [
{
"kid": k.kid,
"created_at": k.created_at.isoformat(),
"expires_at": k.expires_at.isoformat() if k.expires_at else None,
"is_current": k.kid == key_manager.current_signing_key
}
for k in key_manager.keys.values()
]
}PythonAutomated Key Rotation with Background Tasks
Create backend/app/tasks/key_rotation.py:
import asyncio
from datetime import datetime, timedelta
from app.auth.key_manager import key_manager
import logging
logger = logging.getLogger(__name__)
class KeyRotationScheduler:
def __init__(self):
self.running = False
self.task = None
async def start(self):
"""Start the key rotation scheduler"""
if self.running:
return
self.running = True
self.task = asyncio.create_task(self._rotation_loop())
logger.info("Key rotation scheduler started")
async def stop(self):
"""Stop the key rotation scheduler"""
self.running = False
if self.task:
self.task.cancel()
try:
await self.task
except asyncio.CancelledError:
pass
logger.info("Key rotation scheduler stopped")
async def _rotation_loop(self):
"""Main rotation loop"""
while self.running:
try:
# Perform maintenance every hour
key_manager.perform_maintenance()
# Wait for next check (1 hour)
await asyncio.sleep(3600)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Key rotation error: {e}")
# Wait a bit before retrying
await asyncio.sleep(300) # 5 minutes
# Global scheduler instance
key_rotation_scheduler = KeyRotationScheduler()
# Add to FastAPI startup/shutdown events
# In main.py:
# @app.on_event("startup")
# async def startup():
# await key_rotation_scheduler.start()
#
# @app.on_event("shutdown")
# async def shutdown():
# await key_rotation_scheduler.stop()PythonThis key management system provides:
- Automatic Key Rotation: Regular generation of new signing keys
- JWKS Endpoint: Public distribution of verification keys
- Graceful Key Transitions: Old keys remain valid during grace period
- Key Persistence: Keys are stored securely on disk
- Background Tasks: Automated maintenance and rotation
- Admin Interface: Manual control and monitoring of key rotation
The system ensures continuous operation while maintaining security through regular key rotation.
14. Token Revocation and Blacklisting
Token revocation is crucial for immediate security response. This chapter covers various approaches to token revocation and blacklisting strategies.
Token Revocation Strategies
graph TB
A[Token Revocation Methods] --> B[Server-Side Blacklist]
A --> C[Database Tracking]
A --> D[Redis Cache]
A --> E[Short Token Lifetimes]
B --> F[In-Memory Set]
B --> G[File-Based Storage]
C --> H[Token Table]
C --> I[User Sessions Table]
D --> J[Distributed Cache]
D --> K[Pub/Sub Notifications]Redis-Based Token Blacklist
Create backend/app/auth/token_blacklist.py:
import redis
import json
from typing import Optional, Set
from datetime import datetime, timedelta
from decouple import config
import asyncio
import aioredis
class TokenBlacklist:
def __init__(self):
# Redis configuration
self.redis_url = config("REDIS_URL", default="redis://localhost:6379")
self.redis_client = None
self.blacklist_prefix = "jwt_blacklist:"
self.user_sessions_prefix = "user_sessions:"
# Fallback in-memory storage
self.memory_blacklist: Set[str] = set()
self.use_redis = config("USE_REDIS", default=True, cast=bool)
async def initialize(self):
"""Initialize Redis connection"""
if self.use_redis:
try:
self.redis_client = await aioredis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=True
)
# Test connection
await self.redis_client.ping()
print("Redis connected for token blacklist")
except Exception as e:
print(f"Redis connection failed: {e}. Using in-memory blacklist.")
self.use_redis = False
async def blacklist_token(self, jti: str, exp_timestamp: int, user_id: Optional[str] = None):
"""Add token to blacklist"""
if self.use_redis and self.redis_client:
try:
key = f"{self.blacklist_prefix}{jti}"
# Store with expiration based on token's exp claim
current_timestamp = int(datetime.utcnow().timestamp())
ttl = max(exp_timestamp - current_timestamp, 60) # At least 1 minute
token_info = {
"jti": jti,
"blacklisted_at": current_timestamp,
"user_id": user_id,
"expires_at": exp_timestamp
}
await self.redis_client.setex(
key,
ttl,
json.dumps(token_info)
)
return True
except Exception as e:
print(f"Redis blacklist failed: {e}")
# Fall back to memory
self.memory_blacklist.add(jti)
return True
else:
# Use in-memory storage
self.memory_blacklist.add(jti)
return True
async def is_blacklisted(self, jti: str) -> bool:
"""Check if token is blacklisted"""
if self.use_redis and self.redis_client:
try:
key = f"{self.blacklist_prefix}{jti}"
result = await self.redis_client.get(key)
return result is not None
except Exception as e:
print(f"Redis check failed: {e}")
# Fall back to memory
return jti in self.memory_blacklist
else:
return jti in self.memory_blacklist
async def blacklist_user_tokens(self, user_id: str):
"""Blacklist all tokens for a specific user"""
if self.use_redis and self.redis_client:
try:
# Store user blacklist timestamp
key = f"{self.user_sessions_prefix}{user_id}:blacklisted_at"
current_timestamp = int(datetime.utcnow().timestamp())
# Set with reasonable TTL (e.g., max token lifetime)
await self.redis_client.setex(
key,
86400 * 7, # 7 days
current_timestamp
)
return True
except Exception as e:
print(f"User token blacklist failed: {e}")
return False
else:
# For in-memory, we would need to track user tokens separately
# This is simplified for the example
return False
async def is_user_blacklisted(self, user_id: str, token_issued_at: int) -> bool:
"""Check if user was blacklisted after token issuance"""
if self.use_redis and self.redis_client:
try:
key = f"{self.user_sessions_prefix}{user_id}:blacklisted_at"
blacklisted_at = await self.redis_client.get(key)
if blacklisted_at:
return int(blacklisted_at) > token_issued_at
return False
except Exception as e:
print(f"User blacklist check failed: {e}")
return False
else:
return False
async def get_blacklist_stats(self) -> dict:
"""Get blacklist statistics"""
if self.use_redis and self.redis_client:
try:
# Count blacklisted tokens
keys = await self.redis_client.keys(f"{self.blacklist_prefix}*")
token_count = len(keys)
# Count blacklisted users
user_keys = await self.redis_client.keys(f"{self.user_sessions_prefix}*:blacklisted_at")
user_count = len(user_keys)
return {
"blacklisted_tokens": token_count,
"blacklisted_users": user_count,
"storage": "redis"
}
except Exception as e:
print(f"Stats retrieval failed: {e}")
return {"error": str(e)}
else:
return {
"blacklisted_tokens": len(self.memory_blacklist),
"blacklisted_users": 0,
"storage": "memory"
}
async def cleanup_expired(self):
"""Clean up expired blacklist entries (Redis handles this automatically)"""
if not self.use_redis:
# For in-memory storage, we would need manual cleanup
# This is simplified for the example
pass
# Global blacklist instance
token_blacklist = TokenBlacklist()PythonEnhanced JWT Handler with Blacklist Support
Update the JWT handler to support token revocation:
# Enhanced jwt_handler.py with blacklist support
from app.auth.token_blacklist import token_blacklist
import uuid
class BlacklistAwareJWTHandler:
def __init__(self):
# ... existing initialization ...
self.blacklist = token_blacklist
async def create_access_token(self, data: Dict[str, Any]) -> str:
"""Create access token with unique JTI for blacklisting"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
# Generate unique token ID for blacklisting
jti = str(uuid.uuid4())
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"jti": jti,
"type": "access"
})
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
return encoded_jwt
async def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Verify token and check blacklist"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
# Check if token is blacklisted
jti = payload.get("jti")
if jti and await self.blacklist.is_blacklisted(jti):
return None
# Check if user is blacklisted after token issuance
user_id = payload.get("sub")
token_iat = payload.get("iat")
if user_id and token_iat:
if await self.blacklist.is_user_blacklisted(user_id, token_iat):
return None
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.JWTError:
return None
async def revoke_token(self, token: str) -> bool:
"""Revoke a specific token"""
try:
# Decode token without verification to get claims
payload = jwt.decode(
token,
options={"verify_signature": False}
)
jti = payload.get("jti")
exp = payload.get("exp")
user_id = payload.get("sub")
if jti and exp:
await self.blacklist.blacklist_token(jti, exp, user_id)
return True
return False
except Exception as e:
print(f"Token revocation failed: {e}")
return False
async def revoke_user_tokens(self, user_id: str) -> bool:
"""Revoke all tokens for a specific user"""
return await self.blacklist.blacklist_user_tokens(user_id)PythonToken Revocation API Endpoints
Create backend/app/routers/revocation.py:
from fastapi import APIRouter, Depends, HTTPException, status, Form
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.auth.jwt_handler import BlacklistAwareJWTHandler
from app.auth.permissions import RequireAdmin
from typing import Dict
router = APIRouter()
security = HTTPBearer()
jwt_handler = BlacklistAwareJWTHandler()
@router.post("/revoke")
async def revoke_token(
token: str = Form(...),
credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""Revoke a specific token"""
# Verify the requesting user is authenticated
payload = await jwt_handler.verify_token(credentials.credentials)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token"
)
# Users can only revoke their own tokens unless admin
token_payload = jwt.decode(token, options={"verify_signature": False})
token_user_id = token_payload.get("sub")
current_user_id = payload.get("sub")
user_role = payload.get("role")
if token_user_id != current_user_id and user_role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only revoke your own tokens"
)
success = await jwt_handler.revoke_token(token)
if success:
return {"message": "Token revoked successfully"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to revoke token"
)
@router.post("/revoke-user/{user_id}")
async def revoke_user_tokens(
user_id: str,
current_user = Depends(RequireAdmin)
):
"""Revoke all tokens for a specific user (admin only)"""
success = await jwt_handler.revoke_user_tokens(user_id)
if success:
return {"message": f"All tokens revoked for user {user_id}"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to revoke user tokens"
)
@router.post("/logout")
async def logout(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Logout and revoke current token"""
token = credentials.credentials
payload = await jwt_handler.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Revoke the current token
success = await jwt_handler.revoke_token(token)
return {
"message": "Logged out successfully",
"token_revoked": success
}
@router.get("/blacklist/stats")
async def get_blacklist_stats(current_user = Depends(RequireAdmin)):
"""Get blacklist statistics (admin only)"""
stats = await jwt_handler.blacklist.get_blacklist_stats()
return stats
@router.post("/blacklist/cleanup")
async def cleanup_blacklist(current_user = Depends(RequireAdmin)):
"""Clean up expired blacklist entries (admin only)"""
await jwt_handler.blacklist.cleanup_expired()
return {"message": "Blacklist cleanup completed"}PythonFrontend Token Revocation
Create frontend/js/tokenRevocation.js:
/**
* Token Revocation Management
*/
class TokenRevocationManager {
constructor(apiClient) {
this.api = apiClient;
}
/**
* Revoke current access token (logout)
*/
async logout() {
try {
const response = await this.api.post('/revocation/logout');
// Clear local storage
this.api.removeToken();
return response;
} catch (error) {
console.error('Logout failed:', error);
// Clear tokens anyway
this.api.removeToken();
throw error;
}
}
/**
* Revoke a specific token
*/
async revokeToken(token) {
try {
const formData = new FormData();
formData.append('token', token);
const response = await fetch(`${this.api.baseUrl}/revocation/revoke`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.api.getStoredToken()}`
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Token revocation failed:', error);
throw error;
}
}
/**
* Revoke all tokens for a user (admin only)
*/
async revokeUserTokens(userId) {
try {
return await this.api.post(`/revocation/revoke-user/${userId}`);
} catch (error) {
console.error('User token revocation failed:', error);
throw error;
}
}
/**
* Get blacklist statistics (admin only)
*/
async getBlacklistStats() {
try {
return await this.api.get('/revocation/blacklist/stats');
} catch (error) {
console.error('Failed to get blacklist stats:', error);
throw error;
}
}
/**
* Emergency logout - clear all local data
*/
emergencyLogout() {
// Clear all possible token storage locations
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('jwt_user_data');
sessionStorage.clear();
// Clear any cookies (if using cookie storage)
document.cookie.split(";").forEach((c) => {
const eqPos = c.indexOf("=");
const name = eqPos > -1 ? c.substr(0, eqPos) : c;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
});
// Redirect to login
window.location.href = '/login';
}
/**
* Setup automatic token validation
*/
setupTokenValidation() {
setInterval(async () => {
const token = this.api.getStoredToken();
if (token) {
try {
// Try to use the token
await this.api.get('/auth/me');
} catch (error) {
if (error.status === 401) {
// Token is invalid or revoked
this.emergencyLogout();
}
}
}
}, 60000); // Check every minute
}
}
// Add to global API client
if (typeof api !== 'undefined') {
api.revocation = new TokenRevocationManager(api);
// Setup automatic validation
api.revocation.setupTokenValidation();
}JavaScript15. Security Best Practices
This chapter covers comprehensive security practices for JWT implementation, including protection against common attacks and vulnerabilities.
Security Checklist
graph TB
A[JWT Security Best Practices] --> B[Token Security]
A --> C[Storage Security]
A --> D[Transport Security]
A --> E[Application Security]
B --> F[Strong Algorithms]
B --> G[Key Management]
B --> H[Short Lifetimes]
B --> I[Token Validation]
C --> J[Secure Storage]
C --> K[HttpOnly Cookies]
C --> L[Same-Site Policies]
D --> M[HTTPS Only]
D --> N[HSTS Headers]
D --> O[Certificate Pinning]
E --> P[CSRF Protection]
E --> Q[XSS Prevention]
E --> R[Input Validation]
E --> S[Rate Limiting]Comprehensive Security Implementation
Create backend/app/security/security_middleware.py:
from fastapi import Request, Response, HTTPException, status
from fastapi.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import time
import hashlib
import hmac
import re
from typing import Dict, Set
from collections import defaultdict, deque
from datetime import datetime, timedelta
import asyncio
class SecurityMiddleware(BaseHTTPMiddleware):
def __init__(self, app, config: Dict = None):
super().__init__(app)
self.config = config or {}
# Rate limiting
self.rate_limit_requests = self.config.get('rate_limit_requests', 100)
self.rate_limit_window = self.config.get('rate_limit_window', 3600) # 1 hour
self.request_counts = defaultdict(deque)
# CSRF protection
self.csrf_secret = self.config.get('csrf_secret', 'your-csrf-secret-key')
self.csrf_token_header = self.config.get('csrf_header', 'X-CSRF-Token')
# Security headers
self.security_headers = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
}
# Blocked IPs and suspicious patterns
self.blocked_ips: Set[str] = set()
self.suspicious_patterns = [
r'<script',
r'javascript:',
r'onload=',
r'onerror=',
r'eval\(',
r'exec\(',
]
async def dispatch(self, request: Request, call_next):
start_time = time.time()
# Get client IP
client_ip = self.get_client_ip(request)
# Security checks
try:
# 1. Check if IP is blocked
if client_ip in self.blocked_ips:
return JSONResponse(
status_code=403,
content={"detail": "Access denied"}
)
# 2. Rate limiting
if self.is_rate_limited(client_ip):
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded"}
)
# 3. HTTPS enforcement
if not self.is_https(request) and self.config.get('enforce_https', True):
return JSONResponse(
status_code=426,
content={"detail": "HTTPS required"}
)
# 4. CSRF protection for state-changing requests
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
if not self.validate_csrf_token(request):
return JSONResponse(
status_code=403,
content={"detail": "CSRF token missing or invalid"}
)
# 5. Input validation
if not await self.validate_request_data(request):
return JSONResponse(
status_code=400,
content={"detail": "Invalid request data"}
)
# Process request
response = await call_next(request)
# Add security headers
for header, value in self.security_headers.items():
response.headers[header] = value
# Add processing time header
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
except Exception as e:
# Log security incident
await self.log_security_incident(client_ip, str(e), request)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
def get_client_ip(self, request: Request) -> str:
"""Get real client IP address"""
# Check for proxy headers
forwarded_for = request.headers.get('X-Forwarded-For')
if forwarded_for:
return forwarded_for.split(',')[0].strip()
real_ip = request.headers.get('X-Real-IP')
if real_ip:
return real_ip
return request.client.host
def is_rate_limited(self, client_ip: str) -> bool:
"""Check if client is rate limited"""
current_time = time.time()
cutoff_time = current_time - self.rate_limit_window
# Clean old requests
while (self.request_counts[client_ip] and
self.request_counts[client_ip][0] < cutoff_time):
self.request_counts[client_ip].popleft()
# Check current request count
if len(self.request_counts[client_ip]) >= self.rate_limit_requests:
return True
# Add current request
self.request_counts[client_ip].append(current_time)
return False
def is_https(self, request: Request) -> bool:
"""Check if request is over HTTPS"""
return request.url.scheme == 'https'
def generate_csrf_token(self, session_id: str) -> str:
"""Generate CSRF token"""
timestamp = str(int(time.time()))
message = f"{session_id}:{timestamp}"
signature = hmac.new(
self.csrf_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return f"{timestamp}:{signature}"
def validate_csrf_token(self, request: Request) -> bool:
"""Validate CSRF token"""
# Skip CSRF for API calls with proper JWT authentication
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
return True
# Check for CSRF token in header
csrf_token = request.headers.get(self.csrf_token_header)
if not csrf_token:
return False
try:
timestamp, signature = csrf_token.split(':')
# Check token age (valid for 1 hour)
token_time = int(timestamp)
if time.time() - token_time > 3600:
return False
# Validate signature
session_id = request.cookies.get('session_id', 'anonymous')
message = f"{session_id}:{timestamp}"
expected_signature = hmac.new(
self.csrf_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
except (ValueError, TypeError):
return False
async def validate_request_data(self, request: Request) -> bool:
"""Validate request data for suspicious content"""
# Check query parameters
for key, value in request.query_params.items():
if self.contains_suspicious_content(str(value)):
return False
# Check headers
for key, value in request.headers.items():
if self.contains_suspicious_content(str(value)):
return False
# Check body for certain content types
content_type = request.headers.get('content-type', '')
if content_type.startswith('application/json'):
try:
body = await request.body()
if body and self.contains_suspicious_content(body.decode()):
return False
except Exception:
# If we can't read the body, let it through
# (it will likely fail later validation)
pass
return True
def contains_suspicious_content(self, content: str) -> bool:
"""Check if content contains suspicious patterns"""
content_lower = content.lower()
for pattern in self.suspicious_patterns:
if re.search(pattern, content_lower):
return True
return False
async def log_security_incident(self, client_ip: str, error: str, request: Request):
"""Log security incidents"""
incident = {
'timestamp': datetime.utcnow().isoformat(),
'client_ip': client_ip,
'error': error,
'method': request.method,
'url': str(request.url),
'user_agent': request.headers.get('user-agent', ''),
'headers': dict(request.headers)
}
# In production, send to security monitoring system
print(f"Security incident: {incident}")
def block_ip(self, ip_address: str):
"""Block an IP address"""
self.blocked_ips.add(ip_address)
def unblock_ip(self, ip_address: str):
"""Unblock an IP address"""
self.blocked_ips.discard(ip_address)PythonXSS Protection
Create backend/app/security/xss_protection.py:
import html
import re
from typing import Any, Dict, List
import bleach
from markupsafe import Markup
class XSSProtection:
def __init__(self):
# Allowed HTML tags and attributes for rich content
self.allowed_tags = [
'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'
]
self.allowed_attributes = {
'*': ['class'],
'a': ['href', 'title'],
'img': ['src', 'alt', 'width', 'height']
}
# Dangerous patterns to remove
self.dangerous_patterns = [
r'javascript:',
r'vbscript:',
r'onload=',
r'onerror=',
r'onclick=',
r'onmouseover=',
r'onfocus=',
r'onblur=',
r'<script',
r'</script>',
r'<iframe',
r'<object',
r'<embed',
r'<form',
r'<input',
r'<textarea',
r'<select',
r'<button'
]
def sanitize_html(self, content: str, allow_rich_content: bool = False) -> str:
"""Sanitize HTML content"""
if not content:
return content
if allow_rich_content:
# Use bleach for rich content sanitization
return bleach.clean(
content,
tags=self.allowed_tags,
attributes=self.allowed_attributes,
strip=True
)
else:
# Escape all HTML for plain text
return html.escape(content)
def sanitize_user_input(self, data: Any) -> Any:
"""Recursively sanitize user input"""
if isinstance(data, str):
return self.sanitize_string(data)
elif isinstance(data, dict):
return {key: self.sanitize_user_input(value) for key, value in data.items()}
elif isinstance(data, list):
return [self.sanitize_user_input(item) for item in data]
else:
return data
def sanitize_string(self, text: str) -> str:
"""Sanitize a string for dangerous content"""
if not text:
return text
# Remove dangerous patterns
sanitized = text
for pattern in self.dangerous_patterns:
sanitized = re.sub(pattern, '', sanitized, flags=re.IGNORECASE)
# Remove null bytes and control characters
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', sanitized)
return sanitized
def validate_url(self, url: str) -> bool:
"""Validate URL for safety"""
if not url:
return True
# Check for dangerous protocols
dangerous_protocols = ['javascript:', 'vbscript:', 'data:', 'file:']
url_lower = url.lower().strip()
for protocol in dangerous_protocols:
if url_lower.startswith(protocol):
return False
# Only allow HTTP and HTTPS
if not (url_lower.startswith('http://') or url_lower.startswith('https://')):
return False
return True
def create_content_security_policy(self, nonce: str = None) -> str:
"""Create Content Security Policy header"""
policy_parts = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
]
if nonce:
# Add nonce for inline scripts
policy_parts[1] = f"script-src 'self' 'nonce-{nonce}'"
return "; ".join(policy_parts)
# Global XSS protection instance
xss_protection = XSSProtection()PythonCSRF Protection
Create backend/app/security/csrf_protection.py:
import secrets
import hmac
import hashlib
import time
from typing import Optional
from fastapi import Request, HTTPException, status
class CSRFProtection:
def __init__(self, secret_key: str):
self.secret_key = secret_key
self.token_lifetime = 3600 # 1 hour
def generate_csrf_token(self, session_id: str) -> str:
"""Generate CSRF token for session"""
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(16)
# Create message to sign
message = f"{session_id}:{timestamp}:{nonce}"
# Create signature
signature = hmac.new(
self.secret_key.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# Return token
return f"{timestamp}:{nonce}:{signature}"
def validate_csrf_token(self, token: str, session_id: str) -> bool:
"""Validate CSRF token"""
if not token:
return False
try:
timestamp_str, nonce, signature = token.split(':')
timestamp = int(timestamp_str)
# Check token age
if time.time() - timestamp > self.token_lifetime:
return False
# Recreate message
message = f"{session_id}:{timestamp_str}:{nonce}"
# Verify signature
expected_signature = hmac.new(
self.secret_key.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
except (ValueError, TypeError):
return False
def get_csrf_token_from_request(self, request: Request) -> Optional[str]:
"""Extract CSRF token from request"""
# Check header first
token = request.headers.get('X-CSRF-Token')
if token:
return token
# Check form data
if hasattr(request, 'form'):
form_data = request.form()
return form_data.get('csrf_token')
return None
def require_csrf_token(self, request: Request, session_id: str):
"""Middleware function to require CSRF token"""
# Skip for safe methods
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return
# Skip for API requests with JWT
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
return
# Get and validate CSRF token
csrf_token = self.get_csrf_token_from_request(request)
if not csrf_token or not self.validate_csrf_token(csrf_token, session_id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF token missing or invalid"
)PythonSecure Headers Implementation
Create backend/app/security/secure_headers.py:
from fastapi import Response
from typing import Dict, Optional
class SecureHeaders:
def __init__(self):
self.default_headers = {
# Prevent MIME type sniffing
'X-Content-Type-Options': 'nosniff',
# Prevent clickjacking
'X-Frame-Options': 'DENY',
# XSS protection
'X-XSS-Protection': '1; mode=block',
# HTTPS enforcement
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
# Referrer policy
'Referrer-Policy': 'strict-origin-when-cross-origin',
# Permissions policy
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
# Cache control for sensitive pages
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
def get_csp_header(self, nonce: Optional[str] = None, report_uri: Optional[str] = None) -> str:
"""Generate Content Security Policy header"""
directives = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"media-src 'self'",
"object-src 'none'",
"child-src 'none'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests"
]
# Add nonce for inline scripts if provided
if nonce:
directives[1] = f"script-src 'self' 'nonce-{nonce}'"
# Add report URI if provided
if report_uri:
directives.append(f"report-uri {report_uri}")
return "; ".join(directives)
def apply_security_headers(
self,
response: Response,
csp_nonce: Optional[str] = None,
custom_headers: Optional[Dict[str, str]] = None
) -> Response:
"""Apply all security headers to response"""
# Apply default headers
for header, value in self.default_headers.items():
response.headers[header] = value
# Add CSP header
csp_header = self.get_csp_header(nonce=csp_nonce)
response.headers['Content-Security-Policy'] = csp_header
# Apply custom headers
if custom_headers:
for header, value in custom_headers.items():
response.headers[header] = value
return response
def get_cookie_security_attributes(self, is_https: bool = True) -> Dict[str, any]:
"""Get secure cookie attributes"""
attributes = {
'httponly': True,
'samesite': 'strict',
'max_age': 3600 # 1 hour
}
if is_https:
attributes['secure'] = True
return attributes
# Global secure headers instance
secure_headers = SecureHeaders()PythonSecurity Monitoring
Create backend/app/security/security_monitor.py:
import asyncio
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from collections import defaultdict, deque
import logging
class SecurityMonitor:
def __init__(self):
self.logger = logging.getLogger('security')
# Security events tracking
self.failed_logins = defaultdict(deque)
self.suspicious_activities = defaultdict(deque)
self.blocked_ips = set()
# Thresholds
self.max_failed_logins = 5
self.failed_login_window = 300 # 5 minutes
self.suspicious_activity_threshold = 10
self.monitoring_window = 3600 # 1 hour
async def log_failed_login(self, ip_address: str, username: str, user_agent: str):
"""Log failed login attempt"""
event = {
'timestamp': datetime.utcnow().isoformat(),
'ip_address': ip_address,
'username': username,
'user_agent': user_agent,
'event_type': 'failed_login'
}
self.logger.warning(f"Failed login attempt: {json.dumps(event)}")
# Track failed logins
current_time = datetime.utcnow()
self.failed_logins[ip_address].append(current_time)
# Clean old entries
cutoff_time = current_time - timedelta(seconds=self.failed_login_window)
while (self.failed_logins[ip_address] and
self.failed_logins[ip_address][0] < cutoff_time):
self.failed_logins[ip_address].popleft()
# Check if IP should be blocked
if len(self.failed_logins[ip_address]) >= self.max_failed_logins:
await self.block_ip(ip_address, reason="Too many failed login attempts")
async def log_suspicious_activity(self, ip_address: str, activity: str, details: Dict):
"""Log suspicious activity"""
event = {
'timestamp': datetime.utcnow().isoformat(),
'ip_address': ip_address,
'activity': activity,
'details': details,
'event_type': 'suspicious_activity'
}
self.logger.warning(f"Suspicious activity: {json.dumps(event)}")
# Track suspicious activities
current_time = datetime.utcnow()
self.suspicious_activities[ip_address].append(current_time)
# Clean old entries
cutoff_time = current_time - timedelta(seconds=self.monitoring_window)
while (self.suspicious_activities[ip_address] and
self.suspicious_activities[ip_address][0] < cutoff_time):
self.suspicious_activities[ip_address].popleft()
# Check threshold
if len(self.suspicious_activities[ip_address]) >= self.suspicious_activity_threshold:
await self.block_ip(ip_address, reason="Suspicious activity pattern detected")
async def log_security_event(self, event_type: str, details: Dict):
"""Log general security event"""
event = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'details': details
}
self.logger.info(f"Security event: {json.dumps(event)}")
async def block_ip(self, ip_address: str, reason: str, duration: int = 3600):
"""Block IP address"""
self.blocked_ips.add(ip_address)
event = {
'timestamp': datetime.utcnow().isoformat(),
'ip_address': ip_address,
'reason': reason,
'duration': duration,
'event_type': 'ip_blocked'
}
self.logger.error(f"IP blocked: {json.dumps(event)}")
# Schedule unblocking
asyncio.create_task(self._unblock_ip_after_delay(ip_address, duration))
async def _unblock_ip_after_delay(self, ip_address: str, delay: int):
"""Unblock IP after delay"""
await asyncio.sleep(delay)
self.blocked_ips.discard(ip_address)
event = {
'timestamp': datetime.utcnow().isoformat(),
'ip_address': ip_address,
'event_type': 'ip_unblocked'
}
self.logger.info(f"IP unblocked: {json.dumps(event)}")
def is_ip_blocked(self, ip_address: str) -> bool:
"""Check if IP is blocked"""
return ip_address in self.blocked_ips
async def get_security_stats(self) -> Dict:
"""Get security statistics"""
current_time = datetime.utcnow()
hour_ago = current_time - timedelta(hours=1)
# Count recent events
recent_failed_logins = sum(
len([t for t in times if t > hour_ago])
for times in self.failed_logins.values()
)
recent_suspicious = sum(
len([t for t in times if t > hour_ago])
for times in self.suspicious_activities.values()
)
return {
'blocked_ips': len(self.blocked_ips),
'recent_failed_logins': recent_failed_logins,
'recent_suspicious_activities': recent_suspicious,
'monitoring_since': (current_time - timedelta(hours=1)).isoformat()
}
# Global security monitor instance
security_monitor = SecurityMonitor()PythonThis comprehensive security implementation provides:
- Middleware Protection: Rate limiting, CSRF, input validation, and security headers
- XSS Prevention: Content sanitization and CSP headers
- CSRF Protection: Token-based CSRF protection for state-changing requests
- Secure Headers: Comprehensive security headers for all responses
- Security Monitoring: Real-time monitoring of security events and automatic IP blocking
- Attack Prevention: Protection against common web vulnerabilities
The system provides defense-in-depth security while maintaining good user experience and performance.
16. Complete Project Examples
This chapter provides complete, production-ready examples that combine all the concepts covered in this guide.
E-commerce Platform with JWT Authentication
Project Structure
ecommerce-jwt/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── config.py
│ │ ├── database.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── user.py
│ │ │ ├── product.py
│ │ │ └── order.py
│ │ ├── auth/
│ │ │ ├── __init__.py
│ │ │ ├── jwt_handler.py
│ │ │ ├── permissions.py
│ │ │ └── oauth.py
│ │ ├── routers/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── users.py
│ │ │ ├── products.py
│ │ │ └── orders.py
│ │ └── security/
│ │ ├── __init__.py
│ │ ├── middleware.py
│ │ └── validators.py
│ ├── requirements.txt
│ ├── .env
│ └── docker-compose.yml
└── frontend/
├── index.html
├── login.html
├── dashboard.html
├── css/
│ └── styles.css
├── js/
│ ├── api.js
│ ├── auth.js
│ ├── products.js
│ └── orders.js
└── assets/
└── images/BashBackend Implementation
Complete main.py:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer
from contextlib import asynccontextmanager
import uvicorn
from app.config import settings
from app.database import engine, Base, get_db
from app.auth.jwt_handler import jwt_handler
from app.auth.token_blacklist import token_blacklist
from app.security.security_middleware import SecurityMiddleware
from app.routers import auth, users, products, orders
from app.models import user, product, order
# Create tables
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
Base.metadata.create_all(bind=engine)
await token_blacklist.initialize()
yield
# Shutdown
pass
app = FastAPI(
title="E-commerce JWT API",
description="Complete e-commerce platform with JWT authentication",
version="1.0.0",
lifespan=lifespan
)
# Security middleware
app.add_middleware(
SecurityMiddleware,
config={
'rate_limit_requests': 100,
'rate_limit_window': 3600,
'enforce_https': settings.ENVIRONMENT == 'production',
'csrf_secret': settings.SECRET_KEY
}
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["authentication"])
app.include_router(users.router, prefix="/api/users", tags=["users"])
app.include_router(products.router, prefix="/api/products", tags=["products"])
app.include_router(orders.router, prefix="/api/orders", tags=["orders"])
# Health check
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
"version": "1.0.0"
}
# Root endpoint
@app.get("/")
async def root():
return {
"message": "E-commerce JWT API",
"docs": "/docs",
"version": "1.0.0"
}
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=settings.ENVIRONMENT == "development"
)PythonProduct Model (app/models/product.py):
from sqlalchemy import Column, Integer, String, Float, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False, index=True)
description = Column(Text)
price = Column(Float, nullable=False)
category_id = Column(Integer, ForeignKey("categories.id"))
stock_quantity = Column(Integer, default=0)
sku = Column(String(100), unique=True, index=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
category = relationship("Category", back_populates="products")
order_items = relationship("OrderItem", back_populates="product")
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True)
description = Column(Text)
parent_id = Column(Integer, ForeignKey("categories.id"))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
products = relationship("Product", back_populates="category")
parent = relationship("Category", remote_side=[id])PythonOrder Model (app/models/order.py):
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
import enum
class OrderStatus(enum.Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
order_number = Column(String(50), unique=True, index=True)
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING)
total_amount = Column(Float, nullable=False)
shipping_address = Column(String(500))
billing_address = Column(String(500))
payment_method = Column(String(50))
payment_status = Column(String(50), default="pending")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="orders")
order_items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False)
unit_price = Column(Float, nullable=False)
total_price = Column(Float, nullable=False)
# Relationships
order = relationship("Order", back_populates="order_items")
product = relationship("Product", back_populates="order_items")PythonProducts Router (app/routers/products.py):
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from app.database import get_db
from app.models.product import Product, Category
from app.auth.permissions import RequireAuthentication, RequireAdmin
from app.schemas.product import ProductCreate, ProductUpdate, ProductResponse, CategoryResponse
import uuid
router = APIRouter()
@router.get("/", response_model=List[ProductResponse])
async def get_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
category_id: Optional[int] = None,
search: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
db: Session = Depends(get_db)
):
"""Get products with filtering and pagination"""
query = db.query(Product).filter(Product.is_active == True)
# Apply filters
if category_id:
query = query.filter(Product.category_id == category_id)
if search:
query = query.filter(Product.name.contains(search))
if min_price is not None:
query = query.filter(Product.price >= min_price)
if max_price is not None:
query = query.filter(Product.price <= max_price)
# Pagination
products = query.offset(skip).limit(limit).all()
return products
@router.get("/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int, db: Session = Depends(get_db)):
"""Get product by ID"""
product = db.query(Product).filter(
Product.id == product_id,
Product.is_active == True
).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
return product
@router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED)
async def create_product(
product: ProductCreate,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Create new product (admin only)"""
# Generate SKU if not provided
if not product.sku:
product.sku = f"SKU-{uuid.uuid4().hex[:8].upper()}"
# Check if SKU already exists
existing_product = db.query(Product).filter(Product.sku == product.sku).first()
if existing_product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product with this SKU already exists"
)
db_product = Product(**product.dict())
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
@router.put("/{product_id}", response_model=ProductResponse)
async def update_product(
product_id: int,
product_update: ProductUpdate,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Update product (admin only)"""
db_product = db.query(Product).filter(Product.id == product_id).first()
if not db_product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
# Update fields
update_data = product_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_product, field, value)
db.commit()
db.refresh(db_product)
return db_product
@router.delete("/{product_id}")
async def delete_product(
product_id: int,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Soft delete product (admin only)"""
db_product = db.query(Product).filter(Product.id == product_id).first()
if not db_product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
db_product.is_active = False
db.commit()
return {"message": "Product deleted successfully"}
@router.get("/categories/", response_model=List[CategoryResponse])
async def get_categories(db: Session = Depends(get_db)):
"""Get all product categories"""
categories = db.query(Category).filter(Category.is_active == True).all()
return categories
@router.post("/categories/", response_model=CategoryResponse, status_code=status.HTTP_201_CREATED)
async def create_category(
category: CategoryCreate,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Create new category (admin only)"""
db_category = Category(**category.dict())
db.add(db_category)
db.commit()
db.refresh(db_category)
return db_categoryPythonOrders Router (app/routers/orders.py):
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from app.database import get_db
from app.models.order import Order, OrderItem, OrderStatus
from app.models.product import Product
from app.auth.permissions import RequireAuthentication, RequireAdmin
from app.schemas.order import OrderCreate, OrderResponse, OrderItemCreate
import uuid
from datetime import datetime
router = APIRouter()
@router.get("/", response_model=List[OrderResponse])
async def get_orders(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
status: Optional[OrderStatus] = None,
db: Session = Depends(get_db),
current_user = Depends(RequireAuthentication)
):
"""Get user's orders"""
query = db.query(Order).filter(Order.user_id == current_user.id)
if status:
query = query.filter(Order.status == status)
orders = query.order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
return orders
@router.get("/admin/all", response_model=List[OrderResponse])
async def get_all_orders(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
status: Optional[OrderStatus] = None,
user_id: Optional[int] = None,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Get all orders (admin only)"""
query = db.query(Order)
if status:
query = query.filter(Order.status == status)
if user_id:
query = query.filter(Order.user_id == user_id)
orders = query.order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
return orders
@router.get("/{order_id}", response_model=OrderResponse)
async def get_order(
order_id: int,
db: Session = Depends(get_db),
current_user = Depends(RequireAuthentication)
):
"""Get specific order"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
# Users can only see their own orders unless admin
if order.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
return order
@router.post("/", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
async def create_order(
order_data: OrderCreate,
db: Session = Depends(get_db),
current_user = Depends(RequireAuthentication)
):
"""Create new order"""
# Validate products and calculate total
total_amount = 0
order_items_data = []
for item in order_data.items:
product = db.query(Product).filter(
Product.id == item.product_id,
Product.is_active == True
).first()
if not product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Product {item.product_id} not found"
)
if product.stock_quantity < item.quantity:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Insufficient stock for product {product.name}"
)
item_total = product.price * item.quantity
total_amount += item_total
order_items_data.append({
"product_id": product.id,
"quantity": item.quantity,
"unit_price": product.price,
"total_price": item_total
})
# Create order
order_number = f"ORD-{datetime.utcnow().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8].upper()}"
db_order = Order(
user_id=current_user.id,
order_number=order_number,
total_amount=total_amount,
shipping_address=order_data.shipping_address,
billing_address=order_data.billing_address,
payment_method=order_data.payment_method
)
db.add(db_order)
db.flush() # Get order ID
# Create order items
for item_data in order_items_data:
db_order_item = OrderItem(
order_id=db_order.id,
**item_data
)
db.add(db_order_item)
# Update product stock
for item in order_data.items:
product = db.query(Product).filter(Product.id == item.product_id).first()
product.stock_quantity -= item.quantity
db.commit()
db.refresh(db_order)
return db_order
@router.put("/{order_id}/status", response_model=OrderResponse)
async def update_order_status(
order_id: int,
new_status: OrderStatus,
db: Session = Depends(get_db),
current_user = Depends(RequireAdmin)
):
"""Update order status (admin only)"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
order.status = new_status
db.commit()
db.refresh(order)
return order
@router.post("/{order_id}/cancel")
async def cancel_order(
order_id: int,
db: Session = Depends(get_db),
current_user = Depends(RequireAuthentication)
):
"""Cancel order"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
# Users can only cancel their own orders
if order.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Can only cancel pending or confirmed orders
if order.status not in [OrderStatus.PENDING, OrderStatus.CONFIRMED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Order cannot be cancelled"
)
# Restore product stock
for item in order.order_items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product:
product.stock_quantity += item.quantity
order.status = OrderStatus.CANCELLED
db.commit()
return {"message": "Order cancelled successfully"}PythonFrontend Implementation
Complete Dashboard (frontend/dashboard.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-commerce Dashboard</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">
<h2>E-Commerce</h2>
</div>
<div class="nav-menu">
<a href="#products" class="nav-link" data-section="products">Products</a>
<a href="#orders" class="nav-link" data-section="orders">Orders</a>
<a href="#profile" class="nav-link" data-section="profile">Profile</a>
<div class="nav-user">
<span id="user-name"></span>
<button id="logout-btn" class="btn btn-outline">Logout</button>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Products Section -->
<section id="products-section" class="content-section active">
<div class="section-header">
<h2>Products</h2>
<div class="section-controls">
<div class="search-box">
<input type="text" id="product-search" placeholder="Search products...">
<button id="search-btn" class="btn btn-primary">Search</button>
</div>
<div class="filter-box">
<select id="category-filter">
<option value="">All Categories</option>
</select>
<input type="number" id="min-price" placeholder="Min Price">
<input type="number" id="max-price" placeholder="Max Price">
<button id="filter-btn" class="btn btn-secondary">Filter</button>
</div>
</div>
</div>
<div id="products-grid" class="products-grid">
<!-- Products will be loaded here -->
</div>
<!-- Pagination -->
<div id="products-pagination" class="pagination">
<!-- Pagination controls will be added here -->
</div>
</section>
<!-- Orders Section -->
<section id="orders-section" class="content-section">
<div class="section-header">
<h2>My Orders</h2>
<div class="section-controls">
<select id="order-status-filter">
<option value="">All Orders</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="processing">Processing</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div id="orders-list" class="orders-list">
<!-- Orders will be loaded here -->
</div>
</section>
<!-- Profile Section -->
<section id="profile-section" class="content-section">
<div class="section-header">
<h2>Profile</h2>
</div>
<div class="profile-content">
<form id="profile-form" class="form">
<div class="form-group">
<label for="profile-email">Email</label>
<input type="email" id="profile-email" readonly>
</div>
<div class="form-group">
<label for="profile-fullname">Full Name</label>
<input type="text" id="profile-fullname" required>
</div>
<div class="form-group">
<label for="profile-phone">Phone</label>
<input type="tel" id="profile-phone">
</div>
<div class="form-group">
<label for="profile-address">Address</label>
<textarea id="profile-address"></textarea>
</div>
<button type="submit" class="btn btn-primary">Update Profile</button>
</form>
<div class="profile-actions">
<h3>Account Actions</h3>
<button id="change-password-btn" class="btn btn-secondary">Change Password</button>
<button id="download-data-btn" class="btn btn-outline">Download My Data</button>
<button id="delete-account-btn" class="btn btn-danger">Delete Account</button>
</div>
</div>
</section>
</main>
<!-- Shopping Cart Modal -->
<div id="cart-modal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2>Shopping Cart</h2>
<div id="cart-items"></div>
<div class="cart-total">
<strong>Total: $<span id="cart-total">0.00</span></strong>
</div>
<div class="cart-actions">
<button id="clear-cart-btn" class="btn btn-secondary">Clear Cart</button>
<button id="checkout-btn" class="btn btn-primary">Checkout</button>
</div>
</div>
</div>
<!-- Checkout Modal -->
<div id="checkout-modal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2>Checkout</h2>
<form id="checkout-form" class="form">
<div class="form-group">
<label for="shipping-address">Shipping Address</label>
<textarea id="shipping-address" required></textarea>
</div>
<div class="form-group">
<label for="billing-address">Billing Address</label>
<textarea id="billing-address" required></textarea>
</div>
<div class="form-group">
<label for="payment-method">Payment Method</label>
<select id="payment-method" required>
<option value="">Select Payment Method</option>
<option value="credit_card">Credit Card</option>
<option value="debit_card">Debit Card</option>
<option value="paypal">PayPal</option>
<option value="bank_transfer">Bank Transfer</option>
</select>
</div>
<div class="order-summary">
<h3>Order Summary</h3>
<div id="checkout-items"></div>
<div class="checkout-total">
<strong>Total: $<span id="checkout-total">0.00</span></strong>
</div>
</div>
<button type="submit" class="btn btn-primary">Place Order</button>
</form>
</div>
</div>
<!-- Cart Button -->
<div class="cart-button">
<button id="cart-btn" class="btn btn-primary">
Cart (<span id="cart-count">0</span>)
</button>
</div>
<script src="js/api.js"></script>
<script src="js/auth.js"></script>
<script src="js/products.js"></script>
<script src="js/orders.js"></script>
<script src="js/dashboard.js"></script>
</body>
</html>HTMLComplete Products JavaScript (frontend/js/products.js):
/**
* Products Management
*/
class ProductsManager {
constructor(apiClient) {
this.api = apiClient;
this.products = [];
this.categories = [];
this.cart = JSON.parse(localStorage.getItem('cart')) || [];
this.currentPage = 1;
this.itemsPerPage = 12;
this.totalProducts = 0;
this.initializeEventListeners();
this.loadCategories();
this.loadProducts();
this.updateCartDisplay();
}
initializeEventListeners() {
// Search and filter
document.getElementById('search-btn').addEventListener('click', () => this.searchProducts());
document.getElementById('filter-btn').addEventListener('click', () => this.filterProducts());
document.getElementById('product-search').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.searchProducts();
});
// Cart
document.getElementById('cart-btn').addEventListener('click', () => this.showCart());
document.getElementById('clear-cart-btn').addEventListener('click', () => this.clearCart());
document.getElementById('checkout-btn').addEventListener('click', () => this.showCheckout());
// Checkout
document.getElementById('checkout-form').addEventListener('submit', (e) => this.processCheckout(e));
// Modal close buttons
document.querySelectorAll('.modal .close').forEach(closeBtn => {
closeBtn.addEventListener('click', (e) => {
e.target.closest('.modal').style.display = 'none';
});
});
// Close modals when clicking outside
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
e.target.style.display = 'none';
}
});
}
async loadCategories() {
try {
const categories = await this.api.get('/products/categories/');
this.categories = categories;
this.renderCategoryFilter();
} catch (error) {
console.error('Failed to load categories:', error);
}
}
renderCategoryFilter() {
const categoryFilter = document.getElementById('category-filter');
categoryFilter.innerHTML = '<option value="">All Categories</option>';
this.categories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.name;
categoryFilter.appendChild(option);
});
}
async loadProducts(page = 1) {
try {
const params = new URLSearchParams({
skip: (page - 1) * this.itemsPerPage,
limit: this.itemsPerPage
});
// Add filters
const categoryFilter = document.getElementById('category-filter').value;
const searchQuery = document.getElementById('product-search').value;
const minPrice = document.getElementById('min-price').value;
const maxPrice = document.getElementById('max-price').value;
if (categoryFilter) params.append('category_id', categoryFilter);
if (searchQuery) params.append('search', searchQuery);
if (minPrice) params.append('min_price', minPrice);
if (maxPrice) params.append('max_price', maxPrice);
const products = await this.api.get(`/products/?${params}`);
this.products = products;
this.currentPage = page;
this.renderProducts();
this.renderPagination();
} catch (error) {
console.error('Failed to load products:', error);
this.showError('Failed to load products');
}
}
renderProducts() {
const productsGrid = document.getElementById('products-grid');
if (this.products.length === 0) {
productsGrid.innerHTML = '<div class="no-products">No products found</div>';
return;
}
productsGrid.innerHTML = this.products.map(product => `
<div class="product-card" data-product-id="${product.id}">
<div class="product-image">
<img src="/api/products/${product.id}/image" alt="${product.name}"
onerror="this.src='/assets/images/no-image.png'">
</div>
<div class="product-info">
<h3 class="product-name">${product.name}</h3>
<p class="product-description">${product.description || ''}</p>
<div class="product-price">$${product.price.toFixed(2)}</div>
<div class="product-stock">
${product.stock_quantity > 0
? `In Stock (${product.stock_quantity})`
: 'Out of Stock'
}
</div>
</div>
<div class="product-actions">
<div class="quantity-controls">
<button class="quantity-btn" onclick="this.nextElementSibling.stepDown()">-</button>
<input type="number" class="quantity-input" value="1" min="1" max="${product.stock_quantity}">
<button class="quantity-btn" onclick="this.previousElementSibling.stepUp()">+</button>
</div>
<button class="add-to-cart-btn btn btn-primary"
${product.stock_quantity === 0 ? 'disabled' : ''}
onclick="productsManager.addToCart(${product.id})">
Add to Cart
</button>
</div>
</div>
`).join('');
}
renderPagination() {
const pagination = document.getElementById('products-pagination');
const totalPages = Math.ceil(this.totalProducts / this.itemsPerPage);
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let paginationHTML = '';
// Previous button
if (this.currentPage > 1) {
paginationHTML += `<button class="pagination-btn" onclick="productsManager.loadProducts(${this.currentPage - 1})">Previous</button>`;
}
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === this.currentPage) {
paginationHTML += `<button class="pagination-btn active">${i}</button>`;
} else {
paginationHTML += `<button class="pagination-btn" onclick="productsManager.loadProducts(${i})">${i}</button>`;
}
}
// Next button
if (this.currentPage < totalPages) {
paginationHTML += `<button class="pagination-btn" onclick="productsManager.loadProducts(${this.currentPage + 1})">Next</button>`;
}
pagination.innerHTML = paginationHTML;
}
searchProducts() {
this.loadProducts(1);
}
filterProducts() {
this.loadProducts(1);
}
addToCart(productId) {
const product = this.products.find(p => p.id === productId);
if (!product) return;
const productCard = document.querySelector(`[data-product-id="${productId}"]`);
const quantity = parseInt(productCard.querySelector('.quantity-input').value);
const existingItem = this.cart.find(item => item.product_id === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cart.push({
product_id: productId,
product: product,
quantity: quantity,
unit_price: product.price
});
}
this.saveCart();
this.updateCartDisplay();
this.showSuccessMessage('Product added to cart');
}
removeFromCart(productId) {
this.cart = this.cart.filter(item => item.product_id !== productId);
this.saveCart();
this.updateCartDisplay();
this.renderCart();
}
updateCartQuantity(productId, quantity) {
const item = this.cart.find(item => item.product_id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
this.saveCart();
this.updateCartDisplay();
this.renderCart();
}
}
clearCart() {
this.cart = [];
this.saveCart();
this.updateCartDisplay();
this.renderCart();
}
saveCart() {
localStorage.setItem('cart', JSON.stringify(this.cart));
}
updateCartDisplay() {
const cartCount = this.cart.reduce((total, item) => total + item.quantity, 0);
document.getElementById('cart-count').textContent = cartCount;
}
showCart() {
this.renderCart();
document.getElementById('cart-modal').style.display = 'block';
}
renderCart() {
const cartItems = document.getElementById('cart-items');
const cartTotal = document.getElementById('cart-total');
if (this.cart.length === 0) {
cartItems.innerHTML = '<div class="empty-cart">Your cart is empty</div>';
cartTotal.textContent = '0.00';
return;
}
const total = this.cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
cartItems.innerHTML = this.cart.map(item => `
<div class="cart-item">
<div class="cart-item-info">
<h4>${item.product.name}</h4>
<p>$${item.unit_price.toFixed(2)} each</p>
</div>
<div class="cart-item-controls">
<div class="quantity-controls">
<button onclick="productsManager.updateCartQuantity(${item.product_id}, ${item.quantity - 1})">-</button>
<span>${item.quantity}</span>
<button onclick="productsManager.updateCartQuantity(${item.product_id}, ${item.quantity + 1})">+</button>
</div>
<div class="cart-item-total">$${(item.unit_price * item.quantity).toFixed(2)}</div>
<button class="remove-btn" onclick="productsManager.removeFromCart(${item.product_id})">Remove</button>
</div>
</div>
`).join('');
cartTotal.textContent = total.toFixed(2);
}
showCheckout() {
if (this.cart.length === 0) {
this.showError('Your cart is empty');
return;
}
this.renderCheckoutSummary();
document.getElementById('cart-modal').style.display = 'none';
document.getElementById('checkout-modal').style.display = 'block';
}
renderCheckoutSummary() {
const checkoutItems = document.getElementById('checkout-items');
const checkoutTotal = document.getElementById('checkout-total');
const total = this.cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
checkoutItems.innerHTML = this.cart.map(item => `
<div class="checkout-item">
<span>${item.product.name} x ${item.quantity}</span>
<span>$${(item.unit_price * item.quantity).toFixed(2)}</span>
</div>
`).join('');
checkoutTotal.textContent = total.toFixed(2);
}
async processCheckout(event) {
event.preventDefault();
const formData = new FormData(event.target);
const orderData = {
items: this.cart.map(item => ({
product_id: item.product_id,
quantity: item.quantity
})),
shipping_address: formData.get('shipping-address'),
billing_address: formData.get('billing-address'),
payment_method: formData.get('payment-method')
};
try {
const response = await this.api.post('/orders/', orderData);
// Clear cart
this.clearCart();
// Close modal
document.getElementById('checkout-modal').style.display = 'none';
// Show success message
this.showSuccessMessage(`Order placed successfully! Order #${response.order_number}`);
// Refresh products to update stock
this.loadProducts(this.currentPage);
} catch (error) {
console.error('Checkout failed:', error);
this.showError('Failed to place order. Please try again.');
}
}
showSuccessMessage(message) {
// Create and show success notification
const notification = document.createElement('div');
notification.className = 'notification success';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
showError(message) {
// Create and show error notification
const notification = document.createElement('div');
notification.className = 'notification error';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.productsManager = new ProductsManager(api);
});JavaScriptThis complete e-commerce example demonstrates:
- Full-Stack Integration: Complete JWT authentication with frontend/backend
- Production Security: CSRF protection, XSS prevention, rate limiting
- Real-World Features: Shopping cart, orders, user management, admin functions
- Scalable Architecture: Modular design with proper separation of concerns
- Error Handling: Comprehensive error handling and user feedback
- Performance: Pagination, caching, optimized queries
The example shows how all the JWT concepts from this guide work together in a production application.
17. Troubleshooting and FAQ
This chapter addresses common issues, debugging techniques, and frequently asked questions about JWT implementation.
Common JWT Issues and Solutions
Token Verification Failures
graph TD
A[Token Verification Failed] --> B{Check Error Type}
B --> C[Invalid Signature]
B --> D[Token Expired]
B --> E[Invalid Format]
B --> F[Algorithm Mismatch]
C --> C1[Verify secret key]
C --> C2[Check key rotation]
D --> D1[Check token lifetime]
D --> D2[Implement refresh]
E --> E1[Validate JWT structure]
E --> E2[Check encoding]
F --> F1[Verify algorithm settings]
F --> F2[Check JWKS configuration]Debug Token Issues:
# JWT Debugging Utility
import jwt
import json
from datetime import datetime
class JWTDebugger:
def __init__(self):
self.common_issues = {
'ExpiredSignatureError': 'Token has expired',
'InvalidSignatureError': 'Invalid signature - check secret key',
'DecodeError': 'Invalid token format',
'InvalidTokenError': 'Token structure is invalid',
'InvalidKeyError': 'Invalid key provided',
'InvalidAlgorithmError': 'Algorithm not allowed'
}
def debug_token(self, token: str, secret_key: str = None, verify: bool = False) -> dict:
"""Debug JWT token and provide diagnostic information"""
result = {
'valid': False,
'issues': [],
'header': None,
'payload': None,
'signature_valid': False
}
try:
# Try to decode header
result['header'] = jwt.get_unverified_header(token)
print(f"Header: {json.dumps(result['header'], indent=2)}")
except Exception as e:
result['issues'].append(f"Header decode failed: {str(e)}")
return result
try:
# Try to decode payload without verification
result['payload'] = jwt.decode(token, options={"verify_signature": False})
print(f"Payload: {json.dumps(result['payload'], indent=2)}")
# Check expiration
exp = result['payload'].get('exp')
if exp:
exp_time = datetime.fromtimestamp(exp)
current_time = datetime.utcnow()
if exp_time < current_time:
result['issues'].append(f"Token expired at {exp_time}")
else:
print(f"Token expires at: {exp_time}")
except Exception as e:
result['issues'].append(f"Payload decode failed: {str(e)}")
# Try signature verification if secret provided
if secret_key and verify:
try:
algorithm = result['header'].get('alg', 'HS256')
jwt.decode(token, secret_key, algorithms=[algorithm])
result['signature_valid'] = True
result['valid'] = True
print("✓ Signature is valid")
except Exception as e:
error_type = type(e).__name__
error_msg = self.common_issues.get(error_type, str(e))
result['issues'].append(f"Signature verification failed: {error_msg}")
print(f"✗ {error_msg}")
return result
def validate_token_structure(self, token: str) -> bool:
"""Validate basic JWT structure"""
parts = token.split('.')
if len(parts) != 3:
print(f"✗ Invalid JWT structure: expected 3 parts, got {len(parts)}")
return False
print("✓ JWT has correct structure (3 parts)")
# Check if parts are base64 encoded
try:
for i, part in enumerate(parts[:2]): # Don't decode signature
import base64
# Add padding if needed
padded = part + '=' * (4 - len(part) % 4)
base64.urlsafe_b64decode(padded)
print("✓ All parts are properly base64 encoded")
return True
except Exception as e:
print(f"✗ Base64 decode failed: {e}")
return False
def compare_tokens(self, token1: str, token2: str) -> dict:
"""Compare two tokens to identify differences"""
comparison = {
'headers_match': False,
'payloads_match': False,
'signatures_match': False,
'differences': []
}
try:
header1 = jwt.get_unverified_header(token1)
header2 = jwt.get_unverified_header(token2)
if header1 == header2:
comparison['headers_match'] = True
else:
comparison['differences'].append("Headers differ")
print(f"Header 1: {header1}")
print(f"Header 2: {header2}")
except Exception as e:
comparison['differences'].append(f"Header comparison failed: {e}")
try:
payload1 = jwt.decode(token1, options={"verify_signature": False})
payload2 = jwt.decode(token2, options={"verify_signature": False})
if payload1 == payload2:
comparison['payloads_match'] = True
else:
comparison['differences'].append("Payloads differ")
# Find specific differences
for key in set(payload1.keys()) | set(payload2.keys()):
if payload1.get(key) != payload2.get(key):
print(f"Payload difference in '{key}': {payload1.get(key)} vs {payload2.get(key)}")
except Exception as e:
comparison['differences'].append(f"Payload comparison failed: {e}")
# Compare signatures
sig1 = token1.split('.')[2]
sig2 = token2.split('.')[2]
comparison['signatures_match'] = sig1 == sig2
return comparison
# Usage example
debugger = JWTDebugger()
# Debug a problematic token
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
secret = "your-secret-key"
print("=== JWT Debug Report ===")
result = debugger.debug_token(token, secret, verify=True)
if result['issues']:
print("\n=== Issues Found ===")
for issue in result['issues']:
print(f"• {issue}")PythonFrontend Token Management Issues
/**
* JWT Frontend Debugging
*/
class JWTDebugger {
constructor() {
this.debugMode = localStorage.getItem('jwt_debug') === 'true';
}
enableDebugMode() {
localStorage.setItem('jwt_debug', 'true');
this.debugMode = true;
console.log('JWT Debug mode enabled');
}
disableDebugMode() {
localStorage.setItem('jwt_debug', 'false');
this.debugMode = false;
console.log('JWT Debug mode disabled');
}
logTokenInfo(token) {
if (!this.debugMode) return;
try {
// Decode token payload without verification
const payload = this.decodeTokenPayload(token);
console.group('JWT Token Info');
console.log('Token:', token.substring(0, 50) + '...');
console.log('Payload:', payload);
if (payload.exp) {
const expDate = new Date(payload.exp * 1000);
const now = new Date();
const timeUntilExpiry = expDate - now;
console.log('Expires:', expDate.toISOString());
console.log('Time until expiry:', Math.round(timeUntilExpiry / 1000), 'seconds');
if (timeUntilExpiry < 0) {
console.warn('⚠️ Token is expired!');
} else if (timeUntilExpiry < 300000) { // 5 minutes
console.warn('⚠️ Token expires soon!');
}
}
console.groupEnd();
} catch (error) {
console.error('Failed to decode token:', error);
}
}
decodeTokenPayload(token) {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}
const payload = parts[1];
// Add padding if needed
const padded = payload + '='.repeat((4 - payload.length % 4) % 4);
try {
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch (error) {
throw new Error('Failed to decode token payload');
}
}
checkTokenStorage() {
if (!this.debugMode) return;
console.group('Token Storage Check');
const locations = [
{ name: 'localStorage.access_token', value: localStorage.getItem('access_token') },
{ name: 'localStorage.refresh_token', value: localStorage.getItem('refresh_token') },
{ name: 'sessionStorage.access_token', value: sessionStorage.getItem('access_token') },
{ name: 'document.cookie', value: document.cookie }
];
locations.forEach(location => {
if (location.value) {
console.log(`✓ Found in ${location.name}:`, location.value.substring(0, 50) + '...');
if (location.name.includes('token') && location.value.startsWith('eyJ')) {
this.logTokenInfo(location.value);
}
} else {
console.log(`✗ Not found in ${location.name}`);
}
});
console.groupEnd();
}
validateApiResponse(response, requestUrl) {
if (!this.debugMode) return;
console.group(`API Response: ${requestUrl}`);
console.log('Status:', response.status);
console.log('Headers:', Object.fromEntries(response.headers.entries()));
if (response.status === 401) {
console.warn('⚠️ Unauthorized - token may be invalid or expired');
this.checkTokenStorage();
}
console.groupEnd();
}
simulateTokenExpiry() {
// For testing purposes - create an expired token
const header = { typ: 'JWT', alg: 'HS256' };
const payload = {
sub: 'test-user',
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
iat: Math.floor(Date.now() / 1000) - 7200 // Issued 2 hours ago
};
const encodedHeader = btoa(JSON.stringify(header)).replace(/=/g, '');
const encodedPayload = btoa(JSON.stringify(payload)).replace(/=/g, '');
const fakeToken = `${encodedHeader}.${encodedPayload}.fake-signature`;
console.log('Simulated expired token:', fakeToken);
return fakeToken;
}
}
// Enhanced API client with debugging
class DebugAwareAPIClient extends APIClient {
constructor(baseUrl) {
super(baseUrl);
this.debugger = new JWTDebugger();
}
async request(endpoint, options = {}) {
const token = this.getStoredToken();
if (token) {
this.debugger.logTokenInfo(token);
}
try {
const response = await super.request(endpoint, options);
this.debugger.validateApiResponse(response, endpoint);
return response;
} catch (error) {
if (error.status === 401) {
console.warn('Authentication failed - checking token status');
this.debugger.checkTokenStorage();
}
throw error;
}
}
}JavaScriptPerformance Optimization
Token Size Optimization
# Minimize JWT payload size
class OptimizedJWTHandler:
def __init__(self):
# Use short claim names
self.claim_mappings = {
'user_id': 'uid',
'username': 'usr',
'role': 'r',
'permissions': 'prm',
'department': 'dept',
'organization': 'org'
}
self.reverse_mappings = {v: k for k, v in self.claim_mappings.items()}
def create_optimized_token(self, user_data: dict) -> str:
"""Create token with optimized payload"""
# Map to shorter claim names
optimized_payload = {}
for key, value in user_data.items():
short_key = self.claim_mappings.get(key, key)
optimized_payload[short_key] = value
# Use shorter algorithms and remove unnecessary claims
optimized_payload.update({
'exp': datetime.utcnow() + timedelta(minutes=15), # Short expiry
'iat': datetime.utcnow(),
'iss': 'api', # Short issuer
'aud': 'app' # Short audience
})
return jwt.encode(optimized_payload, self.secret_key, algorithm='HS256')
def decode_optimized_token(self, token: str) -> dict:
"""Decode and expand claims"""
payload = jwt.decode(token, self.secret_key, algorithms=['HS256'])
# Map back to full claim names
expanded_payload = {}
for key, value in payload.items():
full_key = self.reverse_mappings.get(key, key)
expanded_payload[full_key] = value
return expanded_payloadJavaScriptCaching Strategies
# Redis-based token caching
import aioredis
import pickle
class TokenCache:
def __init__(self, redis_url: str):
self.redis_url = redis_url
self.redis_client = None
self.default_ttl = 900 # 15 minutes
async def initialize(self):
self.redis_client = await aioredis.from_url(self.redis_url)
async def cache_user_data(self, user_id: str, user_data: dict, ttl: int = None):
"""Cache user data to avoid database lookups"""
key = f"user_data:{user_id}"
ttl = ttl or self.default_ttl
serialized_data = pickle.dumps(user_data)
await self.redis_client.setex(key, ttl, serialized_data)
async def get_cached_user_data(self, user_id: str) -> dict:
"""Get cached user data"""
key = f"user_data:{user_id}"
cached_data = await self.redis_client.get(key)
if cached_data:
return pickle.loads(cached_data)
return None
async def invalidate_user_cache(self, user_id: str):
"""Invalidate user cache when data changes"""
key = f"user_data:{user_id}"
await self.redis_client.delete(key)
# Enhanced authentication with caching
class CachedAuthHandler:
def __init__(self, jwt_handler, token_cache, db_session):
self.jwt_handler = jwt_handler
self.cache = token_cache
self.db = db_session
async def authenticate_user(self, token: str) -> dict:
"""Authenticate user with caching"""
# Verify token
payload = await self.jwt_handler.verify_token(token)
if not payload:
return None
user_id = payload.get('sub')
# Try cache first
user_data = await self.cache.get_cached_user_data(user_id)
if not user_data:
# Cache miss - query database
user = self.db.query(User).filter(User.id == user_id).first()
if not user:
return None
user_data = {
'id': user.id,
'username': user.username,
'email': user.email,
'role': user.role,
'permissions': [p.name for p in user.permissions],
'is_active': user.is_active
}
# Cache for future requests
await self.cache.cache_user_data(user_id, user_data)
return user_dataPythonFAQ Section
Q: Should I store JWTs in localStorage or cookies?
A: It depends on your security requirements:
localStorage:
- ✅ Easy to implement
- ✅ Works well with SPAs
- ❌ Vulnerable to XSS attacks
- ❌ Doesn’t send automatically
HttpOnly Cookies:
- ✅ Protected from XSS
- ✅ Sent automatically
- ❌ Vulnerable to CSRF (need protection)
- ❌ More complex with CORS
Recommendation: Use HttpOnly cookies with CSRF protection for best security.
Q: How long should JWT tokens last?
A: Token lifetime depends on your security requirements:
- Access tokens: 15 minutes to 1 hour
- Refresh tokens: 1 week to 1 month
- Remember me tokens: Up to 1 year
Shorter lifetimes = better security but more refresh requests.
Q: Can I revoke JWTs?
A: JWTs are stateless, but you can implement revocation:
- Blacklist tokens in database/cache
- Short token lifetimes with refresh mechanism
- Token versioning in user records
- Session tracking with database state
Q: Should I include sensitive data in JWT payload?
A: No! JWT payloads are only base64 encoded, not encrypted:
Don’t include:
- Passwords or password hashes
- Credit card numbers
- Social security numbers
- Private API keys
Safe to include:
- User ID
- Username
- Role/permissions
- Non-sensitive metadata
Q: How do I handle JWT in microservices?
A: Use shared verification:
- Shared secret for HMAC algorithms
- Public key for RS256/ES256 algorithms
- JWKS endpoint for key distribution
- API gateway for centralized auth
- Service mesh for automatic token forwarding
Q: What’s the difference between JWT, JWS, and JWE?
A:
- JWT: JSON Web Token (the standard)
- JWS: JSON Web Signature (signed tokens)
- JWE: JSON Web Encryption (encrypted tokens)
Most “JWT” implementations are actually JWS. Use JWE for sensitive data.
Q: How do I debug “Invalid signature” errors?
A: Common causes:
- Wrong secret key – verify environment variables
- Algorithm mismatch – check HS256 vs RS256
- Key rotation – ensure old keys still work
- Clock skew – synchronize server times
- Character encoding – check UTF-8 encoding
Q: Can I use JWT for session management?
A: Yes, but consider:
Pros:
- Stateless
- Scalable
- Cross-domain friendly
Cons:
- Cannot revoke easily
- Larger than session IDs
- No server-side session data
Use hybrid approach: JWT for authentication, sessions for state.
Q: How do I implement “Remember Me” with JWT?
A: Use two-token system:
- Short-lived access token (15 minutes)
- Long-lived refresh token (30 days)
- Automatic refresh before expiry
- Refresh token rotation for security
Q: What happens if my JWT secret is compromised?
A: Immediate actions:
- Rotate the secret immediately
- Invalidate all existing tokens
- Force user re-authentication
- Audit access logs
- Review security practices
Prevention: Use strong secrets, regular rotation, secure storage.
Migration Strategies
Moving from Session-based to JWT
# Gradual migration strategy
class HybridAuthHandler:
def __init__(self, session_handler, jwt_handler):
self.session_handler = session_handler
self.jwt_handler = jwt_handler
async def authenticate(self, request):
"""Try JWT first, fall back to sessions"""
# Try JWT authentication
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
user = await self.jwt_handler.verify_token(token)
if user:
return user
# Fall back to session authentication
session_id = request.cookies.get('session_id')
if session_id:
return await self.session_handler.get_user_from_session(session_id)
return None
async def migrate_user_to_jwt(self, user_id: str):
"""Migrate specific user to JWT"""
# Mark user as JWT-enabled in database
await self.mark_user_jwt_enabled(user_id)
# Invalidate existing sessions
await self.session_handler.invalidate_user_sessions(user_id)
return TruePythonThis comprehensive troubleshooting guide helps developers identify, debug, and resolve common JWT implementation issues while providing best practices for optimization and migration.
18. Production Deployment and Monitoring
This final chapter covers deploying JWT-based applications to production environments with proper monitoring, logging, and maintenance strategies.
Production Deployment Checklist
graph TB
A[Production Deployment] --> B[Security Configuration]
A --> C[Performance Optimization]
A --> D[Monitoring Setup]
A --> E[Backup Strategies]
B --> B1[Environment Variables]
B --> B2[HTTPS Configuration]
B --> B3[Secret Management]
B --> B4[Security Headers]
C --> C1[Caching Strategy]
C --> C2[Database Optimization]
C --> C3[Load Balancing]
C --> C4[CDN Configuration]
D --> D1[Application Metrics]
D --> D2[Security Monitoring]
D --> D3[Error Tracking]
D --> D4[Performance Monitoring]
E --> E1[Database Backups]
E --> E2[Configuration Backups]
E --> E3[Disaster Recovery]
E --> E4[Key Backup]Docker Production Setup
Dockerfile for FastAPI Backend:
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH="/app" \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Create non-root user
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --gid 1001 --no-create-home appuser
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Set work directory
WORKDIR /app
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Change ownership to non-root user
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
# Expose port
EXPOSE 8000
# Start application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]DockerfileProduction Docker Compose:
version: '3.8'
services:
# FastAPI Backend
api:
build:
context: ./backend
dockerfile: Dockerfile
container_name: jwt_api
restart: unless-stopped
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@postgres:5432/jwt_app
- REDIS_URL=redis://redis:6379
- SECRET_KEY=${SECRET_KEY}
- ENVIRONMENT=production
- LOG_LEVEL=INFO
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- jwt_network
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: jwt_postgres
restart: unless-stopped
environment:
POSTGRES_DB: jwt_app
POSTGRES_USER: user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- jwt_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d jwt_app"]
interval: 30s
timeout: 10s
retries: 3
# Redis Cache
redis:
image: redis:7-alpine
container_name: jwt_redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis_data:/data
networks:
- jwt_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: jwt_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- ./frontend:/usr/share/nginx/html
- ./logs/nginx:/var/log/nginx
depends_on:
- api
networks:
- jwt_network
# Monitoring with Prometheus
prometheus:
image: prom/prometheus:latest
container_name: jwt_prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
networks:
- jwt_network
# Grafana Dashboard
grafana:
image: grafana/grafana:latest
container_name: jwt_grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana:/etc/grafana/provisioning
networks:
- jwt_network
volumes:
postgres_data:
redis_data:
prometheus_data:
grafana_data:
networks:
jwt_network:
driver: bridgeYAMLNginx Production Configuration
nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 10M;
# Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Hide nginx version
server_tokens off;
# Upstream backend
upstream api_backend {
server api:8000;
keepalive 32;
}
# HTTP redirect to HTTPS
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# API routes
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Auth endpoints with stricter rate limiting
location /api/auth/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static files
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}NginxApplication Monitoring
Prometheus Configuration (monitoring/prometheus.yml):
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: 'jwt-api'
static_configs:
- targets: ['api:8000']
metrics_path: '/metrics'
scrape_interval: 10s
- job_name: 'nginx'
static_configs:
- targets: ['nginx:9113']
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']YAMLApplication Metrics (app/monitoring/metrics.py):
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from functools import wraps
import time
# Metrics
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_DURATION = Histogram(
'http_request_duration_seconds',
'HTTP request duration',
['method', 'endpoint']
)
ACTIVE_SESSIONS = Gauge(
'active_jwt_sessions',
'Number of active JWT sessions'
)
TOKEN_OPERATIONS = Counter(
'jwt_token_operations_total',
'JWT token operations',
['operation', 'status']
)
LOGIN_ATTEMPTS = Counter(
'login_attempts_total',
'Login attempts',
['status', 'ip']
)
FAILED_AUTH = Counter(
'failed_authentication_total',
'Failed authentication attempts',
['reason']
)
def track_request_metrics(func):
"""Decorator to track request metrics"""
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = await func(*args, **kwargs)
status = getattr(result, 'status_code', 200)
REQUEST_COUNT.labels(
method='POST', # You'd get this from request
endpoint=func.__name__,
status=status
).inc()
return result
except Exception as e:
REQUEST_COUNT.labels(
method='POST',
endpoint=func.__name__,
status=500
).inc()
raise
finally:
REQUEST_DURATION.labels(
method='POST',
endpoint=func.__name__
).observe(time.time() - start_time)
return wrapper
class JWTMetrics:
def __init__(self):
self.active_tokens = set()
def track_token_creation(self, jti: str, success: bool):
"""Track token creation"""
status = 'success' if success else 'failed'
TOKEN_OPERATIONS.labels(operation='create', status=status).inc()
if success:
self.active_tokens.add(jti)
ACTIVE_SESSIONS.set(len(self.active_tokens))
def track_token_verification(self, jti: str, success: bool):
"""Track token verification"""
status = 'success' if success else 'failed'
TOKEN_OPERATIONS.labels(operation='verify', status=status).inc()
def track_token_revocation(self, jti: str):
"""Track token revocation"""
TOKEN_OPERATIONS.labels(operation='revoke', status='success').inc()
self.active_tokens.discard(jti)
ACTIVE_SESSIONS.set(len(self.active_tokens))
def track_login_attempt(self, success: bool, ip: str):
"""Track login attempts"""
status = 'success' if success else 'failed'
LOGIN_ATTEMPTS.labels(status=status, ip=ip).inc()
def track_auth_failure(self, reason: str):
"""Track authentication failures"""
FAILED_AUTH.labels(reason=reason).inc()
# Global metrics instance
jwt_metrics = JWTMetrics()
# Start metrics server
def start_metrics_server(port: int = 8001):
start_http_server(port)PythonLogging Configuration
Structured Logging (app/logging_config.py):
import logging
import json
from datetime import datetime
from typing import Dict, Any
import structlog
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}
# Add extra fields
if hasattr(record, 'user_id'):
log_entry['user_id'] = record.user_id
if hasattr(record, 'request_id'):
log_entry['request_id'] = record.request_id
if hasattr(record, 'ip_address'):
log_entry['ip_address'] = record.ip_address
if hasattr(record, 'jwt_jti'):
log_entry['jwt_jti'] = record.jwt_jti
return json.dumps(log_entry)
def setup_logging(log_level: str = "INFO"):
"""Setup structured logging"""
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="ISO"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# Configure standard logging
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format='%(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('/app/logs/app.log')
]
)
# Set formatter
formatter = JSONFormatter()
for handler in logging.root.handlers:
handler.setFormatter(formatter)
# Security event logger
class SecurityLogger:
def __init__(self):
self.logger = structlog.get_logger("security")
def log_login_attempt(self, username: str, ip: str, success: bool, user_agent: str = None):
self.logger.info(
"login_attempt",
username=username,
ip_address=ip,
success=success,
user_agent=user_agent,
event_type="authentication"
)
def log_token_operation(self, operation: str, jti: str, user_id: str, ip: str):
self.logger.info(
"token_operation",
operation=operation,
jwt_jti=jti,
user_id=user_id,
ip_address=ip,
event_type="token"
)
def log_security_violation(self, violation_type: str, details: Dict[str, Any], ip: str):
self.logger.warning(
"security_violation",
violation_type=violation_type,
details=details,
ip_address=ip,
event_type="security"
)
def log_access_denied(self, user_id: str, resource: str, reason: str, ip: str):
self.logger.warning(
"access_denied",
user_id=user_id,
resource=resource,
reason=reason,
ip_address=ip,
event_type="authorization"
)
# Global security logger
security_logger = SecurityLogger()PythonEnvironment Configuration
.env.production:
# Application
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=INFO
# Database
DATABASE_URL=postgresql://user:password@postgres:5432/jwt_app
DB_PASSWORD=your-secure-db-password
# Redis
REDIS_URL=redis://redis:6379
REDIS_PASSWORD=your-secure-redis-password
# JWT Configuration
SECRET_KEY=your-super-secure-secret-key-min-32-chars
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=30
ALGORITHM=HS256
# Security
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
CORS_ALLOW_CREDENTIALS=true
HTTPS_ONLY=true
SECURE_COOKIES=true
# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600
# Monitoring
PROMETHEUS_ENABLED=true
METRICS_PORT=8001
# Email (for notifications)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
# External Services
OAUTH_GOOGLE_CLIENT_ID=your-google-client-id
OAUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
# Backup
BACKUP_ENABLED=true
BACKUP_S3_BUCKET=your-backup-bucket
AWS_ACCESS_KEY_ID=your-aws-key
AWS_SECRET_ACCESS_KEY=your-aws-secretINIBackup and Disaster Recovery
Backup Script (scripts/backup.sh):
#!/bin/bash
# Configuration
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# Database backup
echo "Starting database backup..."
docker exec jwt_postgres pg_dump -U user -d jwt_app | gzip > "$BACKUP_DIR/db_backup_$DATE.sql.gz"
# Redis backup
echo "Starting Redis backup..."
docker exec jwt_redis redis-cli --rdb /data/backup.rdb
docker cp jwt_redis:/data/backup.rdb "$BACKUP_DIR/redis_backup_$DATE.rdb"
# Configuration backup
echo "Backing up configuration..."
tar -czf "$BACKUP_DIR/config_backup_$DATE.tar.gz" \
.env.production \
nginx/ \
monitoring/ \
docker-compose.yml
# Upload to S3 (if configured)
if [ "$BACKUP_S3_BUCKET" != "" ]; then
echo "Uploading to S3..."
aws s3 cp "$BACKUP_DIR/" "s3://$BACKUP_S3_BUCKET/backups/" --recursive --exclude "*" --include "*$DATE*"
fi
# Cleanup old backups
echo "Cleaning up old backups..."
find "$BACKUP_DIR" -name "*.gz" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "*.rdb" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed successfully!"BashCrontab for automated backups:
# Add to crontab (crontab -e)
# Daily backup at 2 AM
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1
# Weekly full backup on Sunday at 1 AM
0 1 * * 0 /path/to/full_backup.sh >> /var/log/backup.log 2>&1BashHealth Checks and Monitoring
Advanced Health Check (app/health.py):
from fastapi import APIRouter, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth.token_blacklist import token_blacklist
import aioredis
import asyncio
from datetime import datetime
router = APIRouter()
@router.get("/health")
async def health_check():
"""Basic health check"""
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": "1.0.0"
}
@router.get("/health/detailed")
async def detailed_health_check(db: Session = Depends(get_db)):
"""Detailed health check with dependency status"""
health_status = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": "1.0.0",
"checks": {}
}
# Database check
try:
db.execute("SELECT 1")
health_status["checks"]["database"] = {"status": "healthy"}
except Exception as e:
health_status["checks"]["database"] = {"status": "unhealthy", "error": str(e)}
health_status["status"] = "unhealthy"
# Redis check
try:
redis_client = await aioredis.from_url("redis://redis:6379")
await redis_client.ping()
await redis_client.close()
health_status["checks"]["redis"] = {"status": "healthy"}
except Exception as e:
health_status["checks"]["redis"] = {"status": "unhealthy", "error": str(e)}
health_status["status"] = "unhealthy"
# Token blacklist check
try:
stats = await token_blacklist.get_blacklist_stats()
health_status["checks"]["token_blacklist"] = {
"status": "healthy",
"stats": stats
}
except Exception as e:
health_status["checks"]["token_blacklist"] = {"status": "unhealthy", "error": str(e)}
return health_status
@router.get("/metrics")
async def metrics_endpoint():
"""Expose metrics for Prometheus"""
# This would be handled by prometheus_client
passPythonThis production deployment guide provides a complete setup for deploying JWT-based applications with proper security, monitoring, and maintenance procedures. The configuration ensures high availability, security, and observability in production environments.
Conclusion
This comprehensive guide has taken you through every aspect of JWT implementation, from basic concepts to production-ready applications. You now have the knowledge and tools to:
- Understand JWT fundamentals and choose the right token types
- Implement secure authentication with FastAPI and JavaScript
- Handle advanced security scenarios including JWE and OAuth 2.0
- Manage tokens effectively with rotation and revocation strategies
- Deploy to production with proper monitoring and security measures
Remember that security is an ongoing process. Keep your dependencies updated, monitor your applications, and stay informed about the latest security best practices.
Key Takeaways
- Security First: Always prioritize security over convenience
- Token Lifecycle: Implement proper token creation, validation, and revocation
- Error Handling: Provide comprehensive error handling and logging
- Monitoring: Monitor authentication events and security metrics
- Testing: Thoroughly test all authentication flows and edge cases
Next Steps
- Implement the concepts in your own projects
- Customize the examples to fit your specific requirements
- Stay Updated with JWT and security best practices
- Contribute to the community by sharing your experiences
Happy coding with secure JWT implementations!
19. Testing and Quality Assurance
Comprehensive testing is essential for JWT authentication systems. This chapter covers testing strategies, frameworks, and best practices for ensuring your authentication system is robust and secure.
Testing Strategy Overview
graph TB
A[Testing Strategy] --> B[Unit Tests]
A --> C[Integration Tests]
A --> D[End-to-End Tests]
A --> E[Security Tests]
A --> F[Performance Tests]
B --> B1[JWT Handler Tests]
B --> B2[Password Hashing Tests]
B --> B3[Token Validation Tests]
B --> B4[Utility Function Tests]
C --> C1[API Endpoint Tests]
C --> C2[Database Integration Tests]
C --> C3[Authentication Flow Tests]
C --> C4[Authorization Tests]
D --> D1[User Journey Tests]
D --> D2[Frontend Integration Tests]
D --> D3[Cross-Browser Tests]
D --> D4[Mobile Responsiveness Tests]
E --> E1[Penetration Tests]
E --> E2[Vulnerability Scans]
E --> E3[Token Security Tests]
E --> E4[OWASP Testing]
F --> F1[Load Tests]
F --> F2[Stress Tests]
F --> F3[Token Performance Tests]
F --> F4[Scalability Tests]Unit Testing with pytest
JWT Handler Tests
Create backend/tests/test_jwt_handler.py:
import pytest
from datetime import datetime, timedelta
from app.auth.jwt_handler import JWTHandler
from app.auth.jwe_handler import JWEHandler
from freezegun import freeze_time
import jwt
class TestJWTHandler:
@pytest.fixture
def jwt_handler(self):
"""Create JWT handler instance for testing"""
return JWTHandler()
@pytest.fixture
def sample_payload(self):
"""Sample token payload for testing"""
return {
"sub": "user123",
"email": "test@example.com",
"role": "user",
"permissions": ["read", "write"]
}
def test_create_access_token(self, jwt_handler, sample_payload):
"""Test access token creation"""
token = jwt_handler.create_access_token(sample_payload)
assert token is not None
assert isinstance(token, str)
assert len(token.split('.')) == 3 # JWT has 3 parts
# Verify token can be decoded
decoded = jwt_handler.verify_token(token)
assert decoded is not None
assert decoded["sub"] == sample_payload["sub"]
assert decoded["email"] == sample_payload["email"]
assert "exp" in decoded
assert "iat" in decoded
def test_create_refresh_token(self, jwt_handler, sample_payload):
"""Test refresh token creation"""
token = jwt_handler.create_refresh_token(sample_payload)
assert token is not None
decoded = jwt_handler.verify_token(token)
assert decoded is not None
assert decoded["type"] == "refresh"
# Refresh token should have longer expiry
exp_time = datetime.fromtimestamp(decoded["exp"])
iat_time = datetime.fromtimestamp(decoded["iat"])
duration = exp_time - iat_time
assert duration.days >= 7 # At least 7 days
def test_verify_valid_token(self, jwt_handler, sample_payload):
"""Test verification of valid token"""
token = jwt_handler.create_access_token(sample_payload)
decoded = jwt_handler.verify_token(token)
assert decoded is not None
assert decoded["sub"] == sample_payload["sub"]
assert decoded["email"] == sample_payload["email"]
def test_verify_expired_token(self, jwt_handler, sample_payload):
"""Test verification of expired token"""
# Create token that expires immediately
with freeze_time("2023-01-01 12:00:00"):
token = jwt_handler.create_access_token(sample_payload)
# Try to verify it after expiration
with freeze_time("2023-01-01 13:00:00"):
decoded = jwt_handler.verify_token(token)
assert decoded is None
def test_verify_invalid_token(self, jwt_handler):
"""Test verification of invalid token"""
invalid_tokens = [
"invalid.token.here",
"not.a.jwt",
"",
None,
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature"
]
for invalid_token in invalid_tokens:
decoded = jwt_handler.verify_token(invalid_token)
assert decoded is None
def test_password_hashing(self, jwt_handler):
"""Test password hashing and verification"""
password = "test_password_123"
# Hash password
hashed = jwt_handler.hash_password(password)
assert hashed is not None
assert hashed != password
assert len(hashed) > 20 # Bcrypt hashes are long
# Verify correct password
assert jwt_handler.verify_password(password, hashed) is True
# Verify incorrect password
assert jwt_handler.verify_password("wrong_password", hashed) is False
def test_refresh_access_token(self, jwt_handler, sample_payload):
"""Test access token refresh"""
# Create refresh token
refresh_token = jwt_handler.create_refresh_token(sample_payload)
# Refresh access token
new_access_token = jwt_handler.refresh_access_token(refresh_token)
assert new_access_token is not None
# Verify new token
decoded = jwt_handler.verify_token(new_access_token)
assert decoded is not None
assert decoded["sub"] == sample_payload["sub"]
assert decoded["type"] == "access"
def test_refresh_with_invalid_token(self, jwt_handler):
"""Test refresh with invalid refresh token"""
invalid_tokens = [
"invalid_token",
None,
jwt_handler.create_access_token({"sub": "test"}) # Access token, not refresh
]
for invalid_token in invalid_tokens:
result = jwt_handler.refresh_access_token(invalid_token)
assert result is None
def test_token_claims_validation(self, jwt_handler, sample_payload):
"""Test validation of token claims"""
token = jwt_handler.create_access_token(sample_payload)
decoded = jwt_handler.verify_token(token)
# Check required claims
assert "sub" in decoded
assert "exp" in decoded
assert "iat" in decoded
assert "type" in decoded
# Check expiration is in the future
exp_time = datetime.fromtimestamp(decoded["exp"])
assert exp_time > datetime.utcnow()
# Check issued at is in the past
iat_time = datetime.fromtimestamp(decoded["iat"])
assert iat_time <= datetime.utcnow()
class TestJWEHandler:
@pytest.fixture
def jwe_handler(self):
"""Create JWE handler instance for testing"""
return JWEHandler()
@pytest.fixture
def sample_data(self):
"""Sample data for encryption testing"""
return {
"user_id": "123",
"sensitive_data": "confidential information",
"timestamp": "2023-01-01T00:00:00Z"
}
def test_encrypt_decrypt_data(self, jwe_handler, sample_data):
"""Test data encryption and decryption"""
# Encrypt data
encrypted = jwe_handler.encrypt_data(sample_data)
assert encrypted is not None
assert encrypted != sample_data
# Decrypt data
decrypted = jwe_handler.decrypt_data(encrypted)
assert decrypted == sample_data
def test_create_verify_jwe_token(self, jwe_handler, sample_data):
"""Test JWE token creation and verification"""
# Create JWE token
token = jwe_handler.create_jwe_token(sample_data)
assert token is not None
assert isinstance(token, str)
assert len(token.split('.')) == 5 # JWE has 5 parts
# Verify JWE token
decrypted = jwe_handler.verify_jwe_token(token)
assert decrypted is not None
assert decrypted["user_id"] == sample_data["user_id"]
def test_invalid_jwe_token(self, jwe_handler):
"""Test handling of invalid JWE tokens"""
invalid_tokens = [
"invalid.jwe.token.here.invalid",
"not.a.jwe",
"",
None
]
for invalid_token in invalid_tokens:
result = jwe_handler.verify_jwe_token(invalid_token)
assert result is NonePythonAuthentication API Tests
Create backend/tests/test_auth_api.py:
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database.database import get_db, Base
import tempfile
import os
# Create temporary database for testing
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
class TestAuthenticationAPI:
@pytest.fixture(scope="class")
def client(self):
"""Create test client"""
return TestClient(app)
@pytest.fixture(autouse=True)
def setup_database(self):
"""Setup clean database for each test"""
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
def test_register_user_success(self, client):
"""Test successful user registration"""
user_data = {
"email": "test@example.com",
"password": "password123",
"full_name": "Test User"
}
response = client.post("/auth/register", json=user_data)
assert response.status_code == 201
data = response.json()
assert data["email"] == user_data["email"]
assert data["full_name"] == user_data["full_name"]
assert "id" in data
assert "password" not in data # Password should not be returned
def test_register_user_duplicate_email(self, client):
"""Test registration with duplicate email"""
user_data = {
"email": "duplicate@example.com",
"password": "password123",
"full_name": "First User"
}
# First registration should succeed
response1 = client.post("/auth/register", json=user_data)
assert response1.status_code == 201
# Second registration with same email should fail
response2 = client.post("/auth/register", json=user_data)
assert response2.status_code == 400
assert "already registered" in response2.json()["detail"].lower()
def test_register_user_invalid_email(self, client):
"""Test registration with invalid email"""
user_data = {
"email": "invalid-email",
"password": "password123",
"full_name": "Test User"
}
response = client.post("/auth/register", json=user_data)
assert response.status_code == 422 # Validation error
def test_register_user_weak_password(self, client):
"""Test registration with weak password"""
user_data = {
"email": "test@example.com",
"password": "123", # Too short
"full_name": "Test User"
}
# Note: This test assumes password validation is implemented
# You might need to add password strength validation
response = client.post("/auth/register", json=user_data)
# Should either succeed (if no validation) or fail with appropriate error
assert response.status_code in [201, 400, 422]
def test_login_success(self, client):
"""Test successful login"""
# First register a user
user_data = {
"email": "login@example.com",
"password": "password123",
"full_name": "Login User"
}
client.post("/auth/register", json=user_data)
# Then login
login_data = {
"username": user_data["email"],
"password": user_data["password"]
}
response = client.post("/auth/login", data=login_data)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
# Verify token is valid by accessing protected endpoint
headers = {"Authorization": f"Bearer {data['access_token']}"}
profile_response = client.get("/users/profile", headers=headers)
assert profile_response.status_code == 200
def test_login_invalid_credentials(self, client):
"""Test login with invalid credentials"""
# Register a user first
user_data = {
"email": "test@example.com",
"password": "correct_password",
"full_name": "Test User"
}
client.post("/auth/register", json=user_data)
# Try to login with wrong password
login_data = {
"username": user_data["email"],
"password": "wrong_password"
}
response = client.post("/auth/login", data=login_data)
assert response.status_code == 401
assert "invalid" in response.json()["detail"].lower()
def test_login_nonexistent_user(self, client):
"""Test login with nonexistent user"""
login_data = {
"username": "nonexistent@example.com",
"password": "password123"
}
response = client.post("/auth/login", data=login_data)
assert response.status_code == 401
def test_protected_route_with_valid_token(self, client):
"""Test accessing protected route with valid token"""
# Register and login to get token
user_data = {
"email": "protected@example.com",
"password": "password123",
"full_name": "Protected User"
}
client.post("/auth/register", json=user_data)
login_response = client.post("/auth/login", data={
"username": user_data["email"],
"password": user_data["password"]
})
token = login_response.json()["access_token"]
# Access protected route
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/users/profile", headers=headers)
assert response.status_code == 200
data = response.json()
assert data["email"] == user_data["email"]
assert data["full_name"] == user_data["full_name"]
def test_protected_route_without_token(self, client):
"""Test accessing protected route without token"""
response = client.get("/users/profile")
assert response.status_code == 403 # Forbidden
def test_protected_route_with_invalid_token(self, client):
"""Test accessing protected route with invalid token"""
headers = {"Authorization": "Bearer invalid_token_here"}
response = client.get("/users/profile", headers=headers)
assert response.status_code == 401 # Unauthorized
def test_refresh_token_success(self, client):
"""Test successful token refresh"""
# Register and login to get tokens
user_data = {
"email": "refresh@example.com",
"password": "password123",
"full_name": "Refresh User"
}
client.post("/auth/register", json=user_data)
login_response = client.post("/auth/login", data={
"username": user_data["email"],
"password": user_data["password"]
})
refresh_token = login_response.json()["refresh_token"]
# Refresh access token
refresh_data = {"refresh_token": refresh_token}
response = client.post("/auth/refresh", json=refresh_data)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
# Verify new token works
headers = {"Authorization": f"Bearer {data['access_token']}"}
profile_response = client.get("/users/profile", headers=headers)
assert profile_response.status_code == 200
def test_refresh_token_invalid(self, client):
"""Test token refresh with invalid refresh token"""
refresh_data = {"refresh_token": "invalid_refresh_token"}
response = client.post("/auth/refresh", json=refresh_data)
assert response.status_code == 401
def test_logout_success(self, client):
"""Test successful logout"""
# Register and login
user_data = {
"email": "logout@example.com",
"password": "password123",
"full_name": "Logout User"
}
client.post("/auth/register", json=user_data)
login_response = client.post("/auth/login", data={
"username": user_data["email"],
"password": user_data["password"]
})
token = login_response.json()["access_token"]
# Logout
headers = {"Authorization": f"Bearer {token}"}
response = client.post("/auth/logout", headers=headers)
assert response.status_code == 200PythonFrontend Testing with Jest
Setup Jest Testing Environment
Create frontend/package.json:
{
"name": "jwt-frontend-tests",
"version": "1.0.0",
"description": "Frontend tests for JWT authentication",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"@testing-library/jest-dom": "^5.16.0",
"fetch-mock": "^9.11.0"
},
"jest": {
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ["<rootDir>/tests/setup.js"],
"collectCoverageFrom": [
"js/**/*.js",
"!js/main.js"
]
}
}JSONCreate frontend/tests/setup.js:
// Jest setup file
import '@testing-library/jest-dom';
import fetchMock from 'fetch-mock';
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
// Mock sessionStorage
const sessionStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.sessionStorage = sessionStorageMock;
// Setup fetch mock
global.fetch = fetchMock.sandbox();
// Reset mocks before each test
beforeEach(() => {
fetchMock.reset();
localStorage.clear();
sessionStorage.clear();
});JavaScriptAPI Client Tests
Create frontend/tests/api.test.js:
import fetchMock from 'fetch-mock';
// Mock the API client (you'd import the actual implementation)
class ApiClient {
constructor(baseUrl = 'http://localhost:8000') {
this.baseUrl = baseUrl;
this.token = null;
}
getStoredToken() {
return localStorage.getItem('access_token');
}
storeToken(token) {
localStorage.setItem('access_token', token);
this.token = token;
}
removeToken() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
this.token = null;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Request failed');
}
return response.json();
}
async login(email, password) {
const formData = new FormData();
formData.append('username', email);
formData.append('password', password);
const response = await fetch(`${this.baseUrl}/auth/login`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Login failed');
}
const data = await response.json();
this.storeToken(data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return data;
}
}
describe('ApiClient', () => {
let api;
beforeEach(() => {
api = new ApiClient();
fetchMock.reset();
});
describe('Token Management', () => {
test('should store token in localStorage', () => {
const token = 'test.jwt.token';
api.storeToken(token);
expect(localStorage.setItem).toHaveBeenCalledWith('access_token', token);
expect(api.token).toBe(token);
});
test('should retrieve token from localStorage', () => {
const token = 'stored.jwt.token';
localStorage.getItem.mockReturnValue(token);
const retrieved = api.getStoredToken();
expect(retrieved).toBe(token);
expect(localStorage.getItem).toHaveBeenCalledWith('access_token');
});
test('should remove tokens from localStorage', () => {
api.removeToken();
expect(localStorage.removeItem).toHaveBeenCalledWith('access_token');
expect(localStorage.removeItem).toHaveBeenCalledWith('refresh_token');
expect(api.token).toBeNull();
});
});
describe('Login', () => {
test('should login successfully', async () => {
const mockResponse = {
access_token: 'mock.access.token',
refresh_token: 'mock.refresh.token',
token_type: 'bearer'
};
fetchMock.post('http://localhost:8000/auth/login', {
status: 200,
body: mockResponse
});
const result = await api.login('test@example.com', 'password123');
expect(result).toEqual(mockResponse);
expect(localStorage.setItem).toHaveBeenCalledWith('access_token', mockResponse.access_token);
expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', mockResponse.refresh_token);
});
test('should handle login failure', async () => {
fetchMock.post('http://localhost:8000/auth/login', {
status: 401,
body: { detail: 'Invalid credentials' }
});
await expect(api.login('test@example.com', 'wrongpassword'))
.rejects.toThrow('Invalid credentials');
});
});
describe('Request Method', () => {
test('should make successful request', async () => {
const mockData = { message: 'Success' };
fetchMock.get('http://localhost:8000/test', {
status: 200,
body: mockData
});
const result = await api.request('/test');
expect(result).toEqual(mockData);
});
test('should handle request errors', async () => {
fetchMock.get('http://localhost:8000/error', {
status: 400,
body: { detail: 'Bad request' }
});
await expect(api.request('/error'))
.rejects.toThrow('Bad request');
});
});
});JavaScriptEnd-to-End Testing with Playwright
Setup Playwright
Create e2e/package.json:
{
"name": "jwt-e2e-tests",
"version": "1.0.0",
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui"
},
"devDependencies": {
"@playwright/test": "^1.40.0"
}
}JSONCreate e2e/playwright.config.js:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5500', // Your frontend URL
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: [
{
command: 'cd ../backend && uvicorn app.main:app --reload --port 8000',
port: 8000,
reuseExistingServer: !process.env.CI,
},
{
command: 'cd ../frontend && python -m http.server 5500',
port: 5500,
reuseExistingServer: !process.env.CI,
},
],
});JavaScriptE2E Authentication Tests
Create e2e/tests/auth.spec.js:
import { test, expect } from '@playwright/test';
test.describe('JWT Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
// Start with clean state
await page.context().clearCookies();
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
await page.goto('/');
});
test('should display login form by default', async ({ page }) => {
await expect(page.locator('#login-section')).toBeVisible();
await expect(page.locator('#register-section')).toBeHidden();
await expect(page.locator('#dashboard-section')).toBeHidden();
});
test('should register new user successfully', async ({ page }) => {
// Click register navigation
await page.click('button:has-text("Register")');
await expect(page.locator('#register-section')).toBeVisible();
// Fill registration form
await page.fill('#register-name', 'Test User');
await page.fill('#register-email', 'test@example.com');
await page.fill('#register-password', 'password123');
// Submit form
await page.click('#register-form button[type="submit"]');
// Should show success message and redirect to login
await expect(page.locator('#status')).toContainText('Registration successful');
await expect(page.locator('#login-section')).toBeVisible();
});
test('should login successfully and show dashboard', async ({ page }) => {
// First register a user (you might want to use API for setup)
await page.click('button:has-text("Register")');
await page.fill('#register-name', 'Login User');
await page.fill('#register-email', 'login@example.com');
await page.fill('#register-password', 'password123');
await page.click('#register-form button[type="submit"]');
// Wait for redirect to login
await expect(page.locator('#login-section')).toBeVisible();
// Login
await page.fill('#login-email', 'login@example.com');
await page.fill('#login-password', 'password123');
await page.click('#login-form button[type="submit"]');
// Should show dashboard
await expect(page.locator('#dashboard-section')).toBeVisible();
await expect(page.locator('#user-info')).toContainText('login@example.com');
});
test('should handle login failure', async ({ page }) => {
await page.fill('#login-email', 'nonexistent@example.com');
await page.fill('#login-password', 'wrongpassword');
await page.click('#login-form button[type="submit"]');
// Should show error message
await expect(page.locator('#status')).toContainText('Login failed');
await expect(page.locator('#dashboard-section')).toBeHidden();
});
test('should logout successfully', async ({ page }) => {
// Login first (setup)
await page.click('button:has-text("Register")');
await page.fill('#register-name', 'Logout User');
await page.fill('#register-email', 'logout@example.com');
await page.fill('#register-password', 'password123');
await page.click('#register-form button[type="submit"]');
await page.fill('#login-email', 'logout@example.com');
await page.fill('#login-password', 'password123');
await page.click('#login-form button[type="submit"]');
await expect(page.locator('#dashboard-section')).toBeVisible();
// Logout
await page.click('#logout-btn');
// Should return to login
await expect(page.locator('#login-section')).toBeVisible();
await expect(page.locator('#dashboard-section')).toBeHidden();
});
test('should persist authentication across page refresh', async ({ page }) => {
// Login first
await page.click('button:has-text("Register")');
await page.fill('#register-name', 'Persist User');
await page.fill('#register-email', 'persist@example.com');
await page.fill('#register-password', 'password123');
await page.click('#register-form button[type="submit"]');
await page.fill('#login-email', 'persist@example.com');
await page.fill('#login-password', 'password123');
await page.click('#login-form button[type="submit"]');
await expect(page.locator('#dashboard-section')).toBeVisible();
// Refresh page
await page.reload();
// Should still be logged in
await expect(page.locator('#dashboard-section')).toBeVisible();
});
test('should handle token expiration gracefully', async ({ page }) => {
// This test would require mocking or using very short token expiry
// For demonstration, we'll simulate by clearing the token
// Login first
await page.click('button:has-text("Register")');
await page.fill('#register-name', 'Expire User');
await page.fill('#register-email', 'expire@example.com');
await page.fill('#register-password', 'password123');
await page.click('#register-form button[type="submit"]');
await page.fill('#login-email', 'expire@example.com');
await page.fill('#login-password', 'password123');
await page.click('#login-form button[type="submit"]');
await expect(page.locator('#dashboard-section')).toBeVisible();
// Simulate token expiration by clearing localStorage
await page.evaluate(() => {
localStorage.removeItem('access_token');
});
// Try to access protected data
await page.click('button:has-text("Load Protected Data")');
// Should redirect to login
await expect(page.locator('#login-section')).toBeVisible();
});
});
test.describe('Responsive Design', () => {
test('should work on mobile devices', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
await expect(page.locator('.container')).toBeVisible();
await expect(page.locator('#login-form')).toBeVisible();
// Test form interaction on mobile
await page.fill('#login-email', 'mobile@example.com');
await page.fill('#login-password', 'password123');
// Forms should be usable on mobile
const emailInput = page.locator('#login-email');
await expect(emailInput).toHaveValue('mobile@example.com');
});
});JavaScriptSecurity Testing
JWT Security Tests
Create backend/tests/test_security.py:
import pytest
import jwt
from datetime import datetime, timedelta
from app.auth.jwt_handler import JWTHandler
from app.security.security_middleware import SecurityMiddleware
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
class TestJWTSecurity:
@pytest.fixture
def jwt_handler(self):
return JWTHandler()
def test_reject_none_algorithm(self, jwt_handler):
"""Test rejection of 'none' algorithm tokens"""
# Create a token with 'none' algorithm
payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(hours=1)}
none_token = jwt.encode(payload, "", algorithm="none")
# Should reject the token
decoded = jwt_handler.verify_token(none_token)
assert decoded is None
def test_algorithm_confusion_attack(self, jwt_handler):
"""Test protection against algorithm confusion attacks"""
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Generate RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
# Create RS256 token
payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(hours=1)}
rs256_token = jwt.encode(payload, private_key, algorithm="RS256")
# Try to verify with HS256 using public key as secret
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# This should fail - algorithm confusion should be prevented
try:
decoded = jwt.decode(rs256_token, public_pem, algorithms=["HS256"])
assert False, "Algorithm confusion attack succeeded"
except jwt.InvalidSignatureError:
pass # Expected - attack prevented
def test_token_replay_attack(self, jwt_handler):
"""Test protection against token replay attacks"""
payload = {"sub": "user123"}
token1 = jwt_handler.create_access_token(payload)
token2 = jwt_handler.create_access_token(payload)
# Tokens should be different (due to timestamp/nonce)
assert token1 != token2
# Both should be valid
assert jwt_handler.verify_token(token1) is not None
assert jwt_handler.verify_token(token2) is not None
def test_weak_secret_rejection(self):
"""Test rejection of weak secrets"""
weak_secrets = ["", "123", "password", "a" * 10]
for weak_secret in weak_secrets:
# In production, you should validate secret strength
# This is a demonstration of what should be checked
assert len(weak_secret) < 32 # Minimum recommended length
def test_token_tampering_detection(self, jwt_handler):
"""Test detection of token tampering"""
payload = {"sub": "user123", "role": "user"}
token = jwt_handler.create_access_token(payload)
# Tamper with token by changing role
parts = token.split('.')
header = parts[0]
payload_part = parts[1]
signature = parts[2]
# Decode and modify payload
import base64
import json
# Add padding if needed
payload_part += '=' * (4 - len(payload_part) % 4)
decoded_payload = json.loads(base64.urlsafe_b64decode(payload_part))
# Tamper with role
decoded_payload["role"] = "admin"
# Re-encode
tampered_payload = base64.urlsafe_b64encode(
json.dumps(decoded_payload).encode()
).decode().rstrip('=')
# Create tampered token
tampered_token = f"{header}.{tampered_payload}.{signature}"
# Should be rejected
decoded = jwt_handler.verify_token(tampered_token)
assert decoded is None
class TestSecurityMiddleware:
def test_rate_limiting(self):
"""Test rate limiting functionality"""
# This would require testing the middleware with multiple requests
# Simplified test
for i in range(10):
response = client.get("/")
assert response.status_code == 200
# After many requests, should be rate limited
# (This depends on your rate limiting configuration)
def test_csrf_protection(self):
"""Test CSRF protection"""
# Attempt POST without CSRF token
response = client.post("/auth/logout")
# Should require proper authentication
assert response.status_code in [401, 403]
def test_xss_prevention(self):
"""Test XSS prevention in inputs"""
xss_payloads = [
"<script>alert('xss')</script>",
"javascript:alert('xss')",
"<img src=x onerror=alert('xss')>",
"';alert('xss');//"
]
for payload in xss_payloads:
# Try to register with XSS payload
response = client.post("/auth/register", json={
"email": f"test{payload}@example.com",
"password": "password123",
"full_name": payload
})
# Should either reject or sanitize
if response.status_code == 201:
data = response.json()
# Full name should be sanitized
assert "<script>" not in data["full_name"]
assert "javascript:" not in data["full_name"]
class TestPasswordSecurity:
def test_password_hashing_strength(self):
"""Test password hashing strength"""
jwt_handler = JWTHandler()
password = "test_password_123"
hashed = jwt_handler.hash_password(password)
# Should use bcrypt
assert hashed.startswith("$2b$")
# Should be different each time
hashed2 = jwt_handler.hash_password(password)
assert hashed != hashed2
# Both should verify correctly
assert jwt_handler.verify_password(password, hashed)
assert jwt_handler.verify_password(password, hashed2)
def test_password_timing_attack_resistance(self):
"""Test resistance to timing attacks"""
import time
jwt_handler = JWTHandler()
# Hash a password
real_password = "correct_password"
hashed = jwt_handler.hash_password(real_password)
# Time verification with correct password
start = time.time()
jwt_handler.verify_password(real_password, hashed)
correct_time = time.time() - start
# Time verification with incorrect password
start = time.time()
jwt_handler.verify_password("wrong_password", hashed)
incorrect_time = time.time() - start
# Times should be similar (within reasonable bounds)
# bcrypt naturally provides timing attack resistance
assert abs(correct_time - incorrect_time) < 0.1 # 100ms tolerancePythonPerformance Testing
Load Testing with Locust
Create tests/performance/locustfile.py:
from locust import HttpUser, task, between
import random
import string
class JWTAuthUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Setup user data"""
self.email = f"user_{random.randint(1000, 9999)}@example.com"
self.password = "password123"
self.token = None
# Register user
self.register()
# Login to get token
self.login()
def register(self):
"""Register a new user"""
user_data = {
"email": self.email,
"password": self.password,
"full_name": f"User {random.randint(1000, 9999)}"
}
response = self.client.post("/auth/register", json=user_data)
if response.status_code != 201:
print(f"Registration failed: {response.text}")
def login(self):
"""Login and store token"""
login_data = {
"username": self.email,
"password": self.password
}
response = self.client.post("/auth/login", data=login_data)
if response.status_code == 200:
data = response.json()
self.token = data["access_token"]
else:
print(f"Login failed: {response.text}")
@task(3)
def access_protected_data(self):
"""Access protected endpoints"""
if self.token:
headers = {"Authorization": f"Bearer {self.token}"}
self.client.get("/users/profile", headers=headers)
@task(1)
def refresh_token(self):
"""Refresh access token"""
if self.token:
# In a real scenario, you'd use the refresh token
# For this test, we'll just re-login
self.login()
@task(1)
def get_protected_data(self):
"""Get protected business data"""
if self.token:
headers = {"Authorization": f"Bearer {self.token}"}
self.client.get("/users/protected-data", headers=headers)
class TokenCreationUser(HttpUser):
"""Focused on testing token creation performance"""
wait_time = between(0.1, 0.5)
def on_start(self):
self.email = f"perf_{random.randint(10000, 99999)}@example.com"
self.password = "password123"
# Register once
user_data = {
"email": self.email,
"password": self.password,
"full_name": f"Perf User {random.randint(1000, 9999)}"
}
self.client.post("/auth/register", json=user_data)
@task
def login_stress_test(self):
"""Stress test login endpoint"""
login_data = {
"username": self.email,
"password": self.password
}
self.client.post("/auth/login", data=login_data)PythonTesting Configuration and CI/CD
GitHub Actions Workflow
Create .github/workflows/test.yml:
name: JWT Authentication Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
backend-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
cd backend
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov pytest-asyncio
- name: Run unit tests
run: |
cd backend
pytest tests/ -v --cov=app --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./backend/coverage.xml
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
cd frontend
npm ci
- name: Run tests
run: |
cd frontend
npm test -- --coverage --watchAll=false
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Python dependencies
run: |
cd backend
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install Playwright
run: |
cd e2e
npm ci
npx playwright install
- name: Run E2E tests
run: |
cd e2e
npm test
security-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run OWASP ZAP Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:8000'
- name: Run Bandit Security Scan
run: |
cd backend
pip install bandit
bandit -r app/ -f json -o bandit-report.json
- name: Upload security reports
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
backend/bandit-report.json
report_html.htmlYAMLThis comprehensive testing guide covers:
- Unit Testing: JWT handlers, password hashing, token validation
- Integration Testing: API endpoints, database interactions, authentication flows
- Frontend Testing: JavaScript components, API client, user interactions
- End-to-End Testing: Complete user journeys, cross-browser compatibility
- Security Testing: Token security, algorithm confusion, XSS/CSRF protection
- Performance Testing: Load testing, stress testing, token creation performance
- CI/CD Integration: Automated testing workflows and security scans
The testing strategy ensures your JWT authentication system is robust, secure, and performant in production environments.
20. JWT in Microservices Architecture
This chapter covers implementing JWT authentication in microservices environments, including service-to-service authentication, API gateway patterns, and distributed security strategies.
Microservices Authentication Architecture
graph TB
A[Client] --> B[API Gateway]
B --> C[Auth Service]
B --> D[User Service]
B --> E[Order Service]
B --> F[Payment Service]
B --> G[Notification Service]
C --> H[Identity Provider]
C --> I[Token Store/Cache]
D -.->|Service-to-Service| E
E -.->|Service-to-Service| F
F -.->|Service-to-Service| G
subgraph "Security Layer"
J[JWT Validation]
K[Rate Limiting]
L[Authorization]
end
B --> J
J --> K
K --> LAPI Gateway with JWT Authentication
Gateway Implementation with FastAPI
Create api-gateway/app/main.py:
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import httpx
import asyncio
from typing import Dict, Optional
import time
import logging
from functools import wraps
from app.auth.jwt_middleware import JWTMiddleware
from app.auth.service_registry import ServiceRegistry
from app.config import settings
from app.middleware.rate_limiter import RateLimiter
from app.middleware.circuit_breaker import CircuitBreaker
# Initialize FastAPI app
app = FastAPI(
title="API Gateway",
description="Centralized API Gateway with JWT authentication",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Add custom middleware
app.add_middleware(JWTMiddleware)
# Initialize components
service_registry = ServiceRegistry()
rate_limiter = RateLimiter()
circuit_breaker = CircuitBreaker()
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class APIGateway:
def __init__(self):
self.http_client = httpx.AsyncClient(timeout=30.0)
self.service_registry = service_registry
async def forward_request(
self,
service_name: str,
path: str,
method: str,
headers: Dict[str, str],
params: Dict = None,
data: bytes = None
) -> Dict:
"""Forward request to microservice"""
# Get service URL from registry
service_url = await self.service_registry.get_service_url(service_name)
if not service_url:
raise HTTPException(
status_code=503,
detail=f"Service {service_name} unavailable"
)
# Build request URL
url = f"{service_url}{path}"
# Prepare headers (remove host-specific headers)
filtered_headers = {
k: v for k, v in headers.items()
if k.lower() not in ['host', 'content-length']
}
# Add service authentication
filtered_headers.update(await self.get_service_auth_headers(service_name))
try:
# Forward request with circuit breaker
response = await circuit_breaker.call(
service_name,
self._make_request,
method=method,
url=url,
headers=filtered_headers,
params=params,
content=data
)
return {
"status_code": response.status_code,
"headers": dict(response.headers),
"content": response.content,
"json": response.json() if self._is_json_response(response) else None
}
except httpx.RequestError as e:
logger.error(f"Request to {service_name} failed: {e}")
raise HTTPException(
status_code=503,
detail=f"Service {service_name} request failed"
)
async def _make_request(self, **kwargs):
"""Make HTTP request to service"""
return await self.http_client.request(**kwargs)
def _is_json_response(self, response) -> bool:
"""Check if response is JSON"""
content_type = response.headers.get('content-type', '')
return 'application/json' in content_type
async def get_service_auth_headers(self, service_name: str) -> Dict[str, str]:
"""Get authentication headers for service-to-service communication"""
# Generate service token or get from cache
service_token = await self.service_registry.get_service_token(service_name)
return {
"X-Service-Token": service_token,
"X-Gateway-ID": settings.GATEWAY_ID
}
# Global gateway instance
gateway = APIGateway()
# Route handlers
@app.api_route("/auth/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def auth_service(request: Request, path: str):
"""Route to authentication service"""
# Extract request data
headers = dict(request.headers)
params = dict(request.query_params)
body = await request.body()
# Forward to auth service
response = await gateway.forward_request(
service_name="auth-service",
path=f"/{path}",
method=request.method,
headers=headers,
params=params,
data=body
)
return JSONResponse(
status_code=response["status_code"],
content=response["json"] or response["content"],
headers=response["headers"]
)
@app.api_route("/users/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def user_service(request: Request, path: str, current_user: Dict = Depends(JWTMiddleware.get_current_user)):
"""Route to user service (requires authentication)"""
# Add user context to headers
headers = dict(request.headers)
headers["X-User-ID"] = str(current_user["sub"])
headers["X-User-Role"] = current_user.get("role", "user")
params = dict(request.query_params)
body = await request.body()
# Apply rate limiting
await rate_limiter.check_limit(
key=f"user:{current_user['sub']}",
limit=100, # 100 requests per hour
window=3600
)
response = await gateway.forward_request(
service_name="user-service",
path=f"/{path}",
method=request.method,
headers=headers,
params=params,
data=body
)
return JSONResponse(
status_code=response["status_code"],
content=response["json"] or response["content"]
)
@app.api_route("/orders/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def order_service(request: Request, path: str, current_user: Dict = Depends(JWTMiddleware.get_current_user)):
"""Route to order service (requires authentication)"""
headers = dict(request.headers)
headers["X-User-ID"] = str(current_user["sub"])
headers["X-User-Role"] = current_user.get("role", "user")
params = dict(request.query_params)
body = await request.body()
response = await gateway.forward_request(
service_name="order-service",
path=f"/{path}",
method=request.method,
headers=headers,
params=params,
data=body
)
return JSONResponse(
status_code=response["status_code"],
content=response["json"] or response["content"]
)
@app.get("/health")
async def health_check():
"""Gateway health check"""
service_health = await service_registry.check_all_services()
return {
"status": "healthy" if all(service_health.values()) else "degraded",
"services": service_health,
"timestamp": time.time()
}
@app.on_event("startup")
async def startup_event():
"""Initialize gateway on startup"""
await service_registry.initialize()
logger.info("API Gateway started successfully")
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup on shutdown"""
await gateway.http_client.aclose()
logger.info("API Gateway shutdown completed")PythonJWT Middleware for Gateway
Create api-gateway/app/auth/jwt_middleware.py:
from fastapi import HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from typing import Dict, Optional
import time
import redis
import json
from app.config import settings
class JWTMiddleware:
def __init__(self):
self.secret_key = settings.JWT_SECRET_KEY
self.algorithm = settings.JWT_ALGORITHM
self.redis_client = redis.Redis.from_url(settings.REDIS_URL) if settings.REDIS_URL else None
async def __call__(self, request: Request, call_next):
"""Middleware to validate JWT tokens"""
# Skip authentication for certain paths
if self._should_skip_auth(request.url.path):
return await call_next(request)
# Extract token
token = self._extract_token(request)
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
# Validate token
try:
payload = await self._validate_token(token)
# Add user context to request
request.state.current_user = payload
# Continue processing
response = await call_next(request)
# Add token refresh header if needed
if self._token_needs_refresh(payload):
response.headers["X-Token-Refresh-Needed"] = "true"
return response
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
def _should_skip_auth(self, path: str) -> bool:
"""Check if path should skip authentication"""
skip_paths = [
"/auth/login",
"/auth/register",
"/auth/refresh",
"/health",
"/docs",
"/openapi.json"
]
return any(path.startswith(skip_path) for skip_path in skip_paths)
def _extract_token(self, request: Request) -> Optional[str]:
"""Extract JWT token from request"""
# Try Authorization header first
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
return auth_header.split(" ")[1]
# Try cookie
token_cookie = request.cookies.get("access_token")
if token_cookie:
return token_cookie
# Try query parameter (not recommended for production)
token_param = request.query_params.get("token")
if token_param:
return token_param
return None
async def _validate_token(self, token: str) -> Dict:
"""Validate JWT token"""
# Check token blacklist (if using Redis)
if self.redis_client and await self._is_token_blacklisted(token):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has been revoked"
)
# Decode and validate token
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
# Validate required claims
if not payload.get("sub"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token: missing subject"
)
# Check expiration
if payload.get("exp", 0) < time.time():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
async def _is_token_blacklisted(self, token: str) -> bool:
"""Check if token is blacklisted"""
try:
# Extract JTI from token (if present)
payload = jwt.decode(token, options={"verify_signature": False})
jti = payload.get("jti")
if jti:
return await self.redis_client.exists(f"blacklist:{jti}")
return False
except:
return False
def _token_needs_refresh(self, payload: Dict) -> bool:
"""Check if token needs refresh"""
exp = payload.get("exp", 0)
current_time = time.time()
# Refresh if token expires in less than 5 minutes
return (exp - current_time) < 300
@staticmethod
async def get_current_user(request: Request) -> Dict:
"""Dependency to get current user from request state"""
if not hasattr(request.state, 'current_user'):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
return request.state.current_userPythonService-to-Service Authentication
Service Registry Implementation
Create api-gateway/app/auth/service_registry.py:
import asyncio
import httpx
import jwt
import time
from typing import Dict, List, Optional
from datetime import datetime, timedelta
import logging
from app.config import settings
logger = logging.getLogger(__name__)
class ServiceRegistry:
def __init__(self):
self.services: Dict[str, Dict] = {}
self.service_tokens: Dict[str, str] = {}
self.http_client = httpx.AsyncClient()
async def initialize(self):
"""Initialize service registry"""
# Register known services
await self.register_service(
name="auth-service",
url=settings.AUTH_SERVICE_URL,
health_endpoint="/health"
)
await self.register_service(
name="user-service",
url=settings.USER_SERVICE_URL,
health_endpoint="/health"
)
await self.register_service(
name="order-service",
url=settings.ORDER_SERVICE_URL,
health_endpoint="/health"
)
# Start health checking
asyncio.create_task(self._health_check_loop())
logger.info("Service registry initialized")
async def register_service(
self,
name: str,
url: str,
health_endpoint: str = "/health"
):
"""Register a service"""
self.services[name] = {
"url": url,
"health_endpoint": health_endpoint,
"healthy": True,
"last_check": time.time(),
"failures": 0
}
# Generate service token
await self._generate_service_token(name)
logger.info(f"Registered service: {name} at {url}")
async def get_service_url(self, service_name: str) -> Optional[str]:
"""Get URL for healthy service"""
service = self.services.get(service_name)
if not service or not service["healthy"]:
return None
return service["url"]
async def get_service_token(self, service_name: str) -> Optional[str]:
"""Get authentication token for service"""
token = self.service_tokens.get(service_name)
# Check if token needs refresh
if not token or self._token_needs_refresh(token):
await self._generate_service_token(service_name)
token = self.service_tokens.get(service_name)
return token
async def _generate_service_token(self, service_name: str):
"""Generate service-to-service authentication token"""
payload = {
"sub": f"service:{service_name}",
"iss": "api-gateway",
"aud": service_name,
"iat": time.time(),
"exp": time.time() + 3600, # 1 hour expiry
"scope": "service-to-service",
"service_name": service_name
}
token = jwt.encode(
payload,
settings.SERVICE_SECRET_KEY,
algorithm="HS256"
)
self.service_tokens[service_name] = token
logger.debug(f"Generated service token for {service_name}")
def _token_needs_refresh(self, token: str) -> bool:
"""Check if service token needs refresh"""
try:
payload = jwt.decode(token, options={"verify_signature": False})
exp = payload.get("exp", 0)
# Refresh if expires in less than 10 minutes
return (exp - time.time()) < 600
except:
return True
async def check_service_health(self, service_name: str) -> bool:
"""Check health of specific service"""
service = self.services.get(service_name)
if not service:
return False
try:
url = f"{service['url']}{service['health_endpoint']}"
response = await self.http_client.get(url, timeout=5.0)
healthy = response.status_code == 200
# Update service status
service["healthy"] = healthy
service["last_check"] = time.time()
if healthy:
service["failures"] = 0
else:
service["failures"] += 1
# Remove service if too many failures
if service["failures"] >= 3:
logger.warning(f"Service {service_name} marked as unhealthy after {service['failures']} failures")
return healthy
except Exception as e:
logger.error(f"Health check failed for {service_name}: {e}")
service["healthy"] = False
service["failures"] += 1
return False
async def check_all_services(self) -> Dict[str, bool]:
"""Check health of all services"""
health_status = {}
for service_name in self.services:
health_status[service_name] = await self.check_service_health(service_name)
return health_status
async def _health_check_loop(self):
"""Periodic health checking"""
while True:
try:
await self.check_all_services()
await asyncio.sleep(30) # Check every 30 seconds
except Exception as e:
logger.error(f"Health check loop error: {e}")
await asyncio.sleep(60) # Wait longer on errorPythonMicroservice Implementation
User Service with Service Authentication
Create user-service/app/main.py:
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.middleware.cors import CORSMiddleware
import jwt
from typing import Dict, Optional
import time
from app.auth.service_auth import ServiceAuthMiddleware, require_service_auth, get_user_context
from app.models.user import User
from app.database import get_db
from app.config import settings
app = FastAPI(
title="User Service",
description="Microservice for user management",
version="1.0.0"
)
# Add service authentication middleware
app.add_middleware(ServiceAuthMiddleware)
# Add CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health_check():
"""Service health check"""
return {
"status": "healthy",
"service": "user-service",
"timestamp": time.time()
}
@app.get("/users/profile")
async def get_user_profile(
user_context: Dict = Depends(get_user_context),
service_auth: Dict = Depends(require_service_auth),
db = Depends(get_db)
):
"""Get user profile (requires service authentication)"""
user_id = user_context.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User context required"
)
# Query user from database
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"is_active": user.is_active,
"created_at": user.created_at.isoformat()
}
@app.get("/users/{user_id}")
async def get_user_by_id(
user_id: int,
service_auth: Dict = Depends(require_service_auth),
user_context: Dict = Depends(get_user_context),
db = Depends(get_db)
):
"""Get user by ID (admin or service access only)"""
# Check if requesting user is admin or if it's a service request
requesting_user_role = user_context.get("role", "user")
is_service_request = service_auth.get("scope") == "service-to-service"
if not is_service_request and requesting_user_role != "admin":
# Users can only access their own profile
requesting_user_id = user_context.get("user_id")
if str(requesting_user_id) != str(user_id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"is_active": user.is_active
}
@app.post("/users/{user_id}/orders")
async def create_user_order(
user_id: int,
order_data: dict,
service_auth: Dict = Depends(require_service_auth),
db = Depends(get_db)
):
"""Create order for user (service-to-service only)"""
# This endpoint is only accessible by other services
if service_auth.get("scope") != "service-to-service":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Service access required"
)
# Verify user exists
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Process order creation logic here
# This would typically involve business logic specific to your application
return {
"message": "Order created successfully",
"user_id": user_id,
"order_data": order_data
}
@app.get("/internal/users/batch")
async def get_users_batch(
user_ids: str, # Comma-separated IDs
service_auth: Dict = Depends(require_service_auth),
db = Depends(get_db)
):
"""Get multiple users by IDs (internal service use only)"""
# Only allow service-to-service calls
if service_auth.get("scope") != "service-to-service":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Internal endpoint - service access required"
)
# Parse user IDs
try:
ids = [int(id.strip()) for id in user_ids.split(",")]
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user IDs format"
)
# Query users
users = db.query(User).filter(User.id.in_(ids)).all()
return {
"users": [
{
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"is_active": user.is_active
}
for user in users
]
}PythonService Authentication Middleware
Create user-service/app/auth/service_auth.py:
from fastapi import HTTPException, status, Request, Depends
import jwt
from typing import Dict, Optional
import time
from app.config import settings
class ServiceAuthMiddleware:
def __init__(self):
self.service_secret = settings.SERVICE_SECRET_KEY
async def __call__(self, request: Request, call_next):
"""Middleware to validate service-to-service authentication"""
# Skip for health checks
if request.url.path == "/health":
return await call_next(request)
# Extract service token
service_token = request.headers.get("X-Service-Token")
if service_token:
# Validate service token
try:
service_payload = jwt.decode(
service_token,
self.service_secret,
algorithms=["HS256"]
)
# Store service context
request.state.service_auth = service_payload
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid service token"
)
# Extract user context from gateway headers
user_id = request.headers.get("X-User-ID")
user_role = request.headers.get("X-User-Role")
if user_id:
request.state.user_context = {
"user_id": int(user_id),
"role": user_role or "user"
}
return await call_next(request)
async def require_service_auth(request: Request) -> Dict:
"""Dependency to require service authentication"""
if not hasattr(request.state, 'service_auth'):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Service authentication required"
)
return request.state.service_auth
async def get_user_context(request: Request) -> Dict:
"""Dependency to get user context from gateway"""
if not hasattr(request.state, 'user_context'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User context not provided by gateway"
)
return request.state.user_context
def optional_user_context(request: Request) -> Optional[Dict]:
"""Optional user context dependency"""
if hasattr(request.state, 'user_context'):
return request.state.user_context
return NonePythonInter-Service Communication
Service Communication Helper
Create shared/service_client.py:
import httpx
import jwt
import time
from typing import Dict, Optional, Any
import asyncio
from functools import wraps
class ServiceClient:
def __init__(self, service_name: str, base_url: str, secret_key: str):
self.service_name = service_name
self.base_url = base_url
self.secret_key = secret_key
self.http_client = httpx.AsyncClient(timeout=30.0)
self._service_token = None
self._token_expires = 0
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.http_client.aclose()
def _generate_service_token(self) -> str:
"""Generate service authentication token"""
now = time.time()
payload = {
"sub": f"service:{self.service_name}",
"iat": now,
"exp": now + 3600, # 1 hour
"scope": "service-to-service"
}
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
self._service_token = token
self._token_expires = now + 3600
return token
def _get_service_token(self) -> str:
"""Get valid service token"""
if not self._service_token or time.time() >= (self._token_expires - 300):
return self._generate_service_token()
return self._service_token
async def request(
self,
method: str,
endpoint: str,
data: Any = None,
params: Dict = None,
headers: Dict = None,
**kwargs
) -> httpx.Response:
"""Make authenticated request to service"""
url = f"{self.base_url}{endpoint}"
# Prepare headers
request_headers = {
"X-Service-Token": self._get_service_token(),
"X-Service-Name": self.service_name,
"Content-Type": "application/json"
}
if headers:
request_headers.update(headers)
# Prepare request data
request_kwargs = {
"method": method,
"url": url,
"headers": request_headers,
"params": params,
**kwargs
}
if data is not None:
if isinstance(data, dict):
request_kwargs["json"] = data
else:
request_kwargs["content"] = data
# Make request with retry logic
return await self._request_with_retry(**request_kwargs)
async def _request_with_retry(self, max_retries: int = 3, **kwargs) -> httpx.Response:
"""Make request with retry logic"""
last_exception = None
for attempt in range(max_retries):
try:
response = await self.http_client.request(**kwargs)
if response.status_code < 500: # Don't retry client errors
return response
# Server error - retry after delay
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # Exponential backoff
except httpx.RequestError as e:
last_exception = e
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
# All retries failed
if last_exception:
raise last_exception
return response
# Convenience methods
async def get(self, endpoint: str, **kwargs) -> httpx.Response:
return await self.request("GET", endpoint, **kwargs)
async def post(self, endpoint: str, data: Any = None, **kwargs) -> httpx.Response:
return await self.request("POST", endpoint, data=data, **kwargs)
async def put(self, endpoint: str, data: Any = None, **kwargs) -> httpx.Response:
return await self.request("PUT", endpoint, data=data, **kwargs)
async def delete(self, endpoint: str, **kwargs) -> httpx.Response:
return await self.request("DELETE", endpoint, **kwargs)
# Service client factory
class ServiceClientFactory:
def __init__(self, secret_key: str):
self.secret_key = secret_key
self.clients: Dict[str, ServiceClient] = {}
def get_client(self, service_name: str, base_url: str) -> ServiceClient:
"""Get or create service client"""
key = f"{service_name}:{base_url}"
if key not in self.clients:
self.clients[key] = ServiceClient(
service_name=service_name,
base_url=base_url,
secret_key=self.secret_key
)
return self.clients[key]
async def close_all(self):
"""Close all client connections"""
for client in self.clients.values():
await client.http_client.aclose()
# Usage example in order service
class OrderService:
def __init__(self, service_factory: ServiceClientFactory):
self.user_client = service_factory.get_client(
"order-service",
"http://user-service:8000"
)
self.payment_client = service_factory.get_client(
"order-service",
"http://payment-service:8000"
)
async def create_order(self, user_id: int, order_data: Dict) -> Dict:
"""Create order with user validation and payment processing"""
# 1. Validate user exists
user_response = await self.user_client.get(f"/users/{user_id}")
if user_response.status_code != 200:
raise HTTPException(
status_code=400,
detail="Invalid user"
)
user_data = user_response.json()
# 2. Process payment
payment_data = {
"user_id": user_id,
"amount": order_data["total_amount"],
"payment_method": order_data["payment_method"]
}
payment_response = await self.payment_client.post(
"/payments/process",
data=payment_data
)
if payment_response.status_code != 200:
raise HTTPException(
status_code=400,
detail="Payment processing failed"
)
# 3. Create order record
order = {
"user_id": user_id,
"user_email": user_data["email"],
"items": order_data["items"],
"total_amount": order_data["total_amount"],
"payment_id": payment_response.json()["payment_id"],
"status": "confirmed"
}
# Save to database and return
return orderPythonDistributed Tracing and Monitoring
Request Tracing Middleware
Create shared/tracing_middleware.py:
import time
import uuid
from fastapi import Request
import logging
from typing import Dict, Optional
logger = logging.getLogger(__name__)
class TracingMiddleware:
def __init__(self):
self.service_name = None
def __call__(self, service_name: str):
self.service_name = service_name
return self._middleware
async def _middleware(self, request: Request, call_next):
"""Distributed tracing middleware"""
# Generate or extract trace ID
trace_id = request.headers.get("X-Trace-ID") or str(uuid.uuid4())
span_id = str(uuid.uuid4())
parent_span_id = request.headers.get("X-Span-ID")
# Start timing
start_time = time.time()
# Add tracing headers to request state
request.state.trace_id = trace_id
request.state.span_id = span_id
request.state.parent_span_id = parent_span_id
# Log request start
logger.info(
"Request started",
extra={
"trace_id": trace_id,
"span_id": span_id,
"parent_span_id": parent_span_id,
"service": self.service_name,
"method": request.method,
"path": request.url.path,
"user_agent": request.headers.get("User-Agent")
}
)
try:
# Process request
response = await call_next(request)
# Calculate duration
duration = time.time() - start_time
# Log successful response
logger.info(
"Request completed",
extra={
"trace_id": trace_id,
"span_id": span_id,
"service": self.service_name,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2)
}
)
# Add tracing headers to response
response.headers["X-Trace-ID"] = trace_id
response.headers["X-Span-ID"] = span_id
return response
except Exception as e:
# Calculate duration
duration = time.time() - start_time
# Log error
logger.error(
"Request failed",
extra={
"trace_id": trace_id,
"span_id": span_id,
"service": self.service_name,
"error": str(e),
"duration_ms": round(duration * 1000, 2)
}
)
raise
# Usage in services
def setup_tracing(app, service_name: str):
"""Setup tracing for a service"""
tracing_middleware = TracingMiddleware()
app.add_middleware(tracing_middleware(service_name))PythonThis microservices architecture provides:
- Centralized Authentication: API Gateway handles JWT validation
- Service-to-Service Security: Dedicated tokens for inter-service communication
- Service Discovery: Dynamic service registration and health checking
- Circuit Breaking: Fault tolerance for service communication
- Distributed Tracing: Request tracking across service boundaries
- Rate Limiting: Per-user and per-service rate limits
- Secure Communication: Authenticated and authorized service calls
The architecture ensures secure, scalable, and maintainable microservices with proper JWT authentication throughout the system.
21. Performance Optimization
This chapter covers advanced performance optimization techniques for JWT systems, including token size optimization, caching strategies, performance monitoring, and scalability best practices.
JWT Token Size Optimization
Understanding Token Size Impact
graph TB
A[JWT Token Size] --> B[Network Overhead]
A --> C[Storage Requirements]
A --> D[Processing Time]
A --> E[Mobile Performance]
B --> F[Request Latency]
C --> G[Cookie Limits]
D --> H[CPU Usage]
E --> I[Battery Impact]
subgraph "Optimization Strategies"
J[Claim Minimization]
K[Compact Encoding]
L[Reference Tokens]
M[Token Compression]
end
F --> J
G --> K
H --> L
I --> MToken Size Analysis Tool
Create performance/token_analyzer.py:
import jwt
import json
import base64
import gzip
from typing import Dict, Any, List, Tuple
from datetime import datetime, timedelta
import time
class JWTPerformanceAnalyzer:
def __init__(self):
self.secret_key = "your-secret-key"
self.algorithm = "HS256"
def analyze_token_size(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze JWT token size and performance characteristics"""
# Generate token
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
# Basic measurements
token_bytes = len(token.encode('utf-8'))
# Decode token parts
header, payload_data, signature = token.split('.')
# Analyze each part
header_size = len(header.encode('utf-8'))
payload_size = len(payload_data.encode('utf-8'))
signature_size = len(signature.encode('utf-8'))
# Decode payload for analysis
decoded_payload = json.loads(
base64.urlsafe_b64decode(payload_data + '==').decode('utf-8')
)
# Calculate compression potential
payload_json = json.dumps(decoded_payload, separators=(',', ':'))
compressed = gzip.compress(payload_json.encode('utf-8'))
compression_ratio = len(compressed) / len(payload_json)
# Network impact analysis
http_overhead = self._calculate_http_overhead(token_bytes)
return {
"token_size": {
"total_bytes": token_bytes,
"header_bytes": header_size,
"payload_bytes": payload_size,
"signature_bytes": signature_size,
"base64_overhead": self._calculate_base64_overhead(payload_json)
},
"payload_analysis": {
"claim_count": len(decoded_payload),
"json_size": len(payload_json),
"compressed_size": len(compressed),
"compression_ratio": compression_ratio,
"claims_breakdown": self._analyze_claims(decoded_payload)
},
"network_impact": {
"http_overhead_bytes": http_overhead,
"total_request_size": token_bytes + http_overhead,
"cookie_limit_usage": (token_bytes / 4096) * 100, # 4KB cookie limit
"mobile_3g_transfer_time_ms": self._estimate_transfer_time(token_bytes, "3g"),
"mobile_4g_transfer_time_ms": self._estimate_transfer_time(token_bytes, "4g")
},
"recommendations": self._generate_recommendations(decoded_payload, token_bytes)
}
def _calculate_base64_overhead(self, original_size: int) -> float:
"""Calculate Base64 encoding overhead"""
return (original_size * 4 / 3) - original_size
def _calculate_http_overhead(self, token_size: int) -> int:
"""Estimate HTTP header overhead"""
# Authorization: Bearer <token>
return len("Authorization: Bearer ") + 20 # Additional headers
def _estimate_transfer_time(self, bytes_size: int, network_type: str) -> float:
"""Estimate transfer time based on network conditions"""
speeds = {
"3g": 0.5 * 1024 * 1024 / 8, # 0.5 Mbps in bytes/sec
"4g": 10 * 1024 * 1024 / 8, # 10 Mbps in bytes/sec
"wifi": 50 * 1024 * 1024 / 8 # 50 Mbps in bytes/sec
}
speed = speeds.get(network_type, speeds["4g"])
return (bytes_size / speed) * 1000 # Convert to milliseconds
def _analyze_claims(self, payload: Dict[str, Any]) -> Dict[str, int]:
"""Analyze individual claim sizes"""
claims_size = {}
for key, value in payload.items():
claim_json = json.dumps({key: value}, separators=(',', ':'))
claims_size[key] = len(claim_json.encode('utf-8'))
return claims_size
def _generate_recommendations(self, payload: Dict[str, Any], token_size: int) -> List[str]:
"""Generate optimization recommendations"""
recommendations = []
# Size-based recommendations
if token_size > 2048:
recommendations.append("Token size exceeds 2KB - consider using reference tokens")
if token_size > 1024:
recommendations.append("Large token size - review claim necessity")
# Claim-based recommendations
claim_count = len(payload)
if claim_count > 10:
recommendations.append(f"High claim count ({claim_count}) - consider claim consolidation")
# Check for common optimization opportunities
if 'permissions' in payload and isinstance(payload['permissions'], list):
if len(payload['permissions']) > 5:
recommendations.append("Consider using role-based permissions instead of listing all permissions")
if 'groups' in payload and isinstance(payload['groups'], list):
if len(payload['groups']) > 3:
recommendations.append("Large groups list - consider using group IDs instead of names")
# String length checks
for key, value in payload.items():
if isinstance(value, str) and len(value) > 100:
recommendations.append(f"Long string value in '{key}' claim - consider using references")
return recommendations
def benchmark_algorithms(self, payload: Dict[str, Any]) -> Dict[str, Dict]:
"""Benchmark different JWT algorithms"""
algorithms = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512']
results = {}
for alg in algorithms:
try:
# Skip RSA algorithms for this simple test
if alg.startswith('RS'):
continue
start_time = time.perf_counter()
# Encode
token = jwt.encode(payload, self.secret_key, algorithm=alg)
encode_time = time.perf_counter() - start_time
# Decode
start_time = time.perf_counter()
jwt.decode(token, self.secret_key, algorithms=[alg])
decode_time = time.perf_counter() - start_time
results[alg] = {
"token_size": len(token.encode('utf-8')),
"encode_time_ms": encode_time * 1000,
"decode_time_ms": decode_time * 1000,
"total_time_ms": (encode_time + decode_time) * 1000
}
except Exception as e:
results[alg] = {"error": str(e)}
return results
def optimize_payload(self, payload: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, str]]:
"""Optimize payload for minimum size"""
optimized = payload.copy()
optimizations = {}
# Shorten claim names
claim_mappings = {
'user_id': 'uid',
'username': 'usr',
'email': 'eml',
'full_name': 'fn',
'permissions': 'perms',
'groups': 'grps',
'issued_at': 'iat',
'expires_at': 'exp',
'not_before': 'nbf'
}
for old_key, new_key in claim_mappings.items():
if old_key in optimized:
optimized[new_key] = optimized.pop(old_key)
optimizations[f"Claim name shortening"] = f"'{old_key}' -> '{new_key}'"
# Optimize arrays
if 'perms' in optimized and isinstance(optimized['perms'], list):
# Convert permission names to IDs or codes
perm_map = {
'read_users': 'ru',
'write_users': 'wu',
'delete_users': 'du',
'read_orders': 'ro',
'write_orders': 'wo',
'admin_access': 'aa'
}
optimized_perms = []
for perm in optimized['perms']:
if perm in perm_map:
optimized_perms.append(perm_map[perm])
else:
optimized_perms.append(perm)
optimized['perms'] = optimized_perms
optimizations["Permission optimization"] = "Converted to short codes"
# Remove unnecessary claims
current_time = int(time.time())
if 'iat' in optimized and abs(optimized['iat'] - current_time) < 60:
# Remove iat if it's very recent (can be inferred)
del optimized['iat']
optimizations["Remove redundant iat"] = "Recent timestamp removed"
return optimized, optimizations
# Usage example
def analyze_jwt_performance():
"""Example usage of JWT performance analyzer"""
analyzer = JWTPerformanceAnalyzer()
# Sample payload
sample_payload = {
"sub": "1234567890",
"name": "John Doe",
"email": "john.doe@example.com",
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"permissions": ["read_users", "write_users", "read_orders"],
"groups": ["admin", "managers", "developers"],
"department": "Engineering",
"location": "San Francisco, CA"
}
# Analyze original token
print("=== Original Token Analysis ===")
original_analysis = analyzer.analyze_token_size(sample_payload)
print(f"Token size: {original_analysis['token_size']['total_bytes']} bytes")
print(f"Claim count: {original_analysis['payload_analysis']['claim_count']}")
print(f"Compression ratio: {original_analysis['payload_analysis']['compression_ratio']:.2f}")
# Optimize payload
optimized_payload, optimizations = analyzer.optimize_payload(sample_payload)
print("\n=== Optimized Token Analysis ===")
optimized_analysis = analyzer.analyze_token_size(optimized_payload)
print(f"Token size: {optimized_analysis['token_size']['total_bytes']} bytes")
print(f"Size reduction: {original_analysis['token_size']['total_bytes'] - optimized_analysis['token_size']['total_bytes']} bytes")
print("\n=== Optimizations Applied ===")
for optimization, description in optimizations.items():
print(f"- {optimization}: {description}")
print("\n=== Recommendations ===")
for recommendation in original_analysis['recommendations']:
print(f"- {recommendation}")
# Benchmark algorithms
print("\n=== Algorithm Benchmark ===")
benchmark = analyzer.benchmark_algorithms(sample_payload)
for alg, metrics in benchmark.items():
if 'error' not in metrics:
print(f"{alg}: {metrics['token_size']} bytes, "
f"{metrics['total_time_ms']:.2f}ms total")
if __name__ == "__main__":
analyze_jwt_performance()PythonCaching Strategies
Multi-Level JWT Caching
Create performance/jwt_cache.py:
import asyncio
import redis
import json
import time
import hashlib
from typing import Dict, Any, Optional, Union
from functools import wraps
import jwt
class JWTCacheManager:
def __init__(self, redis_url: str, default_ttl: int = 3600):
self.redis_client = redis.from_url(redis_url)
self.default_ttl = default_ttl
self.local_cache = {}
self.cache_stats = {
"hits": 0,
"misses": 0,
"local_hits": 0,
"redis_hits": 0
}
def _generate_cache_key(self, token: str) -> str:
"""Generate cache key for token"""
# Use hash to avoid storing sensitive data in cache keys
return f"jwt:decoded:{hashlib.sha256(token.encode()).hexdigest()[:16]}"
async def get_decoded_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Get decoded token from cache (multi-level)"""
cache_key = self._generate_cache_key(token)
# Level 1: Local memory cache
if cache_key in self.local_cache:
entry = self.local_cache[cache_key]
if entry['expires'] > time.time():
self.cache_stats["hits"] += 1
self.cache_stats["local_hits"] += 1
return entry['data']
else:
# Expired, remove from local cache
del self.local_cache[cache_key]
# Level 2: Redis cache
try:
cached_data = self.redis_client.get(cache_key)
if cached_data:
decoded_data = json.loads(cached_data)
# Store in local cache for faster access
self.local_cache[cache_key] = {
'data': decoded_data,
'expires': time.time() + 300 # 5 minutes in local cache
}
self.cache_stats["hits"] += 1
self.cache_stats["redis_hits"] += 1
return decoded_data
except Exception as e:
print(f"Redis cache error: {e}")
self.cache_stats["misses"] += 1
return None
async def set_decoded_token(self, token: str, decoded_data: Dict[str, Any], ttl: Optional[int] = None):
"""Store decoded token in cache"""
cache_key = self._generate_cache_key(token)
ttl = ttl or self.default_ttl
# Store in Redis
try:
self.redis_client.setex(
cache_key,
ttl,
json.dumps(decoded_data)
)
except Exception as e:
print(f"Redis cache set error: {e}")
# Store in local cache
self.local_cache[cache_key] = {
'data': decoded_data,
'expires': time.time() + min(ttl, 300) # Max 5 minutes local
}
async def invalidate_token(self, token: str):
"""Invalidate cached token"""
cache_key = self._generate_cache_key(token)
# Remove from local cache
if cache_key in self.local_cache:
del self.local_cache[cache_key]
# Remove from Redis
try:
self.redis_client.delete(cache_key)
except Exception as e:
print(f"Redis cache delete error: {e}")
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache performance statistics"""
total_requests = self.cache_stats["hits"] + self.cache_stats["misses"]
hit_rate = (self.cache_stats["hits"] / total_requests * 100) if total_requests > 0 else 0
return {
"total_requests": total_requests,
"cache_hits": self.cache_stats["hits"],
"cache_misses": self.cache_stats["misses"],
"hit_rate_percent": round(hit_rate, 2),
"local_cache_hits": self.cache_stats["local_hits"],
"redis_cache_hits": self.cache_stats["redis_hits"],
"local_cache_size": len(self.local_cache)
}
def cleanup_local_cache(self):
"""Clean up expired entries from local cache"""
current_time = time.time()
expired_keys = [
key for key, entry in self.local_cache.items()
if entry['expires'] <= current_time
]
for key in expired_keys:
del self.local_cache[key]
return len(expired_keys)
class CachedJWTHandler:
def __init__(self, secret_key: str, algorithm: str = "HS256", cache_manager: JWTCacheManager = None):
self.secret_key = secret_key
self.algorithm = algorithm
self.cache_manager = cache_manager
async def decode_token(self, token: str, verify: bool = True) -> Dict[str, Any]:
"""Decode JWT token with caching"""
if not self.cache_manager:
# No caching, decode directly
return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# Try cache first
cached_result = await self.cache_manager.get_decoded_token(token)
if cached_result:
return cached_result
# Cache miss, decode token
try:
decoded_data = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# Calculate TTL based on token expiry
exp = decoded_data.get('exp')
if exp:
ttl = max(int(exp - time.time()), 60) # Minimum 1 minute
else:
ttl = 3600 # Default 1 hour
# Store in cache
await self.cache_manager.set_decoded_token(token, decoded_data, ttl)
return decoded_data
except jwt.ExpiredSignatureError:
# Don't cache expired tokens
raise
except jwt.JWTError:
# Don't cache invalid tokens
raise
async def invalidate_user_tokens(self, user_id: str):
"""Invalidate all cached tokens for a user (simplified approach)"""
# In a real implementation, you'd need to track user tokens
# This is a placeholder for the concept
pass
# Performance monitoring decorator
def monitor_jwt_performance(func):
"""Decorator to monitor JWT operation performance"""
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.perf_counter()
try:
result = await func(*args, **kwargs)
duration = time.perf_counter() - start_time
# Log successful operation
print(f"JWT operation {func.__name__} completed in {duration*1000:.2f}ms")
return result
except Exception as e:
duration = time.perf_counter() - start_time
print(f"JWT operation {func.__name__} failed in {duration*1000:.2f}ms: {e}")
raise
return wrapper
# Usage example
async def example_cached_jwt_usage():
"""Example of using cached JWT handler"""
# Initialize cache manager
cache_manager = JWTCacheManager("redis://localhost:6379", default_ttl=3600)
# Initialize JWT handler with caching
jwt_handler = CachedJWTHandler("your-secret-key", cache_manager=cache_manager)
# Sample token
sample_payload = {
"sub": "1234567890",
"name": "John Doe",
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}
token = jwt.encode(sample_payload, "your-secret-key", algorithm="HS256")
# First decode (cache miss)
print("First decode (cache miss):")
start_time = time.perf_counter()
decoded1 = await jwt_handler.decode_token(token)
print(f"Time: {(time.perf_counter() - start_time)*1000:.2f}ms")
# Second decode (cache hit)
print("Second decode (cache hit):")
start_time = time.perf_counter()
decoded2 = await jwt_handler.decode_token(token)
print(f"Time: {(time.perf_counter() - start_time)*1000:.2f}ms")
# Display cache stats
stats = cache_manager.get_cache_stats()
print(f"Cache stats: {stats}")
if __name__ == "__main__":
asyncio.run(example_cached_jwt_usage())PythonDatabase Query Optimization
Optimized User Repository
Create performance/optimized_repository.py:
from sqlalchemy.orm import Session, joinedload, selectinload
from sqlalchemy import text, and_, or_
from typing import List, Optional, Dict, Any
import redis
import json
import time
from functools import lru_cache
from app.models.user import User, Role, Permission
from app.database import get_db
class OptimizedUserRepository:
def __init__(self, db: Session, redis_client: redis.Redis = None):
self.db = db
self.redis_client = redis_client
self.cache_ttl = 300 # 5 minutes
async def get_user_with_permissions(self, user_id: int) -> Optional[Dict[str, Any]]:
"""Get user with permissions optimized for JWT token generation"""
# Try cache first
if self.redis_client:
cache_key = f"user:permissions:{user_id}"
cached_data = self.redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
# Optimized query with eager loading
user = (
self.db.query(User)
.options(
joinedload(User.roles).joinedload(Role.permissions),
selectinload(User.permissions)
)
.filter(User.id == user_id)
.first()
)
if not user:
return None
# Build permissions set efficiently
permissions = set()
# Direct user permissions
for perm in user.permissions:
permissions.add(perm.name)
# Role-based permissions
for role in user.roles:
for perm in role.permissions:
permissions.add(perm.name)
# Build result
result = {
"id": user.id,
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"is_active": user.is_active,
"roles": [role.name for role in user.roles],
"permissions": list(permissions),
"last_login": user.last_login.isoformat() if user.last_login else None
}
# Cache result
if self.redis_client:
self.redis_client.setex(
cache_key,
self.cache_ttl,
json.dumps(result, default=str)
)
return result
async def batch_get_users(self, user_ids: List[int]) -> Dict[int, Dict[str, Any]]:
"""Efficiently get multiple users"""
# Check cache for each user
cached_users = {}
uncached_ids = []
if self.redis_client:
for user_id in user_ids:
cache_key = f"user:basic:{user_id}"
cached_data = self.redis_client.get(cache_key)
if cached_data:
cached_users[user_id] = json.loads(cached_data)
else:
uncached_ids.append(user_id)
else:
uncached_ids = user_ids
# Query uncached users in batch
if uncached_ids:
users = (
self.db.query(User)
.filter(User.id.in_(uncached_ids))
.all()
)
for user in users:
user_data = {
"id": user.id,
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"is_active": user.is_active
}
cached_users[user.id] = user_data
# Cache individual user
if self.redis_client:
cache_key = f"user:basic:{user.id}"
self.redis_client.setex(
cache_key,
self.cache_ttl,
json.dumps(user_data)
)
return cached_users
async def get_user_roles_compressed(self, user_id: int) -> Optional[str]:
"""Get user roles in compressed format for JWT"""
cache_key = f"user:roles:compressed:{user_id}"
if self.redis_client:
cached_data = self.redis_client.get(cache_key)
if cached_data:
return cached_data.decode('utf-8')
# Query roles efficiently
roles = (
self.db.query(Role.name)
.join(User.roles)
.filter(User.id == user_id)
.all()
)
if not roles:
return None
# Create compressed role representation
role_codes = {
'admin': 'a',
'manager': 'm',
'user': 'u',
'moderator': 'mod',
'support': 's'
}
compressed_roles = ','.join([
role_codes.get(role.name, role.name[:3])
for role in roles
])
# Cache result
if self.redis_client:
self.redis_client.setex(cache_key, self.cache_ttl, compressed_roles)
return compressed_roles
def invalidate_user_cache(self, user_id: int):
"""Invalidate all cache entries for a user"""
if not self.redis_client:
return
cache_patterns = [
f"user:permissions:{user_id}",
f"user:basic:{user_id}",
f"user:roles:compressed:{user_id}"
]
for pattern in cache_patterns:
self.redis_client.delete(pattern)
@lru_cache(maxsize=1000)
def get_permission_mapping(self) -> Dict[str, str]:
"""Get cached permission name to code mapping"""
permissions = self.db.query(Permission).all()
mapping = {}
for perm in permissions:
# Create short codes for common permissions
if perm.name.startswith('read_'):
code = 'r' + perm.name[5:7]
elif perm.name.startswith('write_'):
code = 'w' + perm.name[6:8]
elif perm.name.startswith('delete_'):
code = 'd' + perm.name[7:9]
else:
code = perm.name[:4]
mapping[perm.name] = code
return mapping
# Database connection pooling optimization
class OptimizedDatabase:
def __init__(self, database_url: str):
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
self.engine = create_engine(
database_url,
poolclass=QueuePool,
pool_size=20,
max_overflow=30,
pool_pre_ping=True,
pool_recycle=3600,
echo=False # Set to True for debugging
)
def get_connection_stats(self) -> Dict[str, Any]:
"""Get database connection pool statistics"""
pool = self.engine.pool
return {
"pool_size": pool.size(),
"checked_in": pool.checkedin(),
"checked_out": pool.checkedout(),
"overflow": pool.overflow(),
"invalid": pool.invalid()
}
# Query optimization examples
class OptimizedQueries:
@staticmethod
def get_user_login_optimized(db: Session, email: str) -> Optional[User]:
"""Optimized user login query"""
# Use index hint and specific columns
return (
db.query(User)
.filter(
and_(
User.email == email,
User.is_active == True
)
)
.options(
joinedload(User.roles).load_only(Role.name),
selectinload(User.permissions).load_only(Permission.name)
)
.first()
)
@staticmethod
def bulk_update_last_login(db: Session, user_ids: List[int], timestamp: int):
"""Bulk update last login timestamps"""
# Use bulk update for better performance
db.execute(
text(
"UPDATE users SET last_login = :timestamp WHERE id = ANY(:user_ids)"
),
{"timestamp": timestamp, "user_ids": user_ids}
)
db.commit()
@staticmethod
def get_active_users_count(db: Session) -> int:
"""Get count of active users efficiently"""
# Use count query instead of loading all records
return (
db.query(User.id)
.filter(User.is_active == True)
.count()
)
# Performance monitoring
class DatabasePerformanceMonitor:
def __init__(self):
self.query_stats = {}
def monitor_query(self, query_name: str):
"""Decorator to monitor query performance"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
try:
result = func(*args, **kwargs)
duration = time.perf_counter() - start_time
# Track performance
if query_name not in self.query_stats:
self.query_stats[query_name] = {
"count": 0,
"total_time": 0,
"avg_time": 0,
"max_time": 0
}
stats = self.query_stats[query_name]
stats["count"] += 1
stats["total_time"] += duration
stats["avg_time"] = stats["total_time"] / stats["count"]
stats["max_time"] = max(stats["max_time"], duration)
return result
except Exception as e:
print(f"Query {query_name} failed: {e}")
raise
return wrapper
return decorator
def get_performance_report(self) -> Dict[str, Any]:
"""Get performance report"""
return {
"queries": self.query_stats,
"total_queries": sum(stats["count"] for stats in self.query_stats.values()),
"slowest_queries": sorted(
self.query_stats.items(),
key=lambda x: x[1]["max_time"],
reverse=True
)[:5]
}PythonFrontend Performance Optimization
Optimized Token Management
Create frontend/performance/optimized-auth.js:
class OptimizedAuthManager {
constructor(config = {}) {
this.baseURL = config.baseURL || '/api';
this.tokenKey = config.tokenKey || 'access_token';
this.refreshKey = config.refreshKey || 'refresh_token';
// Performance optimization settings
this.requestQueue = [];
this.isRefreshing = false;
this.refreshPromise = null;
// Token validation cache
this.validationCache = new Map();
this.cacheTimeout = 30000; // 30 seconds
// Request deduplication
this.pendingRequests = new Map();
// Performance metrics
this.metrics = {
requestCount: 0,
cacheHits: 0,
tokenRefreshCount: 0,
averageResponseTime: 0,
totalResponseTime: 0
};
this.initializePerformanceMonitoring();
}
initializePerformanceMonitoring() {
// Monitor page visibility for token management
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.validateTokenOnFocus();
}
});
// Preload critical resources
this.preloadAuthResources();
}
async preloadAuthResources() {
// Preload authentication endpoints for faster response
const criticalEndpoints = ['/auth/validate', '/auth/refresh'];
for (const endpoint of criticalEndpoints) {
try {
// DNS prefetch and preconnect
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = this.baseURL;
document.head.appendChild(link);
} catch (error) {
console.warn('Preload failed:', error);
}
}
}
// Optimized token validation with caching
async validateToken(token = null) {
const tokenToValidate = token || this.getToken();
if (!tokenToValidate) {
return { valid: false, reason: 'no_token' };
}
// Check validation cache first
const cacheKey = this.hashToken(tokenToValidate);
const cached = this.validationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
this.metrics.cacheHits++;
return cached.result;
}
// Validate token
try {
const payload = this.parseJWTPayload(tokenToValidate);
const now = Math.floor(Date.now() / 1000);
const result = {
valid: payload.exp > now,
expiresAt: payload.exp,
needsRefresh: payload.exp - now < 300, // 5 minutes
payload: payload
};
// Cache result
this.validationCache.set(cacheKey, {
result: result,
timestamp: Date.now()
});
return result;
} catch (error) {
const result = { valid: false, reason: 'invalid_token', error: error.message };
// Cache negative result for shorter time
this.validationCache.set(cacheKey, {
result: result,
timestamp: Date.now() - (this.cacheTimeout / 2)
});
return result;
}
}
// Optimized API request with deduplication
async makeAuthenticatedRequest(url, options = {}) {
const requestKey = `${options.method || 'GET'}:${url}:${JSON.stringify(options.body || {})}`;
// Check for duplicate requests
if (this.pendingRequests.has(requestKey)) {
return this.pendingRequests.get(requestKey);
}
const requestPromise = this._executeRequest(url, options);
this.pendingRequests.set(requestKey, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
// Clean up pending request
setTimeout(() => {
this.pendingRequests.delete(requestKey);
}, 1000); // Allow 1 second for potential duplicates
}
}
async _executeRequest(url, options = {}) {
const startTime = performance.now();
this.metrics.requestCount++;
let token = this.getToken();
// Validate token before request
const validation = await this.validateToken(token);
if (!validation.valid) {
// Try to refresh token
const refreshed = await this.refreshTokenIfNeeded();
if (!refreshed) {
throw new Error('Authentication required');
}
token = this.getToken();
} else if (validation.needsRefresh) {
// Refresh in background
this.refreshTokenInBackground();
}
// Make request
const requestOptions = {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
};
try {
const response = await fetch(`${this.baseURL}${url}`, requestOptions);
// Update metrics
const responseTime = performance.now() - startTime;
this.updateMetrics(responseTime);
if (response.status === 401) {
// Token might be invalid, try refresh
const refreshed = await this.refreshTokenIfNeeded();
if (refreshed) {
// Retry request with new token
requestOptions.headers['Authorization'] = `Bearer ${this.getToken()}`;
return fetch(`${this.baseURL}${url}`, requestOptions);
}
}
return response;
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
async refreshTokenIfNeeded() {
if (this.isRefreshing) {
// Wait for ongoing refresh
return this.refreshPromise;
}
this.isRefreshing = true;
this.refreshPromise = this._performTokenRefresh();
try {
const result = await this.refreshPromise;
return result;
} finally {
this.isRefreshing = false;
this.refreshPromise = null;
}
}
async _performTokenRefresh() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
this.clearTokens();
return false;
}
try {
const response = await fetch(`${this.baseURL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refresh_token: refreshToken
})
});
if (response.ok) {
const data = await response.json();
this.setToken(data.access_token);
if (data.refresh_token) {
this.setRefreshToken(data.refresh_token);
}
this.metrics.tokenRefreshCount++;
// Clear validation cache
this.validationCache.clear();
return true;
} else {
this.clearTokens();
return false;
}
} catch (error) {
console.error('Token refresh failed:', error);
this.clearTokens();
return false;
}
}
refreshTokenInBackground() {
// Non-blocking token refresh
setTimeout(() => {
this.refreshTokenIfNeeded();
}, 0);
}
validateTokenOnFocus() {
// Validate token when page regains focus
setTimeout(async () => {
const validation = await this.validateToken();
if (!validation.valid) {
await this.refreshTokenIfNeeded();
}
}, 0);
}
// Utility methods
parseJWTPayload(token) {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}
const payload = parts[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
}
hashToken(token) {
// Simple hash for caching (in production, use proper hashing)
let hash = 0;
for (let i = 0; i < token.length; i++) {
const char = token.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString();
}
updateMetrics(responseTime) {
this.metrics.totalResponseTime += responseTime;
this.metrics.averageResponseTime = this.metrics.totalResponseTime / this.metrics.requestCount;
}
getPerformanceMetrics() {
return {
...this.metrics,
cacheHitRate: (this.metrics.cacheHits / this.metrics.requestCount * 100).toFixed(2) + '%',
validationCacheSize: this.validationCache.size,
pendingRequestsCount: this.pendingRequests.size
};
}
// Token storage methods
getToken() {
return localStorage.getItem(this.tokenKey);
}
setToken(token) {
localStorage.setItem(this.tokenKey, token);
}
getRefreshToken() {
return localStorage.getItem(this.refreshKey);
}
setRefreshToken(token) {
localStorage.setItem(this.refreshKey, token);
}
clearTokens() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshKey);
this.validationCache.clear();
}
// Cleanup method
cleanup() {
this.validationCache.clear();
this.pendingRequests.clear();
}
}
// Performance monitoring utilities
class AuthPerformanceMonitor {
constructor() {
this.metrics = {
authOperations: [],
networkLatency: [],
cachePerformance: []
};
}
recordAuthOperation(operation, duration, success) {
this.metrics.authOperations.push({
operation,
duration,
success,
timestamp: Date.now()
});
// Keep only last 100 operations
if (this.metrics.authOperations.length > 100) {
this.metrics.authOperations.shift();
}
}
recordNetworkLatency(endpoint, latency) {
this.metrics.networkLatency.push({
endpoint,
latency,
timestamp: Date.now()
});
// Keep only last 50 measurements
if (this.metrics.networkLatency.length > 50) {
this.metrics.networkLatency.shift();
}
}
getPerformanceReport() {
const authOps = this.metrics.authOperations;
const networkOps = this.metrics.networkLatency;
return {
authOperations: {
total: authOps.length,
successful: authOps.filter(op => op.success).length,
averageDuration: authOps.reduce((sum, op) => sum + op.duration, 0) / authOps.length,
successRate: (authOps.filter(op => op.success).length / authOps.length * 100).toFixed(2) + '%'
},
networkPerformance: {
totalRequests: networkOps.length,
averageLatency: networkOps.reduce((sum, op) => sum + op.latency, 0) / networkOps.length,
maxLatency: Math.max(...networkOps.map(op => op.latency)),
minLatency: Math.min(...networkOps.map(op => op.latency))
}
};
}
}
// Export for use
export { OptimizedAuthManager, AuthPerformanceMonitor };JavaScriptThis performance optimization chapter provides comprehensive strategies for optimizing JWT systems including token size reduction, multi-level caching, database optimization, and frontend performance enhancements.
22. Migration Strategies
This chapter covers practical strategies for migrating from existing authentication systems to JWT-based authentication, including session-based systems, legacy integrations, and smooth transition approaches.
Migration Planning Framework
Migrating from Session-Based Authentication
Dual Authentication System
Create migration/dual_auth_system.py:
from fastapi import FastAPI, Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from typing import Dict, Any, Optional, Union
import time
import redis
from datetime import datetime, timedelta
from app.models.user import User
from app.database import get_db
from app.config import settings
class DualAuthenticationManager:
"""Manages both session-based and JWT authentication during migration"""
def __init__(self, redis_client: redis.Redis, jwt_secret: str):
self.redis_client = redis_client
self.jwt_secret = jwt_secret
self.session_timeout = 3600 # 1 hour
self.migration_mode = True # Enable dual authentication
async def authenticate_request(self, request: Request) -> Dict[str, Any]:
"""Authenticate request using either session or JWT"""
# Try JWT authentication first (preferred)
jwt_user = await self._authenticate_jwt(request)
if jwt_user:
return {
"user": jwt_user,
"auth_type": "jwt",
"should_migrate": False
}
# Fall back to session authentication
session_user = await self._authenticate_session(request)
if session_user:
return {
"user": session_user,
"auth_type": "session",
"should_migrate": True # Mark for JWT migration
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
async def _authenticate_jwt(self, request: Request) -> Optional[Dict[str, Any]]:
"""Authenticate using JWT token"""
# Extract JWT from Authorization header
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return None
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(
token,
self.jwt_secret,
algorithms=["HS256"]
)
return {
"user_id": payload["sub"],
"email": payload.get("email"),
"roles": payload.get("roles", []),
"permissions": payload.get("permissions", [])
}
except jwt.JWTError:
return None
async def _authenticate_session(self, request: Request) -> Optional[Dict[str, Any]]:
"""Authenticate using legacy session"""
# Extract session ID from cookie
session_id = request.cookies.get("session_id")
if not session_id:
return None
# Check session in Redis/database
session_key = f"session:{session_id}"
session_data = self.redis_client.get(session_key)
if not session_data:
return None
try:
import json
session_info = json.loads(session_data)
# Validate session expiry
if session_info.get("expires_at", 0) < time.time():
self.redis_client.delete(session_key)
return None
return {
"user_id": session_info["user_id"],
"email": session_info.get("email"),
"roles": session_info.get("roles", []),
"permissions": session_info.get("permissions", [])
}
except (json.JSONDecodeError, KeyError):
return None
async def migrate_session_to_jwt(self, user_data: Dict[str, Any], response: Response) -> str:
"""Convert session authentication to JWT token"""
# Create JWT payload
payload = {
"sub": str(user_data["user_id"]),
"email": user_data["email"],
"roles": user_data.get("roles", []),
"permissions": user_data.get("permissions", []),
"iat": int(time.time()),
"exp": int(time.time()) + 3600, # 1 hour
"migration": True # Mark as migrated token
}
# Generate JWT token
token = jwt.encode(payload, self.jwt_secret, algorithm="HS256")
# Set JWT token as httpOnly cookie for smooth transition
response.set_cookie(
key="jwt_token",
value=token,
httponly=True,
secure=True,
samesite="strict",
max_age=3600
)
# Optionally remove old session
session_id = user_data.get("session_id")
if session_id:
self.redis_client.delete(f"session:{session_id}")
return token
def create_migration_response(self, response: Response, token: str):
"""Add migration headers and cookies to response"""
# Add migration indicator header
response.headers["X-Auth-Migrated"] = "true"
response.headers["X-Auth-Type"] = "jwt"
# Set new JWT cookie
response.set_cookie(
key="access_token",
value=token,
httponly=True,
secure=True,
samesite="strict",
max_age=3600
)
class SessionToJWTConverter:
"""Utility to convert existing sessions to JWT format"""
def __init__(self, db_session, redis_client, jwt_secret):
self.db = db_session
self.redis_client = redis_client
self.jwt_secret = jwt_secret
async def convert_all_active_sessions(self) -> Dict[str, Any]:
"""Convert all active sessions to JWT tokens"""
conversion_stats = {
"total_sessions": 0,
"converted": 0,
"failed": 0,
"expired": 0
}
# Get all active sessions from Redis
session_keys = self.redis_client.keys("session:*")
conversion_stats["total_sessions"] = len(session_keys)
for session_key in session_keys:
try:
session_data = self.redis_client.get(session_key)
if not session_data:
continue
import json
session_info = json.loads(session_data)
# Check if session is expired
if session_info.get("expires_at", 0) < time.time():
conversion_stats["expired"] += 1
self.redis_client.delete(session_key)
continue
# Get full user data
user = self.db.query(User).filter(
User.id == session_info["user_id"]
).first()
if not user:
conversion_stats["failed"] += 1
continue
# Create JWT token
jwt_token = await self._create_jwt_for_user(user)
# Store JWT token mapping
jwt_key = f"jwt_migration:{user.id}"
self.redis_client.setex(
jwt_key,
3600, # 1 hour
jwt_token
)
conversion_stats["converted"] += 1
except Exception as e:
print(f"Failed to convert session {session_key}: {e}")
conversion_stats["failed"] += 1
return conversion_stats
async def _create_jwt_for_user(self, user: User) -> str:
"""Create JWT token for user"""
# Get user roles and permissions
roles = [role.name for role in user.roles]
permissions = []
for role in user.roles:
permissions.extend([perm.name for perm in role.permissions])
# Add direct user permissions
permissions.extend([perm.name for perm in user.permissions])
# Remove duplicates
permissions = list(set(permissions))
payload = {
"sub": str(user.id),
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"roles": roles,
"permissions": permissions,
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"migrated": True
}
return jwt.encode(payload, self.jwt_secret, algorithm="HS256")
# FastAPI middleware for dual authentication
class MigrationAuthMiddleware:
def __init__(self, app: FastAPI, auth_manager: DualAuthenticationManager):
self.app = app
self.auth_manager = auth_manager
async def __call__(self, request: Request, call_next):
"""Middleware to handle dual authentication"""
# Skip authentication for certain paths
skip_paths = ["/auth/login", "/auth/register", "/health", "/docs"]
if any(request.url.path.startswith(path) for path in skip_paths):
return await call_next(request)
try:
# Authenticate request
auth_result = await self.auth_manager.authenticate_request(request)
# Store authentication info in request state
request.state.current_user = auth_result["user"]
request.state.auth_type = auth_result["auth_type"]
request.state.should_migrate = auth_result["should_migrate"]
# Process request
response = await call_next(request)
# Handle migration if needed
if auth_result["should_migrate"] and auth_result["auth_type"] == "session":
token = await self.auth_manager.migrate_session_to_jwt(
auth_result["user"],
response
)
self.auth_manager.create_migration_response(response, token)
return response
except HTTPException:
# Authentication failed
raise
except Exception as e:
print(f"Authentication middleware error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Authentication error"
)
# FastAPI app with dual authentication
def create_migration_app():
"""Create FastAPI app with migration support"""
app = FastAPI(title="Migration API", version="1.0.0")
# Initialize components
redis_client = redis.from_url(settings.REDIS_URL)
auth_manager = DualAuthenticationManager(redis_client, settings.JWT_SECRET_KEY)
# Add migration middleware
app.add_middleware(MigrationAuthMiddleware, auth_manager=auth_manager)
@app.post("/auth/migrate")
async def migrate_authentication(request: Request, response: Response):
"""Endpoint to explicitly migrate from session to JWT"""
if not hasattr(request.state, 'current_user'):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
if request.state.auth_type == "jwt":
return {"message": "Already using JWT authentication"}
# Migrate to JWT
token = await auth_manager.migrate_session_to_jwt(
request.state.current_user,
response
)
return {
"message": "Successfully migrated to JWT authentication",
"token_type": "Bearer",
"access_token": token
}
@app.get("/auth/status")
async def authentication_status(request: Request):
"""Get current authentication status"""
if not hasattr(request.state, 'current_user'):
return {"authenticated": False}
return {
"authenticated": True,
"auth_type": request.state.auth_type,
"user_id": request.state.current_user["user_id"],
"should_migrate": request.state.should_migrate
}
return appPythonLegacy Database Integration
Database Migration Utilities
Create migration/database_migration.py:
from sqlalchemy import create_engine, text, Column, Integer, String, DateTime, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from alembic import command
from alembic.config import Config
from typing import Dict, List, Any
import hashlib
import secrets
from datetime import datetime
Base = declarative_base()
class LegacyUser(Base):
"""Legacy user table structure"""
__tablename__ = "legacy_users"
id = Column(Integer, primary_key=True)
username = Column(String(255), unique=True)
email = Column(String(255), unique=True)
password_hash = Column(String(255)) # Different hashing algorithm
salt = Column(String(255))
full_name = Column(String(255))
created_at = Column(DateTime)
last_login = Column(DateTime)
is_active = Column(Boolean, default=True)
user_role = Column(String(50)) # Single role field
class DatabaseMigrationManager:
def __init__(self, legacy_db_url: str, new_db_url: str):
self.legacy_engine = create_engine(legacy_db_url)
self.new_engine = create_engine(new_db_url)
# Create sessions
LegacySession = sessionmaker(bind=self.legacy_engine)
NewSession = sessionmaker(bind=self.new_engine)
self.legacy_session = LegacySession()
self.new_session = NewSession()
self.migration_stats = {
"users_migrated": 0,
"users_failed": 0,
"roles_created": 0,
"permissions_mapped": 0
}
async def migrate_users(self, batch_size: int = 1000) -> Dict[str, Any]:
"""Migrate users from legacy database"""
# Get total count
total_users = self.legacy_session.query(LegacyUser).count()
print(f"Migrating {total_users} users...")
# Process in batches
offset = 0
while offset < total_users:
batch = (
self.legacy_session.query(LegacyUser)
.offset(offset)
.limit(batch_size)
.all()
)
await self._migrate_user_batch(batch)
offset += batch_size
print(f"Processed {min(offset, total_users)}/{total_users} users")
return self.migration_stats
async def _migrate_user_batch(self, users: List[LegacyUser]):
"""Migrate a batch of users"""
from app.models.user import User, Role
from app.auth.password_handler import get_password_hash
for legacy_user in users:
try:
# Check if user already exists
existing_user = (
self.new_session.query(User)
.filter(User.email == legacy_user.email)
.first()
)
if existing_user:
print(f"User {legacy_user.email} already exists, skipping")
continue
# Convert legacy password hash to new format
new_password_hash = await self._convert_password_hash(
legacy_user.password_hash,
legacy_user.salt
)
# Create new user
new_user = User(
username=legacy_user.username,
email=legacy_user.email,
hashed_password=new_password_hash,
full_name=legacy_user.full_name,
is_active=legacy_user.is_active,
created_at=legacy_user.created_at or datetime.utcnow(),
last_login=legacy_user.last_login
)
# Migrate user role
if legacy_user.user_role:
role = await self._get_or_create_role(legacy_user.user_role)
new_user.roles.append(role)
self.new_session.add(new_user)
self.new_session.commit()
self.migration_stats["users_migrated"] += 1
except Exception as e:
print(f"Failed to migrate user {legacy_user.email}: {e}")
self.new_session.rollback()
self.migration_stats["users_failed"] += 1
async def _convert_password_hash(self, legacy_hash: str, salt: str) -> str:
"""Convert legacy password hash to new format"""
# Option 1: Mark for password reset (recommended)
# Generate a temporary hash that will force password reset
return get_password_hash(secrets.token_urlsafe(32))
# Option 2: Convert hash if possible (depends on legacy algorithm)
# This example assumes MD5 -> bcrypt conversion
# In practice, you might need to invalidate passwords and force reset
async def _get_or_create_role(self, role_name: str) -> 'Role':
"""Get existing role or create new one"""
from app.models.user import Role
role = self.new_session.query(Role).filter(Role.name == role_name).first()
if not role:
role = Role(
name=role_name,
description=f"Migrated role: {role_name}"
)
self.new_session.add(role)
self.new_session.commit()
self.migration_stats["roles_created"] += 1
return role
async def migrate_custom_tables(self):
"""Migrate custom authentication-related tables"""
# Example: Migrate user groups
await self._migrate_user_groups()
# Example: Migrate user preferences
await self._migrate_user_preferences()
# Example: Migrate audit logs
await self._migrate_audit_logs()
async def _migrate_user_groups(self):
"""Migrate user groups to roles/permissions"""
# Query legacy groups
legacy_groups = self.legacy_session.execute(
text("SELECT * FROM user_groups")
).fetchall()
from app.models.user import Role, Permission
for group in legacy_groups:
# Create role for group
role = Role(
name=group.group_name,
description=group.description or f"Migrated from group: {group.group_name}"
)
self.new_session.add(role)
self.new_session.commit()
# Migrate group permissions
group_permissions = self.legacy_session.execute(
text("SELECT permission_name FROM group_permissions WHERE group_id = :group_id"),
{"group_id": group.id}
).fetchall()
for perm in group_permissions:
permission = (
self.new_session.query(Permission)
.filter(Permission.name == perm.permission_name)
.first()
)
if not permission:
permission = Permission(
name=perm.permission_name,
description=f"Migrated permission: {perm.permission_name}"
)
self.new_session.add(permission)
self.new_session.commit()
role.permissions.append(permission)
self.new_session.commit()
async def _migrate_user_preferences(self):
"""Migrate user preferences/settings"""
# This would be custom based on your legacy system
preferences = self.legacy_session.execute(
text("SELECT user_id, preference_key, preference_value FROM user_preferences")
).fetchall()
# Store in new format (could be JSON column or separate table)
user_prefs = {}
for pref in preferences:
if pref.user_id not in user_prefs:
user_prefs[pref.user_id] = {}
user_prefs[pref.user_id][pref.preference_key] = pref.preference_value
# Update users with preferences
from app.models.user import User
import json
for user_id, prefs in user_prefs.items():
user = self.new_session.query(User).filter(User.id == user_id).first()
if user:
# Assuming you have a preferences JSON column
user.preferences = json.dumps(prefs)
self.new_session.commit()
async def _migrate_audit_logs(self):
"""Migrate authentication audit logs"""
# Migrate login history
login_logs = self.legacy_session.execute(
text("""
SELECT user_id, login_time, ip_address, user_agent, success
FROM login_audit
WHERE login_time > NOW() - INTERVAL 90 DAY
""")
).fetchall()
# Insert into new audit system
for log in login_logs:
self.new_session.execute(
text("""
INSERT INTO auth_audit_logs (user_id, event_type, ip_address, user_agent, success, created_at)
VALUES (:user_id, 'login', :ip_address, :user_agent, :success, :created_at)
"""),
{
"user_id": log.user_id,
"ip_address": log.ip_address,
"user_agent": log.user_agent,
"success": log.success,
"created_at": log.login_time
}
)
self.new_session.commit()
def create_migration_report(self) -> Dict[str, Any]:
"""Generate migration report"""
return {
"migration_stats": self.migration_stats,
"timestamp": datetime.utcnow().isoformat(),
"legacy_db_info": {
"total_users": self.legacy_session.query(LegacyUser).count(),
"active_users": self.legacy_session.query(LegacyUser).filter(LegacyUser.is_active == True).count()
},
"new_db_info": {
"migrated_users": self.migration_stats["users_migrated"],
"failed_migrations": self.migration_stats["users_failed"]
}
}
def cleanup(self):
"""Clean up database connections"""
self.legacy_session.close()
self.new_session.close()
# Migration CLI tool
class MigrationCLI:
def __init__(self, config_file: str):
self.config = self._load_config(config_file)
self.migration_manager = DatabaseMigrationManager(
self.config["legacy_db_url"],
self.config["new_db_url"]
)
def _load_config(self, config_file: str) -> Dict[str, Any]:
"""Load migration configuration"""
import json
with open(config_file, 'r') as f:
return json.load(f)
async def run_migration(self):
"""Run complete migration process"""
print("Starting database migration...")
try:
# Step 1: Migrate users
print("Migrating users...")
await self.migration_manager.migrate_users()
# Step 2: Migrate custom tables
print("Migrating custom tables...")
await self.migration_manager.migrate_custom_tables()
# Step 3: Generate report
print("Generating migration report...")
report = self.migration_manager.create_migration_report()
# Save report
with open("migration_report.json", "w") as f:
import json
json.dump(report, f, indent=2)
print("Migration completed successfully!")
print(f"Users migrated: {report['migration_stats']['users_migrated']}")
print(f"Failed migrations: {report['migration_stats']['users_failed']}")
except Exception as e:
print(f"Migration failed: {e}")
raise
finally:
self.migration_manager.cleanup()
# Usage example
async def main():
"""Example migration execution"""
migration_cli = MigrationCLI("migration_config.json")
await migration_cli.run_migration()
if __name__ == "__main__":
import asyncio
asyncio.run(main())PythonGradual Migration Strategy
Feature Flag System for Authentication
Create migration/feature_flags.py:
import redis
import json
from typing import Dict, Any, Optional, List
from enum import Enum
from dataclasses import dataclass
class AuthFeatureFlag(Enum):
JWT_AUTHENTICATION = "jwt_auth"
SESSION_FALLBACK = "session_fallback"
MIGRATION_MODE = "migration_mode"
JWT_REFRESH = "jwt_refresh"
ENHANCED_SECURITY = "enhanced_security"
ROLE_BASED_AUTH = "rbac_auth"
@dataclass
class FeatureFlagConfig:
enabled: bool
rollout_percentage: float
user_groups: List[str]
start_date: Optional[str] = None
end_date: Optional[str] = None
conditions: Dict[str, Any] = None
class FeatureFlagManager:
def __init__(self, redis_client: redis.Redis):
self.redis_client = redis_client
self.flag_prefix = "feature_flag:"
def set_flag(self, flag: AuthFeatureFlag, config: FeatureFlagConfig):
"""Set feature flag configuration"""
flag_key = f"{self.flag_prefix}{flag.value}"
flag_data = {
"enabled": config.enabled,
"rollout_percentage": config.rollout_percentage,
"user_groups": config.user_groups,
"start_date": config.start_date,
"end_date": config.end_date,
"conditions": config.conditions or {}
}
self.redis_client.set(flag_key, json.dumps(flag_data))
def is_enabled(self, flag: AuthFeatureFlag, user_context: Dict[str, Any] = None) -> bool:
"""Check if feature flag is enabled for user"""
flag_key = f"{self.flag_prefix}{flag.value}"
flag_data = self.redis_client.get(flag_key)
if not flag_data:
return False
try:
config = json.loads(flag_data)
# Check if flag is globally disabled
if not config.get("enabled", False):
return False
# Check rollout percentage
if user_context and "user_id" in user_context:
user_hash = hash(str(user_context["user_id"])) % 100
if user_hash >= config.get("rollout_percentage", 0):
return False
# Check user groups
user_groups = user_context.get("groups", []) if user_context else []
required_groups = config.get("user_groups", [])
if required_groups and not any(group in user_groups for group in required_groups):
return False
# Check date conditions
from datetime import datetime
now = datetime.utcnow()
if config.get("start_date"):
start_date = datetime.fromisoformat(config["start_date"])
if now < start_date:
return False
if config.get("end_date"):
end_date = datetime.fromisoformat(config["end_date"])
if now > end_date:
return False
# Check custom conditions
conditions = config.get("conditions", {})
if conditions and not self._evaluate_conditions(conditions, user_context):
return False
return True
except (json.JSONDecodeError, ValueError):
return False
def _evaluate_conditions(self, conditions: Dict[str, Any], user_context: Dict[str, Any]) -> bool:
"""Evaluate custom conditions"""
if not user_context:
return False
for key, expected_value in conditions.items():
if key not in user_context or user_context[key] != expected_value:
return False
return True
def get_all_flags(self) -> Dict[str, Dict[str, Any]]:
"""Get all feature flags"""
flags = {}
flag_keys = self.redis_client.keys(f"{self.flag_prefix}*")
for key in flag_keys:
flag_name = key.decode('utf-8').replace(self.flag_prefix, "")
flag_data = self.redis_client.get(key)
if flag_data:
try:
flags[flag_name] = json.loads(flag_data)
except json.JSONDecodeError:
continue
return flags
class GradualMigrationController:
"""Controls gradual migration from session to JWT authentication"""
def __init__(self, feature_flag_manager: FeatureFlagManager):
self.feature_flags = feature_flag_manager
self.migration_phases = [
{"name": "Phase 1", "percentage": 10, "duration_days": 7},
{"name": "Phase 2", "percentage": 25, "duration_days": 7},
{"name": "Phase 3", "percentage": 50, "duration_days": 14},
{"name": "Phase 4", "percentage": 75, "duration_days": 14},
{"name": "Phase 5", "percentage": 100, "duration_days": 7}
]
def initialize_migration(self):
"""Initialize gradual migration flags"""
# Enable migration mode for all users
self.feature_flags.set_flag(
AuthFeatureFlag.MIGRATION_MODE,
FeatureFlagConfig(
enabled=True,
rollout_percentage=100,
user_groups=[]
)
)
# Start with JWT for small percentage
self.feature_flags.set_flag(
AuthFeatureFlag.JWT_AUTHENTICATION,
FeatureFlagConfig(
enabled=True,
rollout_percentage=10, # Start with 10%
user_groups=["beta_users", "admin"]
)
)
# Keep session fallback enabled
self.feature_flags.set_flag(
AuthFeatureFlag.SESSION_FALLBACK,
FeatureFlagConfig(
enabled=True,
rollout_percentage=100,
user_groups=[]
)
)
def advance_migration_phase(self, phase_number: int):
"""Advance to next migration phase"""
if phase_number <= 0 or phase_number > len(self.migration_phases):
raise ValueError("Invalid phase number")
phase = self.migration_phases[phase_number - 1]
# Update JWT authentication rollout
self.feature_flags.set_flag(
AuthFeatureFlag.JWT_AUTHENTICATION,
FeatureFlagConfig(
enabled=True,
rollout_percentage=phase["percentage"],
user_groups=["beta_users", "admin"]
)
)
print(f"Advanced to {phase['name']}: {phase['percentage']}% rollout")
def complete_migration(self):
"""Complete migration - disable session fallback"""
# Enable JWT for all users
self.feature_flags.set_flag(
AuthFeatureFlag.JWT_AUTHENTICATION,
FeatureFlagConfig(
enabled=True,
rollout_percentage=100,
user_groups=[]
)
)
# Disable session fallback
self.feature_flags.set_flag(
AuthFeatureFlag.SESSION_FALLBACK,
FeatureFlagConfig(
enabled=False,
rollout_percentage=0,
user_groups=[]
)
)
print("Migration completed - JWT authentication is now primary")
def rollback_migration(self):
"""Rollback migration in case of issues"""
# Disable JWT authentication
self.feature_flags.set_flag(
AuthFeatureFlag.JWT_AUTHENTICATION,
FeatureFlagConfig(
enabled=False,
rollout_percentage=0,
user_groups=[]
)
)
# Re-enable session fallback
self.feature_flags.set_flag(
AuthFeatureFlag.SESSION_FALLBACK,
FeatureFlagConfig(
enabled=True,
rollout_percentage=100,
user_groups=[]
)
)
print("Migration rolled back - session authentication restored")
def get_migration_status(self) -> Dict[str, Any]:
"""Get current migration status"""
jwt_flag = self.feature_flags.redis_client.get(
f"{self.feature_flags.flag_prefix}{AuthFeatureFlag.JWT_AUTHENTICATION.value}"
)
session_flag = self.feature_flags.redis_client.get(
f"{self.feature_flags.flag_prefix}{AuthFeatureFlag.SESSION_FALLBACK.value}"
)
jwt_config = json.loads(jwt_flag) if jwt_flag else {}
session_config = json.loads(session_flag) if session_flag else {}
return {
"jwt_enabled": jwt_config.get("enabled", False),
"jwt_rollout_percentage": jwt_config.get("rollout_percentage", 0),
"session_fallback_enabled": session_config.get("enabled", False),
"migration_phase": self._get_current_phase(jwt_config.get("rollout_percentage", 0))
}
def _get_current_phase(self, percentage: float) -> str:
"""Determine current migration phase"""
for i, phase in enumerate(self.migration_phases):
if percentage <= phase["percentage"]:
return f"Phase {i + 1}"
return "Complete"
# Migration monitoring
class MigrationMonitor:
def __init__(self, redis_client: redis.Redis):
self.redis_client = redis_client
def log_authentication_event(self, event_type: str, user_id: int, auth_method: str, success: bool):
"""Log authentication events during migration"""
event_data = {
"timestamp": time.time(),
"user_id": user_id,
"auth_method": auth_method,
"success": success,
"event_type": event_type
}
# Store in Redis list for analysis
self.redis_client.lpush(
"migration:auth_events",
json.dumps(event_data)
)
# Keep only last 10000 events
self.redis_client.ltrim("migration:auth_events", 0, 9999)
def get_migration_metrics(self) -> Dict[str, Any]:
"""Get migration performance metrics"""
# Get recent events
events = self.redis_client.lrange("migration:auth_events", 0, -1)
jwt_events = []
session_events = []
total_events = 0
for event_data in events:
try:
event = json.loads(event_data)
total_events += 1
if event["auth_method"] == "jwt":
jwt_events.append(event)
elif event["auth_method"] == "session":
session_events.append(event)
except json.JSONDecodeError:
continue
# Calculate metrics
jwt_success_rate = len([e for e in jwt_events if e["success"]]) / len(jwt_events) if jwt_events else 0
session_success_rate = len([e for e in session_events if e["success"]]) / len(session_events) if session_events else 0
return {
"total_auth_events": total_events,
"jwt_events": len(jwt_events),
"session_events": len(session_events),
"jwt_success_rate": jwt_success_rate * 100,
"session_success_rate": session_success_rate * 100,
"jwt_adoption_rate": (len(jwt_events) / total_events * 100) if total_events > 0 else 0
}
# Usage example
def setup_gradual_migration():
"""Example setup for gradual migration"""
import redis
redis_client = redis.from_url("redis://localhost:6379")
# Initialize components
feature_flags = FeatureFlagManager(redis_client)
migration_controller = GradualMigrationController(feature_flags)
monitor = MigrationMonitor(redis_client)
# Start migration
migration_controller.initialize_migration()
print("Gradual migration initialized")
print("Phase 1: 10% of users will use JWT authentication")
print("Monitor metrics and advance phases as needed")
return migration_controller, monitor
if __name__ == "__main__":
setup_gradual_migration()PythonThis migration strategies chapter provides comprehensive guidance for transitioning from legacy authentication systems to JWT-based authentication, including dual authentication support, database migration utilities, and gradual rollout strategies with feature flags.
23. Future Standards and Trends
This chapter explores emerging JWT standards, future authentication technologies, and evolving security practices that will shape the future of web authentication.
Emerging JWT Standards
JWT Profile for OAuth 2.1
graph TB
A[OAuth 2.1] --> B[Simplified Flows]
A --> C[Enhanced Security]
A --> D[JWT Profiles]
B --> E[Authorization Code + PKCE]
B --> F[Removed Implicit Flow]
B --> G[Removed Resource Owner Password]
C --> H[MTLS Support]
C --> I[DPoP Tokens]
C --> J[JWT Secured Authorization]
D --> K[Structured Tokens]
D --> L[Token Introspection]
D --> M[Rich Authorization Requests]
subgraph "Future Enhancements"
N[Zero-Knowledge Proofs]
O[Quantum-Resistant Algorithms]
P[Selective Disclosure]
Q[Credential Presentations]
endJWT Secured Authorization Requests (JAR)
Create future/jar_implementation.py:
import jwt
import json
import time
from typing import Dict, Any, Optional
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import base64
import secrets
class JWTSecuredAuthorizationRequest:
"""Implementation of JWT Secured Authorization Requests (RFC 9101)"""
def __init__(self, client_id: str, private_key_pem: str, key_id: str):
self.client_id = client_id
self.private_key = load_pem_private_key(private_key_pem.encode(), password=None)
self.key_id = key_id
self.algorithm = "RS256"
def create_request_object(
self,
authorization_details: Dict[str, Any],
redirect_uri: str,
state: str,
nonce: Optional[str] = None
) -> str:
"""Create JWT-secured authorization request object"""
# Create request object payload
payload = {
# Standard OAuth parameters
"iss": self.client_id,
"aud": "https://authorization-server.example.com",
"client_id": self.client_id,
"response_type": "code",
"redirect_uri": redirect_uri,
"scope": "openid profile email",
"state": state,
# JWT-specific claims
"iat": int(time.time()),
"exp": int(time.time()) + 300, # 5 minutes
"jti": secrets.token_urlsafe(16),
# Rich Authorization Requests (RAR)
"authorization_details": authorization_details,
# PKCE parameters
"code_challenge": self._generate_code_challenge(),
"code_challenge_method": "S256",
# Additional security parameters
"max_age": 3600, # Maximum authentication age
"acr_values": "urn:mace:incommon:iap:silver" # Authentication context
}
if nonce:
payload["nonce"] = nonce
# Create JWT header
header = {
"alg": self.algorithm,
"kid": self.key_id,
"typ": "oauth-authz-req+jwt"
}
# Sign the request object
request_object = jwt.encode(
payload,
self.private_key,
algorithm=self.algorithm,
headers=header
)
return request_object
def _generate_code_challenge(self) -> str:
"""Generate PKCE code challenge"""
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8')
code_verifier = code_verifier.rstrip('=')
# Store code verifier for later use
self.code_verifier = code_verifier
# Create code challenge
digest = hashes.Hash(hashes.SHA256())
digest.update(code_verifier.encode('utf-8'))
code_challenge = base64.urlsafe_b64encode(digest.finalize()).decode('utf-8')
code_challenge = code_challenge.rstrip('=')
return code_challenge
def create_rich_authorization_request(
self,
resource_access: Dict[str, Any],
redirect_uri: str,
state: str
) -> str:
"""Create Rich Authorization Request (RAR) with detailed permissions"""
# Example authorization details for different resource types
authorization_details = [
{
"type": "payment_initiation",
"actions": ["initiate", "status", "cancel"],
"creditor": {
"name": "Merchant Corp",
"account": "DE89370400440532013000"
},
"instructed_amount": {
"currency": "EUR",
"amount": "123.50"
}
},
{
"type": "account_information",
"actions": ["read"],
"accounts": [
{
"iban": "DE40100100103307118608",
"currency": "EUR"
}
]
},
{
"type": "openid_credential",
"credential_type": "https://credentials.example.com/identity_credential",
"format": "jwt_vc_json",
"locations": ["https://credential-issuer.example.com"]
}
]
return self.create_request_object(
authorization_details=authorization_details,
redirect_uri=redirect_uri,
state=state
)
class DemonstrableProofOfPossession:
"""Implementation of DPoP (Demonstrable Proof of Possession) tokens"""
def __init__(self, private_key_pem: str):
self.private_key = load_pem_private_key(private_key_pem.encode(), password=None)
self.public_key = self.private_key.public_key()
def create_dpop_proof(
self,
http_method: str,
http_uri: str,
access_token: Optional[str] = None,
nonce: Optional[str] = None
) -> str:
"""Create DPoP proof JWT"""
# Get public key JWK
public_key_jwk = self._get_public_key_jwk()
# Create DPoP proof payload
payload = {
"jti": secrets.token_urlsafe(16),
"htm": http_method,
"htu": http_uri,
"iat": int(time.time())
}
if access_token:
# Hash the access token
digest = hashes.Hash(hashes.SHA256())
digest.update(access_token.encode('utf-8'))
ath = base64.urlsafe_b64encode(digest.finalize()).decode('utf-8').rstrip('=')
payload["ath"] = ath
if nonce:
payload["nonce"] = nonce
# Create JWT header with public key
header = {
"alg": "RS256",
"typ": "dpop+jwt",
"jwk": public_key_jwk
}
# Sign the DPoP proof
dpop_proof = jwt.encode(
payload,
self.private_key,
algorithm="RS256",
headers=header
)
return dpop_proof
def _get_public_key_jwk(self) -> Dict[str, Any]:
"""Get public key in JWK format"""
# Extract public key components
public_numbers = self.public_key.public_numbers()
# Convert to JWK format
def _int_to_base64url(value: int) -> str:
"""Convert integer to base64url encoding"""
byte_length = (value.bit_length() + 7) // 8
bytes_value = value.to_bytes(byte_length, byteorder='big')
return base64.urlsafe_b64encode(bytes_value).decode('utf-8').rstrip('=')
return {
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": _int_to_base64url(public_numbers.n),
"e": _int_to_base64url(public_numbers.e)
}
def verify_dpop_proof(
self,
dpop_proof: str,
expected_method: str,
expected_uri: str,
access_token: Optional[str] = None
) -> bool:
"""Verify DPoP proof"""
try:
# Decode header to get public key
header = jwt.get_unverified_header(dpop_proof)
if header.get("typ") != "dpop+jwt":
return False
# Extract public key from JWK
jwk = header.get("jwk")
if not jwk:
return False
# Reconstruct public key
n = int.from_bytes(
base64.urlsafe_b64decode(jwk["n"] + "=="),
byteorder='big'
)
e = int.from_bytes(
base64.urlsafe_b64decode(jwk["e"] + "=="),
byteorder='big'
)
public_key = rsa.RSAPublicNumbers(e, n).public_key()
# Verify JWT
payload = jwt.decode(
dpop_proof,
public_key,
algorithms=["RS256"]
)
# Verify claims
if payload.get("htm") != expected_method:
return False
if payload.get("htu") != expected_uri:
return False
# Verify access token hash if provided
if access_token and "ath" in payload:
digest = hashes.Hash(hashes.SHA256())
digest.update(access_token.encode('utf-8'))
expected_ath = base64.urlsafe_b64encode(digest.finalize()).decode('utf-8').rstrip('=')
if payload["ath"] != expected_ath:
return False
return True
except Exception:
return False
# Selective Disclosure for JWT
class SelectiveDisclosureJWT:
"""Implementation of Selective Disclosure for JWTs (SD-JWT)"""
def __init__(self, issuer_key: str):
self.issuer_key = issuer_key
def create_sd_jwt(
self,
claims: Dict[str, Any],
disclosable_claims: list,
holder_public_key: Optional[str] = None
) -> Dict[str, Any]:
"""Create Selective Disclosure JWT"""
import hashlib
# Separate disclosable and non-disclosable claims
disclosed_claims = {}
disclosures = []
sd_hash_claims = {}
for claim_name, claim_value in claims.items():
if claim_name in disclosable_claims:
# Create disclosure for this claim
salt = secrets.token_urlsafe(16)
disclosure = [salt, claim_name, claim_value]
disclosure_json = json.dumps(disclosure, separators=(',', ':'))
disclosure_b64 = base64.urlsafe_b64encode(
disclosure_json.encode('utf-8')
).decode('utf-8').rstrip('=')
disclosures.append(disclosure_b64)
# Create hash for SD claim
digest = hashlib.sha256(disclosure_b64.encode('utf-8')).digest()
sd_hash = base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')
if "_sd" not in sd_hash_claims:
sd_hash_claims["_sd"] = []
sd_hash_claims["_sd"].append(sd_hash)
else:
# Include claim directly
disclosed_claims[claim_name] = claim_value
# Combine claims
jwt_payload = {**disclosed_claims, **sd_hash_claims}
if holder_public_key:
jwt_payload["cnf"] = {"jwk": holder_public_key}
# Add SD-JWT specific claims
jwt_payload["_sd_alg"] = "sha-256"
jwt_payload["iss"] = "https://issuer.example.com"
jwt_payload["iat"] = int(time.time())
jwt_payload["exp"] = int(time.time()) + 3600
# Create JWT
sd_jwt = jwt.encode(jwt_payload, self.issuer_key, algorithm="HS256")
return {
"jwt": sd_jwt,
"disclosures": disclosures
}
def create_presentation(
self,
sd_jwt_data: Dict[str, Any],
claims_to_disclose: list,
holder_private_key: Optional[str] = None
) -> str:
"""Create presentation with selective disclosure"""
# Select disclosures for claims to reveal
selected_disclosures = []
for disclosure_b64 in sd_jwt_data["disclosures"]:
# Decode disclosure to check claim name
disclosure_json = base64.urlsafe_b64decode(
disclosure_b64 + "=="
).decode('utf-8')
disclosure = json.loads(disclosure_json)
claim_name = disclosure[1]
if claim_name in claims_to_disclose:
selected_disclosures.append(disclosure_b64)
# Create presentation format: <JWT>~<disclosure>~<disclosure>~...
presentation = sd_jwt_data["jwt"]
for disclosure in selected_disclosures:
presentation += f"~{disclosure}"
# Add holder binding if private key provided
if holder_private_key:
# Create key binding JWT
kb_payload = {
"iat": int(time.time()),
"aud": "https://verifier.example.com",
"nonce": secrets.token_urlsafe(16)
}
kb_jwt = jwt.encode(kb_payload, holder_private_key, algorithm="HS256")
presentation += f"~{kb_jwt}"
else:
presentation += "~"
return presentation
def verify_presentation(
self,
presentation: str,
issuer_public_key: str,
expected_holder_key: Optional[str] = None
) -> Dict[str, Any]:
"""Verify selective disclosure presentation"""
import hashlib
# Parse presentation
parts = presentation.split("~")
jwt_part = parts[0]
disclosure_parts = parts[1:-1]
kb_jwt_part = parts[-1] if parts[-1] else None
# Verify main JWT
try:
jwt_payload = jwt.decode(
jwt_part,
issuer_public_key,
algorithms=["HS256"]
)
except jwt.JWTError as e:
raise ValueError(f"Invalid JWT: {e}")
# Verify disclosures
disclosed_claims = {}
sd_hashes = jwt_payload.get("_sd", [])
for disclosure_b64 in disclosure_parts:
if not disclosure_b64:
continue
# Verify disclosure hash
digest = hashlib.sha256(disclosure_b64.encode('utf-8')).digest()
disclosure_hash = base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')
if disclosure_hash not in sd_hashes:
raise ValueError("Invalid disclosure hash")
# Decode disclosure
disclosure_json = base64.urlsafe_b64decode(
disclosure_b64 + "=="
).decode('utf-8')
disclosure = json.loads(disclosure_json)
salt, claim_name, claim_value = disclosure
disclosed_claims[claim_name] = claim_value
# Verify holder binding if present
if kb_jwt_part and expected_holder_key:
try:
kb_payload = jwt.decode(
kb_jwt_part,
expected_holder_key,
algorithms=["HS256"]
)
except jwt.JWTError as e:
raise ValueError(f"Invalid key binding: {e}")
# Combine verified claims
verified_claims = {
k: v for k, v in jwt_payload.items()
if not k.startswith("_sd")
}
verified_claims.update(disclosed_claims)
return verified_claims
# Example usage
def demo_future_standards():
"""Demonstrate future JWT standards"""
# Generate keys for demonstration
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
from cryptography.hazmat.primitives import serialization
private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
).decode('utf-8')
# 1. JWT Secured Authorization Request
print("=== JWT Secured Authorization Request ===")
jar = JWTSecuredAuthorizationRequest(
client_id="client123",
private_key_pem=private_key_pem,
key_id="key1"
)
request_object = jar.create_rich_authorization_request(
resource_access={"type": "payment", "amount": "100.00"},
redirect_uri="https://client.example.com/callback",
state="xyz123"
)
print(f"Request Object: {request_object[:100]}...")
# 2. DPoP Implementation
print("\n=== DPoP (Demonstrable Proof of Possession) ===")
dpop = DemonstrableProofOfPossession(private_key_pem)
dpop_proof = dpop.create_dpop_proof(
http_method="POST",
http_uri="https://api.example.com/resource",
access_token="access_token_value"
)
print(f"DPoP Proof: {dpop_proof[:100]}...")
# 3. Selective Disclosure JWT
print("\n=== Selective Disclosure JWT ===")
sd_jwt = SelectiveDisclosureJWT("secret_key")
user_claims = {
"sub": "user123",
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"address": "123 Main St",
"phone": "555-1234"
}
sd_jwt_data = sd_jwt.create_sd_jwt(
claims=user_claims,
disclosable_claims=["email", "age", "address", "phone"]
)
print(f"SD-JWT: {sd_jwt_data['jwt'][:100]}...")
print(f"Disclosures count: {len(sd_jwt_data['disclosures'])}")
# Create presentation with only name and email
presentation = sd_jwt.create_presentation(
sd_jwt_data,
claims_to_disclose=["name", "email"]
)
print(f"Presentation: {presentation[:100]}...")
if __name__ == "__main__":
demo_future_standards()PythonWebAuthn Integration with JWT
FIDO2/WebAuthn + JWT Implementation
Create future/webauthn_jwt.py:
import base64
import json
import secrets
import hashlib
from typing import Dict, Any, Optional, List
import jwt
import time
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key
class WebAuthnJWTManager:
"""Integration of WebAuthn with JWT for passwordless authentication"""
def __init__(self, rp_id: str, rp_name: str, jwt_secret: str):
self.rp_id = rp_id # Relying Party ID
self.rp_name = rp_name
self.jwt_secret = jwt_secret
self.origin = f"https://{rp_id}"
def generate_registration_options(self, user_id: str, username: str) -> Dict[str, Any]:
"""Generate WebAuthn registration options"""
challenge = secrets.token_bytes(32)
options = {
"publicKey": {
"challenge": base64.urlsafe_b64encode(challenge).decode('utf-8').rstrip('='),
"rp": {
"name": self.rp_name,
"id": self.rp_id
},
"user": {
"id": base64.urlsafe_b64encode(user_id.encode()).decode('utf-8').rstrip('='),
"name": username,
"displayName": username
},
"pubKeyCredParams": [
{"alg": -7, "type": "public-key"}, # ES256
{"alg": -257, "type": "public-key"}, # RS256
{"alg": -37, "type": "public-key"} # PS256
],
"authenticatorSelection": {
"authenticatorAttachment": "platform", # or "cross-platform"
"userVerification": "required",
"residentKey": "preferred"
},
"timeout": 60000,
"attestation": "direct"
}
}
# Store challenge for later verification
self._store_challenge(user_id, challenge)
return options
def verify_registration(
self,
user_id: str,
credential_response: Dict[str, Any]
) -> Dict[str, Any]:
"""Verify WebAuthn registration response and create credential"""
# Extract response data
response = credential_response["response"]
client_data = json.loads(
base64.urlsafe_b64decode(response["clientDataJSON"] + "==").decode('utf-8')
)
# Verify challenge
stored_challenge = self._get_stored_challenge(user_id)
received_challenge = base64.urlsafe_b64decode(client_data["challenge"] + "==")
if stored_challenge != received_challenge:
raise ValueError("Challenge mismatch")
# Verify origin
if client_data["origin"] != self.origin:
raise ValueError("Origin mismatch")
# Verify type
if client_data["type"] != "webauthn.create":
raise ValueError("Invalid ceremony type")
# Parse attestation object
attestation_object = base64.urlsafe_b64decode(
response["attestationObject"] + "=="
)
# In a real implementation, you would:
# 1. Parse CBOR attestation object
# 2. Verify attestation statement
# 3. Extract public key
# 4. Store credential
# For this example, we'll simulate the process
credential_id = credential_response["id"]
# Create credential record
credential = {
"id": credential_id,
"user_id": user_id,
"public_key": response["attestationObject"], # Simplified
"counter": 0,
"created_at": time.time()
}
return credential
def generate_authentication_options(self, user_id: Optional[str] = None) -> Dict[str, Any]:
"""Generate WebAuthn authentication options"""
challenge = secrets.token_bytes(32)
options = {
"publicKey": {
"challenge": base64.urlsafe_b64encode(challenge).decode('utf-8').rstrip('='),
"timeout": 60000,
"rpId": self.rp_id,
"userVerification": "required"
}
}
# Add allowed credentials if user is known
if user_id:
credentials = self._get_user_credentials(user_id)
options["publicKey"]["allowCredentials"] = [
{
"type": "public-key",
"id": cred["id"]
}
for cred in credentials
]
# Store challenge for specific user
self._store_challenge(user_id, challenge)
else:
# Store challenge for any user (discoverable credentials)
self._store_challenge("*", challenge)
return options
def verify_authentication_and_create_jwt(
self,
credential_response: Dict[str, Any],
user_id: Optional[str] = None
) -> Dict[str, Any]:
"""Verify WebAuthn authentication and create JWT token"""
# Extract response data
response = credential_response["response"]
credential_id = credential_response["id"]
# Get stored credential
credential = self._get_credential(credential_id)
if not credential:
raise ValueError("Credential not found")
# Verify user if specified
if user_id and credential["user_id"] != user_id:
raise ValueError("Credential does not belong to user")
# Parse client data
client_data = json.loads(
base64.urlsafe_b64decode(response["clientDataJSON"] + "==").decode('utf-8')
)
# Verify challenge
stored_challenge = self._get_stored_challenge(
credential["user_id"] if user_id else "*"
)
received_challenge = base64.urlsafe_b64decode(client_data["challenge"] + "==")
if stored_challenge != received_challenge:
raise ValueError("Challenge mismatch")
# Verify origin and type
if client_data["origin"] != self.origin:
raise ValueError("Origin mismatch")
if client_data["type"] != "webauthn.get":
raise ValueError("Invalid ceremony type")
# In a real implementation, verify the signature
# This would involve:
# 1. Parsing authenticator data
# 2. Verifying signature with stored public key
# 3. Checking counter value
# For this example, we'll assume verification passed
# Create JWT token
payload = {
"sub": credential["user_id"],
"iss": f"https://{self.rp_id}",
"aud": f"https://{self.rp_id}",
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"auth_method": "webauthn",
"credential_id": credential_id,
"user_verification": True,
"authenticator_attachment": "platform" # or from response
}
token = jwt.encode(payload, self.jwt_secret, algorithm="HS256")
return {
"access_token": token,
"token_type": "Bearer",
"expires_in": 3600,
"user_id": credential["user_id"]
}
def create_webauthn_jwt_middleware(self):
"""Create middleware for WebAuthn JWT verification"""
async def webauthn_jwt_middleware(request, call_next):
"""Middleware to verify WebAuthn-issued JWTs"""
# Extract token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return await call_next(request)
token = auth_header.split(" ")[1]
try:
# Decode and verify JWT
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
# Additional WebAuthn-specific verification
if payload.get("auth_method") != "webauthn":
raise ValueError("Not a WebAuthn token")
# Verify credential still exists
credential_id = payload.get("credential_id")
if not self._get_credential(credential_id):
raise ValueError("Credential no longer valid")
# Add user context to request
request.state.current_user = {
"user_id": payload["sub"],
"auth_method": "webauthn",
"credential_id": credential_id,
"user_verification": payload.get("user_verification", False)
}
return await call_next(request)
except (jwt.JWTError, ValueError) as e:
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid WebAuthn token: {e}"
)
return webauthn_jwt_middleware
# Helper methods (simplified storage)
def _store_challenge(self, user_id: str, challenge: bytes):
"""Store challenge for verification (use Redis in production)"""
# Simplified storage - use proper storage in production
if not hasattr(self, '_challenges'):
self._challenges = {}
self._challenges[user_id] = challenge
def _get_stored_challenge(self, user_id: str) -> bytes:
"""Get stored challenge"""
if not hasattr(self, '_challenges'):
return None
return self._challenges.get(user_id)
def _get_credential(self, credential_id: str) -> Optional[Dict[str, Any]]:
"""Get stored credential (use database in production)"""
# Simplified storage - use proper database in production
if not hasattr(self, '_credentials'):
self._credentials = {}
return self._credentials.get(credential_id)
def _get_user_credentials(self, user_id: str) -> List[Dict[str, Any]]:
"""Get all credentials for user"""
if not hasattr(self, '_credentials'):
return []
return [
cred for cred in self._credentials.values()
if cred["user_id"] == user_id
]
class PasskeyJWTManager(WebAuthnJWTManager):
"""Extended WebAuthn manager for Passkey support"""
def __init__(self, rp_id: str, rp_name: str, jwt_secret: str):
super().__init__(rp_id, rp_name, jwt_secret)
def generate_passkey_registration_options(
self,
user_id: str,
username: str
) -> Dict[str, Any]:
"""Generate registration options optimized for passkeys"""
options = self.generate_registration_options(user_id, username)
# Optimize for passkeys
options["publicKey"]["authenticatorSelection"].update({
"residentKey": "required", # Must store credential
"requireResidentKey": True,
"userVerification": "required"
})
# Add hints for better UX
options["publicKey"]["hints"] = ["client-device", "hybrid"]
return options
def create_cross_device_authentication_url(
self,
options: Dict[str, Any]
) -> str:
"""Create URL for cross-device authentication (QR code)"""
# Encode options for cross-device transport
options_b64 = base64.urlsafe_b64encode(
json.dumps(options).encode('utf-8')
).decode('utf-8').rstrip('=')
# Create URL with encoded options
return f"https://auth.{self.rp_id}/cross-device?options={options_b64}"
def verify_passkey_and_create_enhanced_jwt(
self,
credential_response: Dict[str, Any]
) -> Dict[str, Any]:
"""Verify passkey and create enhanced JWT with additional claims"""
# Standard verification
result = self.verify_authentication_and_create_jwt(credential_response)
# Decode the JWT to add passkey-specific claims
payload = jwt.decode(
result["access_token"],
self.jwt_secret,
algorithms=["HS256"]
)
# Add passkey-specific claims
payload.update({
"auth_method": "passkey",
"passkey_verified": True,
"authenticator_type": "multi-device", # or "single-device"
"cross_platform": True,
"backup_eligible": True, # From authenticator data
"backup_state": False # From authenticator data
})
# Create new JWT with enhanced claims
enhanced_token = jwt.encode(payload, self.jwt_secret, algorithm="HS256")
result["access_token"] = enhanced_token
return result
# Frontend JavaScript for WebAuthn + JWT
webauthn_frontend_js = '''
class WebAuthnJWTClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.accessToken = localStorage.getItem('webauthn_token');
}
async registerPasskey(username) {
try {
// Get registration options from server
const optionsResponse = await fetch(`${this.baseURL}/webauthn/register/begin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsResponse.json();
// Convert base64url to ArrayBuffer
options.publicKey.challenge = this.base64urlToBuffer(options.publicKey.challenge);
options.publicKey.user.id = this.base64urlToBuffer(options.publicKey.user.id);
// Create credential
const credential = await navigator.credentials.create(options);
// Prepare response for server
const credentialResponse = {
id: credential.id,
response: {
clientDataJSON: this.bufferToBase64url(credential.response.clientDataJSON),
attestationObject: this.bufferToBase64url(credential.response.attestationObject)
}
};
// Send to server for verification
const verifyResponse = await fetch(`${this.baseURL}/webauthn/register/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialResponse)
});
const result = await verifyResponse.json();
if (result.success) {
console.log('Passkey registered successfully');
return result;
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('Passkey registration failed:', error);
throw error;
}
}
async authenticateWithPasskey(username = null) {
try {
// Get authentication options
const optionsResponse = await fetch(`${this.baseURL}/webauthn/authenticate/begin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsResponse.json();
// Convert challenge to ArrayBuffer
options.publicKey.challenge = this.base64urlToBuffer(options.publicKey.challenge);
// Convert allowCredentials if present
if (options.publicKey.allowCredentials) {
options.publicKey.allowCredentials.forEach(cred => {
cred.id = this.base64urlToBuffer(cred.id);
});
}
// Get credential
const credential = await navigator.credentials.get(options);
// Prepare response
const credentialResponse = {
id: credential.id,
response: {
clientDataJSON: this.bufferToBase64url(credential.response.clientDataJSON),
authenticatorData: this.bufferToBase64url(credential.response.authenticatorData),
signature: this.bufferToBase64url(credential.response.signature),
userHandle: credential.response.userHandle ?
this.bufferToBase64url(credential.response.userHandle) : null
}
};
// Send to server for verification and JWT creation
const verifyResponse = await fetch(`${this.baseURL}/webauthn/authenticate/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialResponse)
});
const result = await verifyResponse.json();
if (result.access_token) {
// Store JWT token
this.accessToken = result.access_token;
localStorage.setItem('webauthn_token', this.accessToken);
console.log('Authenticated successfully with passkey');
return result;
} else {
throw new Error(result.error || 'Authentication failed');
}
} catch (error) {
console.error('Passkey authentication failed:', error);
throw error;
}
}
async makeAuthenticatedRequest(url, options = {}) {
if (!this.accessToken) {
throw new Error('No access token available');
}
const requestOptions = {
...options,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
};
const response = await fetch(`${this.baseURL}${url}`, requestOptions);
if (response.status === 401) {
// Token might be expired
this.accessToken = null;
localStorage.removeItem('webauthn_token');
throw new Error('Authentication expired');
}
return response;
}
// Utility methods
base64urlToBuffer(base64url) {
const padding = '='.repeat((4 - base64url.length % 4) % 4);
const base64 = (base64url + padding).replace(/\-/g, '+').replace(/_/g, '/');
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
bufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let str = '';
for (const byte of bytes) {
str += String.fromCharCode(byte);
}
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
logout() {
this.accessToken = null;
localStorage.removeItem('webauthn_token');
}
}
// Usage example
const webauthnClient = new WebAuthnJWTClient('/api');
// Register passkey
document.getElementById('register-btn').addEventListener('click', async () => {
try {
await webauthnClient.registerPasskey('user@example.com');
alert('Passkey registered successfully!');
} catch (error) {
alert('Registration failed: ' + error.message);
}
});
// Authenticate with passkey
document.getElementById('login-btn').addEventListener('click', async () => {
try {
await webauthnClient.authenticateWithPasskey();
alert('Logged in successfully!');
} catch (error) {
alert('Login failed: ' + error.message);
}
});
'''
def demo_webauthn_jwt():
"""Demonstrate WebAuthn + JWT integration"""
# Initialize WebAuthn JWT manager
webauthn_jwt = PasskeyJWTManager(
rp_id="example.com",
rp_name="Example Corp",
jwt_secret="your-jwt-secret"
)
print("=== WebAuthn + JWT Integration Demo ===")
# 1. Generate registration options
print("1. Generating passkey registration options...")
reg_options = webauthn_jwt.generate_passkey_registration_options(
user_id="user123",
username="john@example.com"
)
print(f"Challenge: {reg_options['publicKey']['challenge'][:20]}...")
# 2. Generate authentication options
print("2. Generating authentication options...")
auth_options = webauthn_jwt.generate_authentication_options("user123")
print(f"Challenge: {auth_options['publicKey']['challenge'][:20]}...")
# 3. Create cross-device URL
print("3. Creating cross-device authentication URL...")
cross_device_url = webauthn_jwt.create_cross_device_authentication_url(auth_options)
print(f"QR Code URL: {cross_device_url[:50]}...")
print("\nWebAuthn + JWT integration provides:")
print("- Passwordless authentication")
print("- Cross-device support")
print("- Enhanced security with biometrics")
print("- Seamless JWT token issuance")
if __name__ == "__main__":
demo_webauthn_jwt()PythonThis future standards chapter covers emerging JWT technologies, selective disclosure, WebAuthn integration, and advanced authentication patterns that represent the future of web authentication security.
24. Summary and Best Practices
This comprehensive guide has taken you through the complete journey of JWT implementation, from basic concepts to expert-level production systems. Let’s summarize the key learnings and best practices.
Key Takeaways
JWT Fundamentals
- JWT Structure: Header, Payload, Signature provide secure, stateless authentication
- Cryptographic Security: Use strong algorithms (RS256/ES256 for production)
- Token Lifecycle: Proper creation, validation, and expiration management
- Security First: Always prioritize security over convenience
Implementation Best Practices
Backend Security
# ✅ Secure JWT Configuration
JWT_CONFIG = {
"algorithm": "RS256", # Use asymmetric algorithms
"access_token_expire": 900, # 15 minutes
"refresh_token_expire": 604800, # 7 days
"issuer": "your-app.com",
"audience": "your-app-users",
"verify_signature": True,
"verify_exp": True,
"verify_iat": True,
"require_exp": True,
"require_iat": True
}
# ✅ Proper Error Handling
@app.exception_handler(JWTError)
async def jwt_exception_handler(request: Request, exc: JWTError):
return JSONResponse(
status_code=401,
content={"detail": "Invalid authentication credentials"}
)
# ✅ Secure Password Handling
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)PythonFrontend Security
// ✅ Secure Token Storage
class SecureTokenManager {
static setToken(token) {
// Use httpOnly cookies for sensitive tokens
document.cookie = `access_token=${token}; HttpOnly; Secure; SameSite=Strict`;
}
static getToken() {
// Implement proper token extraction
return this.getCookie('access_token');
}
static clearToken() {
document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
}
}
// ✅ Automatic Token Refresh
class AuthManager {
async makeRequest(url, options) {
let token = TokenManager.getToken();
// Check if token needs refresh
if (this.tokenNeedsRefresh(token)) {
token = await this.refreshToken();
}
return fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
...options.headers
}
});
}
}JavaScriptSecurity Checklist
Essential Security Measures
- Use HTTPS in production
- Implement proper CORS policies
- Use strong, unique secret keys
- Implement token rotation
- Add rate limiting
- Validate all inputs
- Use secure headers
- Implement proper logging
- Regular security audits
- Keep dependencies updated
Token Security
- Short-lived access tokens (15-30 minutes)
- Secure refresh token storage
- Token blacklisting capability
- Proper token validation
- Audience and issuer verification
- Protection against replay attacks
- Secure key management
- Regular key rotation
Production Deployment
- Environment-specific configurations
- Database connection pooling
- Caching strategy implementation
- Monitoring and alerting
- Backup and recovery procedures
- Performance optimization
- Load balancing configuration
- SSL/TLS certificates
Architecture Patterns
Recommended Architectures
graph TB
subgraph "Production Architecture"
A[Load Balancer] --> B[API Gateway]
B --> C[Auth Service]
B --> D[User Service]
B --> E[Business Services]
C --> F[Redis Cache]
C --> G[Database]
D --> G
E --> G
H[Monitoring] --> A
H --> B
H --> C
H --> D
H --> E
end
subgraph "Security Layer"
I[WAF]
J[Rate Limiting]
K[JWT Validation]
L[RBAC]
end
A --> I
I --> J
J --> K
K --> LMicroservices Considerations
- Service-to-Service Authentication: Use dedicated service tokens
- API Gateway Pattern: Centralize authentication and authorization
- Token Propagation: Securely pass user context between services
- Circuit Breakers: Implement fault tolerance
- Distributed Tracing: Track requests across services
Performance Optimization
Key Optimization Strategies
- Token Caching: Multi-level caching (memory + Redis)
- Database Optimization: Connection pooling and query optimization
- Token Size: Minimize payload size
- Algorithm Choice: Balance security and performance
- Network Optimization: Compression and CDN usage
Monitoring Metrics
# Essential metrics to track
METRICS_TO_MONITOR = {
"authentication": [
"login_success_rate",
"token_generation_time",
"token_validation_time",
"failed_authentication_attempts"
],
"performance": [
"api_response_time",
"database_query_time",
"cache_hit_ratio",
"token_refresh_frequency"
],
"security": [
"suspicious_login_attempts",
"token_validation_failures",
"brute_force_attempts",
"unusual_access_patterns"
]
}JavaScriptMigration Strategy
Phased Approach
- Assessment Phase: Analyze current system
- Dual Authentication: Run both systems in parallel
- Gradual Migration: Use feature flags for rollout
- Legacy Sunset: Phase out old system
- Optimization: Fine-tune new system
Future Considerations
Emerging Technologies
- WebAuthn/Passkeys: Passwordless authentication
- Zero-Knowledge Proofs: Enhanced privacy
- Quantum-Resistant Algorithms: Future-proof security
- Selective Disclosure: Privacy-preserving claims
- DeFi Integration: Blockchain-based identity
Continuous Improvement
- Stay updated with security best practices
- Regular penetration testing
- Community engagement and learning
- Technology stack evaluation
- Performance benchmarking
Common Pitfalls to Avoid
Security Pitfalls
❌ Storing sensitive data in JWT payload
❌ Using weak or default secret keys
❌ Ignoring token expiration
❌ Not validating token signatures
❌ Missing rate limiting
❌ Inadequate error handling
❌ Exposing debug information
Implementation Pitfalls
❌ Overly complex token structures
❌ Poor error messages
❌ Lack of proper testing
❌ Missing monitoring
❌ Inadequate documentation
❌ Not planning for scalability
Final Recommendations
For Beginners
- Start with basic implementation
- Focus on security fundamentals
- Use established libraries
- Implement comprehensive testing
- Follow security best practices
For Intermediate Developers
- Implement advanced features gradually
- Focus on performance optimization
- Add comprehensive monitoring
- Plan for scalability
- Consider microservices patterns
For Expert Developers
- Contribute to open-source solutions
- Stay current with emerging standards
- Implement custom security enhancements
- Mentor junior developers
- Share knowledge with the community
Conclusion
JWT authentication, when implemented correctly, provides a robust, scalable, and secure solution for modern web applications. This guide has covered:
- Fundamental Concepts: From basic JWT structure to advanced cryptographic principles
- Practical Implementation: Complete backend and frontend solutions
- Production Readiness: Deployment, monitoring, and optimization strategies
- Advanced Patterns: Microservices, performance optimization, and migration strategies
- Future Technologies: Emerging standards and authentication trends
The key to successful JWT implementation is balancing security, performance, and usability while maintaining a clear understanding of the underlying principles. Continue learning, stay updated with security best practices, and always prioritize the security of your users’ data.
Resources for Continued Learning
Official Specifications
- RFC 7519 – JSON Web Token (JWT)
- RFC 7515 – JSON Web Signature (JWS)
- RFC 7516 – JSON Web Encryption (JWE)
- RFC 7517 – JSON Web Key (JWK)
Security Resources
- OWASP JWT Security Cheat Sheet
- JWT.io – JWT debugger and library information
- Auth0 Blog – Authentication and security insights
Tools and Libraries
- Python: PyJWT, python-jose, authlib
- JavaScript: jsonwebtoken, jose, node-jsonwebtoken
- Testing: pytest, Jest, Playwright
- Monitoring: Prometheus, Grafana, ELK Stack
Thank you for following this comprehensive JWT guide. Apply these concepts responsibly and always prioritize security in your implementations.
24. System Design and Architectural Patterns
Introduction to JWT System Architecture
Building production-ready JWT authentication systems requires careful architectural planning. This chapter covers enterprise-level design patterns, scalability considerations, and architectural best practices for JWT implementations.
Core Architectural Principles
1. Separation of Concerns
graph TB
subgraph "Authentication Layer"
A[Auth Service]
B[Token Generator]
C[Token Validator]
end
subgraph "Business Logic Layer"
D[User Service]
E[Resource Service]
F[Permission Service]
end
subgraph "Data Layer"
G[User Database]
H[Token Blacklist]
I[Audit Logs]
end
A --> B
A --> C
B --> G
C --> H
D --> G
E --> F
F --> G2. Single Responsibility Principle
Each component should have one clear responsibility:
# ❌ Bad: Mixed responsibilities
class AuthService:
def authenticate_user(self, credentials):
# Validates credentials
# Generates token
# Logs audit
# Sends notification
# Updates user status
pass
# ✅ Good: Single responsibility
class AuthenticationValidator:
def validate_credentials(self, credentials):
"""Only validates credentials"""
pass
class TokenGenerator:
def create_token(self, user_data):
"""Only generates tokens"""
pass
class AuditLogger:
def log_auth_event(self, event):
"""Only handles logging"""
pass
class NotificationService:
def send_login_notification(self, user):
"""Only sends notifications"""
passPythonEnterprise Architectural Patterns
Pattern 1: Gateway Pattern (API Gateway)
Use Case: Centralized entry point for all client requests
sequenceDiagram
participant Client
participant Gateway as API Gateway
participant Auth as Auth Service
participant UserSvc as User Service
participant OrderSvc as Order Service
Client->>Gateway: Request with JWT
Gateway->>Gateway: Extract & Validate Token
Gateway->>Auth: Verify Token (Cache First)
Auth->>Gateway: Token Valid + Claims
Gateway->>Gateway: Check Rate Limits
Gateway->>Gateway: Apply Routing Rules
Gateway->>UserSvc: Forward Request (if user endpoint)
Gateway->>OrderSvc: Forward Request (if order endpoint)
UserSvc->>Gateway: Response
Gateway->>Client: ResponseImplementation:
# FastAPI API Gateway Example
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import JSONResponse
import httpx
from typing import Dict, Optional
import redis
from datetime import timedelta
class APIGateway:
def __init__(self):
self.app = FastAPI(title="API Gateway")
self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
self.token_cache_ttl = 300 # 5 minutes
# Service registry
self.services = {
"auth": "http://auth-service:8001",
"users": "http://user-service:8002",
"orders": "http://order-service:8003",
"products": "http://product-service:8004"
}
self.setup_routes()
def setup_routes(self):
@self.app.middleware("http")
async def gateway_middleware(request: Request, call_next):
# Extract token
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
# Validate token (with caching)
if not await self.validate_token_cached(token):
return JSONResponse(
status_code=401,
content={"detail": "Invalid or expired token"}
)
# Rate limiting
if not await self.check_rate_limit(token):
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded"}
)
response = await call_next(request)
return response
@self.app.api_route("/{service}/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def gateway_route(service: str, path: str, request: Request):
"""Route requests to appropriate microservice"""
if service not in self.services:
raise HTTPException(status_code=404, detail="Service not found")
# Build target URL
target_url = f"{self.services[service]}/{path}"
# Forward request
async with httpx.AsyncClient() as client:
response = await client.request(
method=request.method,
url=target_url,
headers=dict(request.headers),
content=await request.body()
)
return JSONResponse(
status_code=response.status_code,
content=response.json()
)
async def validate_token_cached(self, token: str) -> bool:
"""Validate token with Redis caching"""
cache_key = f"token:valid:{token}"
# Check cache first
cached_result = self.redis_client.get(cache_key)
if cached_result is not None:
return cached_result.decode() == "1"
# Validate with auth service
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{self.services['auth']}/validate",
json={"token": token}
)
is_valid = response.status_code == 200
# Cache result
self.redis_client.setex(
cache_key,
self.token_cache_ttl,
"1" if is_valid else "0"
)
return is_valid
except Exception:
return False
async def check_rate_limit(self, token: str) -> bool:
"""Implement token bucket rate limiting"""
rate_key = f"rate:{token}"
# Get current count
current = self.redis_client.get(rate_key)
if current is None:
# First request, set counter
self.redis_client.setex(rate_key, 60, "1")
return True
count = int(current.decode())
max_requests = 100 # 100 requests per minute
if count >= max_requests:
return False
# Increment counter
self.redis_client.incr(rate_key)
return True
# Initialize gateway
gateway = APIGateway()
app = gateway.appPythonPattern 2: Service Mesh Pattern
Use Case: Decentralized service-to-service communication
graph TB
subgraph "Service Mesh Control Plane"
A[Service Registry]
B[Config Management]
C[Policy Engine]
end
subgraph "Service Mesh Data Plane"
D[Service A + Sidecar]
E[Service B + Sidecar]
F[Service C + Sidecar]
end
A --> D
A --> E
A --> F
D <--> E
E <--> F
D <--> F
style D fill:#e1f5ff
style E fill:#e1f5ff
style F fill:#e1f5ffImplementation with Istio:
# JWT Authentication Policy for Istio
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-authentication
namespace: default
spec:
selector:
matchLabels:
app: user-service
jwtRules:
- issuer: "https://your-auth-service.com"
jwksUri: "https://your-auth-service.com/.well-known/jwks.json"
audiences:
- "user-service"
- "api-gateway"
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: default
spec:
selector:
matchLabels:
app: user-service
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["*"]
to:
- operation:
methods: ["GET", "POST"]
paths: ["/api/users/*"]
when:
- key: request.auth.claims[role]
values: ["admin", "user"]YAMLPattern 3: CQRS with JWT
Use Case: Separate read and write operations with different security models
graph LR
A[Client] -->|Write Command + JWT| B[Command Service]
A -->|Read Query + JWT| C[Query Service]
B -->|Write| D[Write Database]
D -->|Event Stream| E[Event Bus]
E -->|Update| F[Read Database]
C -->|Read| F
B -->|Validate Token| G[Auth Service]
C -->|Validate Token| GImplementation:
from fastapi import FastAPI, Depends, HTTPException
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
import asyncio
# Command Models
class CreateUserCommand(BaseModel):
email: str
full_name: str
role: str
class UpdateUserCommand(BaseModel):
user_id: int
full_name: Optional[str] = None
role: Optional[str] = None
# Query Models
class UserQuery(BaseModel):
user_id: Optional[int] = None
email: Optional[str] = None
role: Optional[str] = None
# Command Handler (Write Side)
class UserCommandHandler:
def __init__(self, write_db, event_bus):
self.write_db = write_db
self.event_bus = event_bus
async def handle_create_user(
self,
command: CreateUserCommand,
current_user: dict
):
"""Handle user creation command"""
# Require admin role for write operations
if current_user.get("role") != "admin":
raise HTTPException(
status_code=403,
detail="Only admins can create users"
)
# Write to write database
user_id = await self.write_db.create_user(
email=command.email,
full_name=command.full_name,
role=command.role
)
# Publish event
await self.event_bus.publish("UserCreated", {
"user_id": user_id,
"email": command.email,
"full_name": command.full_name,
"role": command.role,
"timestamp": datetime.utcnow().isoformat()
})
return {"user_id": user_id, "status": "created"}
async def handle_update_user(
self,
command: UpdateUserCommand,
current_user: dict
):
"""Handle user update command"""
# Users can update their own profile, admins can update anyone
if current_user.get("role") != "admin" and \
str(current_user.get("sub")) != str(command.user_id):
raise HTTPException(
status_code=403,
detail="Insufficient permissions"
)
# Update write database
await self.write_db.update_user(
user_id=command.user_id,
full_name=command.full_name,
role=command.role
)
# Publish event
await self.event_bus.publish("UserUpdated", {
"user_id": command.user_id,
"full_name": command.full_name,
"role": command.role,
"updated_by": current_user.get("sub"),
"timestamp": datetime.utcnow().isoformat()
})
return {"status": "updated"}
# Query Handler (Read Side)
class UserQueryHandler:
def __init__(self, read_db):
self.read_db = read_db
async def handle_get_user(
self,
query: UserQuery,
current_user: dict
):
"""Handle user query - relaxed permissions for reads"""
# Any authenticated user can read
if query.user_id:
user = await self.read_db.get_user_by_id(query.user_id)
elif query.email:
user = await self.read_db.get_user_by_email(query.email)
else:
# Filter by role if specified
users = await self.read_db.get_users(role=query.role)
return users
return user
async def handle_search_users(
self,
search_term: str,
current_user: dict,
limit: int = 10
):
"""Handle user search query"""
# Implement search with read-optimized database
results = await self.read_db.search_users(
search_term=search_term,
limit=limit
)
return results
# Event Bus for synchronizing read and write sides
class EventBus:
def __init__(self):
self.subscribers = {}
def subscribe(self, event_type: str, handler):
"""Subscribe to events"""
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(handler)
async def publish(self, event_type: str, event_data: dict):
"""Publish events to subscribers"""
if event_type in self.subscribers:
for handler in self.subscribers[event_type]:
await handler(event_data)
# Read Model Updater
class ReadModelUpdater:
def __init__(self, read_db):
self.read_db = read_db
async def handle_user_created(self, event_data: dict):
"""Update read model when user is created"""
await self.read_db.insert_user(event_data)
async def handle_user_updated(self, event_data: dict):
"""Update read model when user is updated"""
await self.read_db.update_user_view(event_data)
# FastAPI Application Setup
app_command = FastAPI(title="Command Service (Write)")
app_query = FastAPI(title="Query Service (Read)")
# Initialize components
event_bus = EventBus()
command_handler = UserCommandHandler(write_db=None, event_bus=event_bus)
query_handler = UserQueryHandler(read_db=None)
read_model_updater = ReadModelUpdater(read_db=None)
# Subscribe read model updater to events
event_bus.subscribe("UserCreated", read_model_updater.handle_user_created)
event_bus.subscribe("UserUpdated", read_model_updater.handle_user_updated)
# Command Endpoints (Write)
@app_command.post("/commands/users")
async def create_user(
command: CreateUserCommand,
current_user: dict = Depends(get_current_user_from_jwt)
):
return await command_handler.handle_create_user(command, current_user)
@app_command.put("/commands/users/{user_id}")
async def update_user(
user_id: int,
command: UpdateUserCommand,
current_user: dict = Depends(get_current_user_from_jwt)
):
command.user_id = user_id
return await command_handler.handle_update_user(command, current_user)
# Query Endpoints (Read)
@app_query.get("/queries/users")
async def get_users(
query: UserQuery = Depends(),
current_user: dict = Depends(get_current_user_from_jwt)
):
return await query_handler.handle_get_user(query, current_user)
@app_query.get("/queries/users/search")
async def search_users(
q: str,
limit: int = 10,
current_user: dict = Depends(get_current_user_from_jwt)
):
return await query_handler.handle_search_users(q, current_user, limit)PythonPattern 4: Event-Driven Architecture
Use Case: Asynchronous, loosely coupled services
sequenceDiagram
participant Client
participant Auth
participant EventBus
participant AuditService
participant EmailService
participant AnalyticsService
Client->>Auth: Login Request
Auth->>Auth: Validate Credentials
Auth->>Auth: Generate JWT
Auth->>EventBus: Publish LoginEvent
Auth->>Client: Return JWT
par Async Event Processing
EventBus->>AuditService: LoginEvent
AuditService->>AuditService: Log to Database
and
EventBus->>EmailService: LoginEvent
EmailService->>EmailService: Send Welcome Email
and
EventBus->>AnalyticsService: LoginEvent
AnalyticsService->>AnalyticsService: Update Metrics
endImplementation with RabbitMQ:
import pika
import json
from datetime import datetime
from typing import Dict, Any, Callable
import asyncio
from functools import wraps
class EventBusService:
def __init__(self, rabbitmq_url: str = "amqp://localhost"):
self.connection = pika.BlockingConnection(
pika.URLParameters(rabbitmq_url)
)
self.channel = self.connection.channel()
# Declare exchanges
self.channel.exchange_declare(
exchange='auth_events',
exchange_type='topic',
durable=True
)
self.channel.exchange_declare(
exchange='user_events',
exchange_type='topic',
durable=True
)
def publish_event(
self,
exchange: str,
routing_key: str,
event_data: Dict[str, Any]
):
"""Publish event to message bus"""
message = {
"event_id": str(uuid.uuid4()),
"timestamp": datetime.utcnow().isoformat(),
"data": event_data
}
self.channel.basic_publish(
exchange=exchange,
routing_key=routing_key,
body=json.dumps(message),
properties=pika.BasicProperties(
delivery_mode=2, # Persistent
content_type='application/json'
)
)
def subscribe_to_events(
self,
exchange: str,
routing_key: str,
callback: Callable
):
"""Subscribe to events"""
# Create queue
result = self.channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
# Bind queue to exchange
self.channel.queue_bind(
exchange=exchange,
queue=queue_name,
routing_key=routing_key
)
# Set up consumer
self.channel.basic_consume(
queue=queue_name,
on_message_callback=callback,
auto_ack=True
)
def start_consuming(self):
"""Start consuming messages"""
self.channel.start_consuming()
# Authentication Service with Events
class AuthenticationService:
def __init__(self, event_bus: EventBusService):
self.event_bus = event_bus
async def login(self, credentials: dict) -> dict:
"""Login user and publish events"""
# Validate credentials
user = await self.validate_credentials(credentials)
if not user:
# Publish failed login event
self.event_bus.publish_event(
exchange='auth_events',
routing_key='auth.login.failed',
event_data={
"email": credentials.get("email"),
"ip_address": credentials.get("ip_address"),
"reason": "Invalid credentials"
}
)
raise HTTPException(401, "Invalid credentials")
# Generate JWT
access_token = self.create_access_token(user)
refresh_token = self.create_refresh_token(user)
# Publish successful login event
self.event_bus.publish_event(
exchange='auth_events',
routing_key='auth.login.success',
event_data={
"user_id": user["id"],
"email": user["email"],
"ip_address": credentials.get("ip_address"),
"user_agent": credentials.get("user_agent"),
"timestamp": datetime.utcnow().isoformat()
}
)
return {
"access_token": access_token,
"refresh_token": refresh_token
}
async def logout(self, token: str, user_id: int):
"""Logout user and publish event"""
# Blacklist token
await self.blacklist_token(token)
# Publish logout event
self.event_bus.publish_event(
exchange='auth_events',
routing_key='auth.logout',
event_data={
"user_id": user_id,
"timestamp": datetime.utcnow().isoformat()
}
)
# Audit Service - Subscribes to auth events
class AuditService:
def __init__(self, event_bus: EventBusService, database):
self.event_bus = event_bus
self.database = database
# Subscribe to all auth events
self.event_bus.subscribe_to_events(
exchange='auth_events',
routing_key='auth.#',
callback=self.handle_auth_event
)
def handle_auth_event(self, ch, method, properties, body):
"""Handle authentication events"""
message = json.loads(body)
event_data = message["data"]
routing_key = method.routing_key
# Log to audit database
self.database.insert_audit_log({
"event_type": routing_key,
"event_id": message["event_id"],
"timestamp": message["timestamp"],
"data": event_data
})
# Check for suspicious activity
if routing_key == 'auth.login.failed':
self.check_brute_force(event_data)
def check_brute_force(self, event_data: dict):
"""Check for brute force attempts"""
email = event_data.get("email")
# Count failed attempts in last 10 minutes
failed_count = self.database.count_failed_logins(
email=email,
since=datetime.utcnow() - timedelta(minutes=10)
)
if failed_count >= 5:
# Publish security alert
self.event_bus.publish_event(
exchange='security_events',
routing_key='security.alert.brute_force',
event_data={
"email": email,
"failed_attempts": failed_count,
"ip_address": event_data.get("ip_address")
}
)
# Email Service - Subscribes to user events
class EmailService:
def __init__(self, event_bus: EventBusService):
self.event_bus = event_bus
# Subscribe to login events
self.event_bus.subscribe_to_events(
exchange='auth_events',
routing_key='auth.login.success',
callback=self.handle_login_event
)
def handle_login_event(self, ch, method, properties, body):
"""Send email on successful login"""
message = json.loads(body)
event_data = message["data"]
# Send notification email
self.send_email(
to=event_data["email"],
subject="New Login Detected",
body=f"""
A new login was detected on your account.
Time: {event_data['timestamp']}
IP Address: {event_data.get('ip_address', 'Unknown')}
Device: {event_data.get('user_agent', 'Unknown')}
If this wasn't you, please secure your account immediately.
"""
)PythonScalability Patterns
Horizontal Scaling with Stateless Architecture
graph TB
A[Load Balancer] --> B1[App Instance 1]
A --> B2[App Instance 2]
A --> B3[App Instance 3]
A --> B4[App Instance N]
B1 --> C[Shared Redis Cache]
B2 --> C
B3 --> C
B4 --> C
B1 --> D[Database Cluster]
B2 --> D
B3 --> D
B4 --> D
style B1 fill:#e1f5ff
style B2 fill:#e1f5ff
style B3 fill:#e1f5ff
style B4 fill:#e1f5ffDocker Compose Configuration:
version: '3.8'
services:
# Load Balancer
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app1
- app2
- app3
networks:
- jwt-network
# Application Instances
app1:
build: ./app
environment:
- REDIS_URL=redis://redis:6379
- DB_URL=postgresql://postgres:5432/jwtdb
- INSTANCE_ID=app1
depends_on:
- redis
- postgres
networks:
- jwt-network
app2:
build: ./app
environment:
- REDIS_URL=redis://redis:6379
- DB_URL=postgresql://postgres:5432/jwtdb
- INSTANCE_ID=app2
depends_on:
- redis
- postgres
networks:
- jwt-network
app3:
build: ./app
environment:
- REDIS_URL=redis://redis:6379
- DB_URL=postgresql://postgres:5432/jwtdb
- INSTANCE_ID=app3
depends_on:
- redis
- postgres
networks:
- jwt-network
# Shared Cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- jwt-network
# Database Cluster
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=jwtdb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secure_password
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- jwt-network
# Message Queue
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=secure_password
networks:
- jwt-network
volumes:
redis-data:
postgres-data:
networks:
jwt-network:
driver: bridgeYAMLNginx Load Balancer Configuration:
upstream app_servers {
least_conn; # Use least connections algorithm
server app1:8000 max_fails=3 fail_timeout=30s;
server app2:8000 max_fails=3 fail_timeout=30s;
server app3:8000 max_fails=3 fail_timeout=30s;
# Health check
check interval=3000 rise=2 fall=3 timeout=1000;
}
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/s;
limit_req_zone $http_authorization zone=token_limit:10m rate=100r/s;
server {
listen 80;
server_name api.example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# Authentication endpoints with strict rate limiting
location /auth/ {
limit_req zone=auth_limit burst=5 nodelay;
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
# Protected API endpoints
location /api/ {
limit_req zone=token_limit burst=20 nodelay;
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Caching for GET requests
proxy_cache api_cache;
proxy_cache_valid 200 5m;
proxy_cache_key "$scheme$request_method$host$request_uri$http_authorization";
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://app_servers;
}
}NginxSecurity Architecture Patterns
Defense in Depth Strategy
graph TB
A[Client] --> B[WAF - Web Application Firewall]
B --> C[DDoS Protection]
C --> D[Load Balancer with SSL]
D --> E[API Gateway - Rate Limiting]
E --> F[JWT Validation]
F --> G[Application Security]
G --> H[Database Encryption]
I[Monitoring & Alerts] -.-> B
I -.-> C
I -.-> D
I -.-> E
I -.-> F
I -.-> G
I -.-> HMulti-Layer Security Implementation:
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import hashlib
import hmac
from typing import Optional
from datetime import datetime, timedelta
class SecurityMiddleware:
"""Multi-layer security middleware"""
def __init__(self, app: FastAPI):
self.app = app
self.setup_security_layers()
def setup_security_layers(self):
# Layer 1: CORS Protection
self.app.add_middleware(
CORSMiddleware,
allow_origins=["https://trusted-domain.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
max_age=3600,
)
# Layer 2: Trusted Host Protection
self.app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "*.example.com"]
)
# Layer 3: Rate Limiting
limiter = Limiter(key_func=get_remote_address)
self.app.state.limiter = limiter
self.app.add_exception_handler(
RateLimitExceeded,
_rate_limit_exceeded_handler
)
# Layer 4: Request Signature Verification
@self.app.middleware("http")
async def verify_request_signature(request: Request, call_next):
if request.url.path.startswith("/api/critical/"):
if not self.verify_signature(request):
return JSONResponse(
status_code=401,
content={"detail": "Invalid request signature"}
)
response = await call_next(request)
return response
# Layer 5: JWT Token Validation
@self.app.middleware("http")
async def validate_jwt_middleware(request: Request, call_next):
if request.url.path.startswith("/api/"):
token = self.extract_token(request)
if token and not self.is_token_blacklisted(token):
payload = self.verify_jwt(token)
if payload:
request.state.user = payload
else:
return JSONResponse(
status_code=401,
content={"detail": "Invalid token"}
)
response = await call_next(request)
return response
# Layer 6: Audit Logging
@self.app.middleware("http")
async def audit_logging_middleware(request: Request, call_next):
start_time = datetime.utcnow()
response = await call_next(request)
# Log request
self.log_request(
path=request.url.path,
method=request.method,
user=getattr(request.state, "user", None),
status_code=response.status_code,
duration=(datetime.utcnow() - start_time).total_seconds()
)
return response
def verify_signature(self, request: Request) -> bool:
"""Verify HMAC signature of request"""
signature = request.headers.get("X-Signature")
timestamp = request.headers.get("X-Timestamp")
if not signature or not timestamp:
return False
# Check timestamp (prevent replay attacks)
try:
request_time = datetime.fromisoformat(timestamp)
if abs((datetime.utcnow() - request_time).total_seconds()) > 300:
return False
except:
return False
# Verify signature
expected_signature = hmac.new(
SIGNATURE_SECRET.encode(),
f"{request.method}{request.url.path}{timestamp}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Initialize app with security layers
app = FastAPI()
security = SecurityMiddleware(app)PythonHigh Availability Patterns
Active-Active Deployment
graph TB
subgraph "Region 1 - US East"
A1[Load Balancer] --> B1[App Cluster]
B1 --> C1[DB Primary]
B1 --> D1[Redis Cluster]
end
subgraph "Region 2 - US West"
A2[Load Balancer] --> B2[App Cluster]
B2 --> C2[DB Primary]
B2 --> D2[Redis Cluster]
end
E[Global Load Balancer] --> A1
E --> A2
C1 <-.Replication.-> C2
D1 <-.Sync.-> D2Kubernetes Deployment Configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: jwt-auth-service
labels:
app: jwt-auth
spec:
replicas: 3
selector:
matchLabels:
app: jwt-auth
template:
metadata:
labels:
app: jwt-auth
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- jwt-auth
topologyKey: kubernetes.io/hostname
containers:
- name: auth-service
image: your-registry/jwt-auth:latest
ports:
- containerPort: 8000
env:
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: app-config
key: redis.url
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database.url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt.secret
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
# Graceful shutdown
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: jwt-auth-service
spec:
type: LoadBalancer
selector:
app: jwt-auth
ports:
- protocol: TCP
port: 80
targetPort: 8000
sessionAffinity: None
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: jwt-auth-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: jwt-auth-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: MaxYAMLMonitoring and Observability Architecture
graph TB
subgraph "Application Layer"
A[Auth Service]
B[User Service]
C[API Gateway]
end
subgraph "Observability Stack"
D[Metrics Collector - Prometheus]
E[Log Aggregator - Loki]
F[Tracing - Jaeger]
end
subgraph "Visualization & Alerting"
G[Grafana Dashboards]
H[Alert Manager]
end
A --> D
A --> E
A --> F
B --> D
B --> E
B --> F
C --> D
C --> E
C --> F
D --> G
E --> G
F --> G
D --> H
E --> HPrometheus Metrics Implementation:
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from fastapi import FastAPI, Response
import time
from functools import wraps
# Define metrics
jwt_tokens_generated = Counter(
'jwt_tokens_generated_total',
'Total number of JWT tokens generated',
['token_type']
)
jwt_tokens_validated = Counter(
'jwt_tokens_validated_total',
'Total number of JWT tokens validated',
['status']
)
jwt_validation_duration = Histogram(
'jwt_validation_duration_seconds',
'Time spent validating JWT tokens',
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]
)
active_sessions = Gauge(
'active_sessions',
'Number of active user sessions'
)
failed_login_attempts = Counter(
'failed_login_attempts_total',
'Total number of failed login attempts',
['reason']
)
# Instrumented JWT Handler
class InstrumentedJWTHandler:
def create_access_token(self, data: dict) -> str:
"""Create access token with metrics"""
start_time = time.time()
try:
token = self._generate_token(data, "access")
jwt_tokens_generated.labels(token_type='access').inc()
active_sessions.inc()
return token
finally:
duration = time.time() - start_time
jwt_validation_duration.observe(duration)
def verify_token(self, token: str) -> Optional[dict]:
"""Verify token with metrics"""
start_time = time.time()
try:
payload = self._verify_token_internal(token)
if payload:
jwt_tokens_validated.labels(status='valid').inc()
else:
jwt_tokens_validated.labels(status='invalid').inc()
return payload
finally:
duration = time.time() - start_time
jwt_validation_duration.observe(duration)
def handle_failed_login(self, reason: str):
"""Track failed login attempts"""
failed_login_attempts.labels(reason=reason).inc()
# Metrics endpoint
@app.get("/metrics")
async def metrics():
return Response(
content=generate_latest(),
media_type="text/plain"
)PythonBest Practices Summary
Architectural Best Practices
- Stateless Design: Keep application servers stateless for easy scaling
- Separation of Concerns: Divide responsibilities clearly across services
- Idempotency: Design operations to be safely retryable
- Circuit Breakers: Prevent cascading failures
- Health Checks: Implement comprehensive health monitoring
- Graceful Degradation: Handle failures gracefully
- Caching Strategy: Cache aggressively where appropriate
- Event-Driven: Use asynchronous communication for non-critical operations
Security Best Practices
- Defense in Depth: Implement multiple security layers
- Least Privilege: Grant minimum necessary permissions
- Token Rotation: Regularly rotate cryptographic keys
- Audit Everything: Comprehensive logging and monitoring
- Secure Communication: Always use TLS/SSL
- Input Validation: Validate all inputs rigorously
- Rate Limiting: Protect against abuse
- Secrets Management: Use secure secret storage (Vault, AWS Secrets Manager)
Scalability Best Practices
- Horizontal Scaling: Design for adding more instances
- Database Optimization: Use connection pooling, read replicas
- Caching Layers: Implement multi-tier caching
- Asynchronous Processing: Use message queues for heavy operations
- Content Delivery: Use CDNs for static content
- Load Testing: Regularly test system limits
- Auto-Scaling: Implement automatic scaling policies
- Performance Monitoring: Continuous performance tracking
25. Summary and Best Practices {#25-summary-and-best-practices}
Overview
This comprehensive guide has covered JWT authentication from fundamentals to enterprise architecture. This chapter consolidates the key learnings, best practices, and critical security considerations into actionable guidelines.
Core Concepts Recap
What We’ve Learned
mindmap
root((JWT Mastery))
Fundamentals
Structure (Header.Payload.Signature)
Claims & Payload
Cryptographic Algorithms
Token Lifecycle
Implementation
FastAPI Backend
JavaScript Frontend
Token Storage
RBAC Authorization
Security
JWE Encryption
OAuth 2.0 Integration
Key Rotation
Token Revocation
Production
Deployment Strategies
Monitoring & Logging
Performance Optimization
Testing QA
Enterprise
Microservices
System Design
Architecture Patterns
Future StandardsSecurity Best Practices Checklist
✅ Critical Security Requirements
1. Algorithm Security
# ✅ DO: Use strong algorithms
ALGORITHM = "RS256" # or ES256
KEY_SIZE = 2048 # minimum
# ❌ DON'T: Use weak algorithms
ALGORITHM = "none" # NEVER!
ALGORITHM = "HS256" # Only for single servicePython2. Token Expiration
# ✅ DO: Set appropriate expiration times
ACCESS_TOKEN_EXPIRE_MINUTES = 15 # Short-lived
REFRESH_TOKEN_EXPIRE_DAYS = 7 # Longer-lived
# ❌ DON'T: Use excessively long expiration
ACCESS_TOKEN_EXPIRE_MINUTES = 43200 # 30 days - Too long!Python3. Secret Key Management
# ✅ DO: Use strong, random secrets
SECRET_KEY = secrets.token_urlsafe(32)
# ❌ DON'T: Use weak or hardcoded secrets
SECRET_KEY = "mysecret" # Too weak!Python4. Token Validation
# ✅ DO: Validate all claims
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="your-api",
issuer="your-auth-server",
options={
"verify_exp": True,
"verify_aud": True,
"verify_iss": True
}
)
# ❌ DON'T: Skip validation
payload = jwt.decode(
token,
options={"verify_signature": False} # Dangerous!
)Python5. Secure Storage
// ✅ DO: Use HttpOnly cookies for sensitive tokens
document.cookie = `refresh_token=${token}; HttpOnly; Secure; SameSite=Strict`;
// ❌ DON'T: Store sensitive tokens in localStorage
localStorage.setItem('refresh_token', token); // Vulnerable to XSSJavaScriptArchitecture Decision Matrix
Choose the right architecture based on your requirements:
| Requirement | Pattern | When to Use | Chapter Reference |
|---|---|---|---|
| Single service app | HS256 | Simple apps, single backend | Chapter 4 |
| Microservices | RS256/ES256 | Distributed systems | Chapter 20 |
| High security | JWE + RS256 | Financial, healthcare | Chapter 11 |
| Third-party auth | OAuth 2.0 | Social login, SSO | Chapter 12 |
| Multi-tenant | Tenant isolation | SaaS platforms | Extended Patterns |
| Legacy migration | Strangler Fig | Gradual migration | Extended Patterns |
| Mobile apps | Token binding | Native apps | Extended Patterns |
| Zero trust | Continuous auth | Enterprise security | Extended Patterns |
Performance Optimization Guidelines
Token Size Optimization
Problem: Large tokens increase bandwidth and latency
Solutions:
- Minimize Claims
// ✅ Good: Essential claims only (≈200 bytes)
{
"sub": "123",
"role": "user",
"exp": 1697648400
}
// ❌ Bad: Excessive data (≈2000 bytes)
{
"sub": "123",
"user_profile": { /* large object */ },
"preferences": { /* large object */ },
"history": [ /* large array */ ]
}
JSON2. Use Short Claim Names
// ✅ Optimized
{"sub": "123", "r": "admin", "p": ["read", "write"]}
// ❌ Verbose
{"subject": "123", "role": "admin", "permissions": ["read", "write"]}JSON- Reference Pattern
// ✅ Reference to data
{"sub": "123", "profile_id": "abc"}
// ❌ Embed full data
{"sub": "123", "profile": { /* 1KB of data */ }}JSONCaching Strategy
# Redis caching for token validation
class CachedTokenValidator:
def __init__(self):
self.redis = redis.Redis()
self.cache_ttl = 300 # 5 minutes
async def validate_token(self, token: str) -> dict:
# Check cache first
cache_key = f"token:{hashlib.sha256(token.encode()).hexdigest()}"
cached_result = self.redis.get(cache_key)
if cached_result:
return json.loads(cached_result)
# Validate token
payload = jwt.decode(token, public_key, algorithms=["RS256"])
# Cache result
self.redis.setex(
cache_key,
self.cache_ttl,
json.dumps(payload)
)
return payloadPythonConnection Pooling
# Database connection pooling
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
engine = create_engine(
DATABASE_URL,
poolclass=QueuePool,
pool_size=20,
max_overflow=40,
pool_pre_ping=True,
pool_recycle=3600
)PythonTesting Strategy
Unit Tests
# Test token creation
def test_create_token():
handler = JWTHandler()
payload = {"sub": "user123", "role": "admin"}
token = handler.create_access_token(payload)
assert token is not None
decoded = handler.verify_token(token)
assert decoded["sub"] == "user123"
assert decoded["role"] == "admin"
# Test token expiration
def test_expired_token():
handler = JWTHandler()
payload = {"sub": "user123", "exp": datetime.utcnow() - timedelta(hours=1)}
token = jwt.encode(payload, handler.secret_key, algorithm="RS256")
decoded = handler.verify_token(token)
assert decoded is None
# Test signature verification
def test_invalid_signature():
handler = JWTHandler()
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature"
decoded = handler.verify_token(token)
assert decoded is NonePythonIntegration Tests
# Test authentication flow
async def test_authentication_flow():
# Register
response = await client.post("/auth/register", json={
"email": "test@example.com",
"password": "password123",
"full_name": "Test User"
})
assert response.status_code == 201
# Login
response = await client.post("/auth/login", data={
"username": "test@example.com",
"password": "password123"
})
assert response.status_code == 200
tokens = response.json()
# Access protected route
response = await client.get(
"/users/profile",
headers={"Authorization": f"Bearer {tokens['access_token']}"}
)
assert response.status_code == 200
# Refresh token
response = await client.post("/auth/refresh", json={
"refresh_token": tokens['refresh_token']
})
assert response.status_code == 200PythonLoad Testing
# Locust load test
from locust import HttpUser, task, between
class JWTAuthUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
# Login and get token
response = self.client.post("/auth/login", data={
"username": "test@example.com",
"password": "password123"
})
self.token = response.json()["access_token"]
@task(3)
def get_profile(self):
self.client.get(
"/users/profile",
headers={"Authorization": f"Bearer {self.token}"}
)
@task(1)
def get_protected_data(self):
self.client.get(
"/users/protected-data",
headers={"Authorization": f"Bearer {self.token}"}
)PythonMonitoring and Observability
Key Metrics to Track
from prometheus_client import Counter, Histogram, Gauge
# Token metrics
token_created = Counter('jwt_tokens_created_total', 'Total tokens created')
token_validated = Counter('jwt_tokens_validated_total', 'Total tokens validated')
token_expired = Counter('jwt_tokens_expired_total', 'Total expired tokens')
token_invalid = Counter('jwt_tokens_invalid_total', 'Total invalid tokens')
# Performance metrics
token_validation_duration = Histogram(
'jwt_validation_duration_seconds',
'Token validation duration'
)
active_sessions = Gauge('jwt_active_sessions', 'Number of active sessions')
# Implementation
@token_validation_duration.time()
async def validate_token(token: str):
try:
payload = jwt.decode(token, public_key, algorithms=["RS256"])
token_validated.inc()
return payload
except jwt.ExpiredSignatureError:
token_expired.inc()
return None
except jwt.InvalidTokenError:
token_invalid.inc()
return NonePythonLogging Best Practices
import logging
import json
# Structured logging
logger = logging.getLogger(__name__)
def log_authentication_event(event_type: str, user_id: str, **kwargs):
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"event_type": event_type,
"user_id": user_id,
"ip_address": kwargs.get("ip_address"),
"user_agent": kwargs.get("user_agent"),
"success": kwargs.get("success", True),
"error": kwargs.get("error")
}
if log_entry["success"]:
logger.info(json.dumps(log_entry))
else:
logger.warning(json.dumps(log_entry))
# Usage
log_authentication_event(
"login",
user_id="123",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0...",
success=True
)PythonCommon Pitfalls and Solutions
1. Clock Skew Issues
Problem: Token validation fails due to time differences between servers
Solution:
# Add clock skew tolerance
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
options={"verify_exp": True},
leeway=timedelta(seconds=10) # Allow 10 seconds skew
)Python2. Token Size in URLs
Problem: Tokens in URL query parameters get truncated
Solution:
// ❌ DON'T: Put tokens in URLs
fetch(`/api/data?token=${token}`);
// ✅ DO: Use Authorization header
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});JavaScript3. Missing Token Revocation
Problem: Logout doesn’t actually invalidate tokens
Solution:
# Implement token blacklist
class TokenBlacklist:
def __init__(self):
self.redis = redis.Redis()
async def revoke_token(self, token: str):
# Decode to get expiration
payload = jwt.decode(token, options={"verify_signature": False})
exp = payload.get("exp")
if exp:
ttl = exp - int(datetime.utcnow().timestamp())
if ttl > 0:
token_hash = hashlib.sha256(token.encode()).hexdigest()
self.redis.setex(f"blacklist:{token_hash}", ttl, "1")
async def is_revoked(self, token: str) -> bool:
token_hash = hashlib.sha256(token.encode()).hexdigest()
return self.redis.exists(f"blacklist:{token_hash}") > 0Python4. CORS Configuration
Problem: Frontend can’t access API due to CORS errors
Solution:
# Proper CORS configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"https://admin.example.com"
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
expose_headers=["X-Total-Count"],
max_age=3600
)Python5. Refresh Token Rotation
Problem: Refresh tokens never expire, security risk
Solution:
# Implement refresh token rotation
async def refresh_access_token(refresh_token: str):
# Validate refresh token
payload = verify_token(refresh_token)
if not payload or payload.get("type") != "refresh":
raise HTTPException(status_code=401)
# Create new access token
new_access_token = create_access_token({
"sub": payload["sub"],
"role": payload["role"]
})
# Create new refresh token (rotation)
new_refresh_token = create_refresh_token({
"sub": payload["sub"]
})
# Revoke old refresh token
await blacklist_token(refresh_token)
return {
"access_token": new_access_token,
"refresh_token": new_refresh_token
}PythonProduction Deployment Checklist
Pre-Deployment
- Security Audit
- Review all algorithms and key sizes
- Verify secret key strength (min 256 bits)
- Test token expiration policies
- Validate all input parameters
- Enable HTTPS/TLS everywhere
- Performance Testing
- Load test authentication endpoints
- Measure token validation latency
- Test under concurrent users
- Verify database connection pooling
- Check Redis/cache performance
- Infrastructure
- Set up key rotation mechanism
- Configure backup and recovery
- Enable monitoring and alerting
- Set up log aggregation
- Configure auto-scaling
Post-Deployment
- Monitoring
- Track token creation/validation rates
- Monitor error rates
- Watch for security anomalies
- Track response times
- Monitor cache hit rates
- Security
- Enable rate limiting
- Set up intrusion detection
- Monitor failed login attempts
- Review audit logs
- Scan for vulnerabilities
Future-Proofing Your JWT Implementation
Stay Updated with Standards
Current Standards:
- RFC 7519: JWT
- RFC 7515: JWS
- RFC 7516: JWE
- RFC 7517: JWK
- RFC 7518: JWA
- RFC 8693: Token Exchange
- RFC 7662: Token Introspection
Emerging Standards:
- DPoP (RFC 9449): Demonstrating Proof-of-Possession
- SD-JWT: Selective Disclosure JWT
- WebAuthn: Passwordless authentication
- FIDO2: Strong authentication
Adopt Modern Practices
# 1. Implement DPoP for enhanced security
class DPoPValidator:
async def validate_dpop_proof(
self,
dpop_proof: str,
http_method: str,
http_uri: str
) -> bool:
# Validate DPoP proof
try:
payload = jwt.decode(dpop_proof, options={"verify_signature": True})
# Verify HTTP method and URI
if payload.get("htm") != http_method:
return False
if payload.get("htu") != http_uri:
return False
# Verify timestamp
iat = payload.get("iat")
if abs(datetime.utcnow().timestamp() - iat) > 60:
return False
return True
except:
return False
# 2. Implement Selective Disclosure
class SelectiveDisclosureJWT:
def create_sd_jwt(self, claims: dict, disclosable_claims: list) -> str:
# Create SD-JWT with selective disclosure
sd_claims = {}
disclosure_map = {}
for claim in disclosable_claims:
if claim in claims:
salt = secrets.token_urlsafe(16)
disclosure = f"{salt}.{claim}.{json.dumps(claims[claim])}"
disclosure_hash = hashlib.sha256(disclosure.encode()).hexdigest()
sd_claims[f"_sd_{claim}"] = disclosure_hash
disclosure_map[claim] = disclosure
# Create JWT with hashed claims
token = jwt.encode(sd_claims, private_key, algorithm="RS256")
return token, disclosure_mapPythonLearning Path Summary
Beginner → Expert Journey
Phase 1: Foundations (Weeks 1-2)
- ✅ Understand JWT structure
- ✅ Learn basic authentication flow
- ✅ Implement simple FastAPI backend
- ✅ Build basic frontend integration
Phase 2: Intermediate (Weeks 3-4)
- ✅ Implement RBAC
- ✅ Add token refresh mechanism
- ✅ Secure token storage
- ✅ Error handling and validation
Phase 3: Advanced (Weeks 5-6)
- ✅ JWE encryption
- ✅ OAuth 2.0 integration
- ✅ Key rotation
- ✅ Token revocation
Phase 4: Production (Weeks 7-8)
- ✅ Deployment strategies
- ✅ Monitoring and logging
- ✅ Performance optimization
- ✅ Comprehensive testing
Phase 5: Enterprise (Weeks 9-12)
- ✅ Microservices architecture
- ✅ System design patterns
- ✅ Zero trust implementation
- ✅ Future standards adoption
Quick Reference Cards
1. Token Creation
# FastAPI Token Creation
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(minutes=15)
}
return jwt.encode(payload, private_key, algorithm="RS256")Python2. Token Validation
# FastAPI Token Validation
def validate_token(token: str) -> dict:
try:
return jwt.decode(
token,
public_key,
algorithms=["RS256"],
options={"verify_exp": True}
)
except jwt.InvalidTokenError:
raise HTTPException(status_code=401)Python3. Frontend Integration
// JavaScript Token Usage
class AuthClient {
async login(email, password) {
const response = await fetch('/auth/login', {
method: 'POST',
body: new FormData({username: email, password})
});
const {access_token} = await response.json();
localStorage.setItem('token', access_token);
}
async fetchProtected(url) {
return fetch(url, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
}
}JavaScriptFinal Recommendations
Do’s ✅
- Always use HTTPS in production
- Implement proper token expiration
- Use strong cryptographic algorithms (RS256, ES256)
- Validate all token claims
- Implement refresh token rotation
- Monitor and log authentication events
- Use HttpOnly cookies for refresh tokens
- Implement rate limiting
- Regular security audits
- Keep dependencies updated
Don’ts ❌
- Never use the “none” algorithm
- Don’t store sensitive data in tokens
- Don’t use weak secrets
- Don’t skip signature verification
- Don’t trust client-side validation alone
- Don’t use excessively long expiration times
- Don’t expose tokens in URLs
- Don’t forget to implement logout
- Don’t ignore error handling
- Don’t deploy without testing
Conclusion
This comprehensive guide has equipped you with the knowledge and tools to implement JWT authentication from basic concepts to enterprise-grade systems. Key achievements:
🎯 Technical Mastery
- Deep understanding of JWT structure and cryptography
- Production-ready FastAPI and JavaScript implementations
- Advanced security patterns and best practices
- Enterprise architecture and system design
🔒 Security Excellence
- Zero-trust architecture patterns
- Token binding and DPoP
- Multi-factor authentication
- Comprehensive threat mitigation
⚡ Performance & Scale
- Microservices integration
- Caching and optimization
- Load balancing strategies
- High-availability patterns
🚀 Future Ready
- Emerging standards (SD-JWT, DPoP, WebAuthn)
- Cloud-native deployments
- Kubernetes orchestration
- GitOps workflows
Next Steps
- Practice: Implement the examples in your projects
- Experiment: Try different architectural patterns
- Contribute: Share your learnings with the community
- Stay Updated: Follow JWT and OAuth standards evolution
- Secure: Regular security audits and updates
Additional Resources
Official Specifications:
- https://jwt.io – JWT Debugger and Resources
- https://datatracker.ietf.org/doc/html/rfc7519 – JWT RFC
- https://oauth.net/2/ – OAuth 2.0 Specifications
Security Resources:
- OWASP JWT Security Cheat Sheet
- NIST Cryptographic Standards
- CWE Top 25 Security Weaknesses
Community:
- FastAPI Discord Community
- JWT.io Slack Channel
- Stack Overflow JWT Tag
Thank you for completing this comprehensive JWT journey! 🎉
You now have the expertise to build secure, scalable, and production-ready authentication systems. Remember: security is a continuous process, not a destination. Keep learning, stay vigilant, and build amazing applications!
End of Guide
Discover more from Altgr Blog
Subscribe to get the latest posts sent to your email.
