JWT Authentication Service — Capstone Project for Token-Based Security
In this tutorial, you will learn about JWT Authentication Service. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete JWT authentication service supporting multiple signing algorithms, refresh token rotation, JWKS key distribution, blacklisting, and multi-service audience validation.
Project Overview
You will build a Flask-based JWT authentication service (Auth Hub) that issues tokens for three internal APIs. The service must support service-to-service auth (RS256 with JWKS) and user auth (HS256 with refresh rotation).
flowchart TD
A["Auth Hub Service"] --> B["/api/v1/login\nIssue HS256 tokens"]
A --> C["/api/v1/refresh\nRotate tokens"]
A --> D["/api/v1/oauth/token\nIssue RS256 tokens\n(Client Credentials)"]
A --> E["/.well-known/jwks.json\nPublish public keys"]
A --> F["/api/v1/revoke\nBlacklist tokens"]
B --> G["User API\naud: user-api"]
C --> G
D --> H["Threat API\naud: threat-api"]
D --> I["Device API\naud: device-api"]
style A fill:#dbeafe,stroke:#2563eb
style G fill:#dcfce7,stroke:#16a34a
style H fill:#dcfce7,stroke:#16a34a
style I fill:#dcfce7,stroke:#16a34a
Requirements
1. User Authentication (HS256)
POST /api/v1/login— Accept username/password, return access token (15 min) + refresh token (7 days)POST /api/v1/refresh— Accept refresh token, rotate both access and refresh tokensPOST /api/v1/logout— Blacklist the access token and invalidate the refresh tokenGET /api/v1/me— Return user info from the access token claims
2. Service Authentication (RS256 + JWKS)
POST /api/v1/oauth/token— Client Credentials grant, return RS256 signed access tokenGET /.well-known/jwks.json— Return public keys for RS256 verification- Support multiple service clients with different audience values
3. Token Blacklisting (Redis)
- Blacklist access tokens on logout
- Blacklist refresh tokens on rotation (old token no longer valid)
- Automatic TTL cleanup (blacklist entries expire when the token would have expired)
4. Multi-Service Audience
- Token
audclaim specifies which API the token is for - Three APIs: user-api, threat-api, device-api
- Tokens for one API cannot access another
Starter Code
from flask import Flask, request, jsonify
import jwt, redis, datetime, secrets, hashlib
app = Flask(__name__)
r = redis.Redis(decode_responses=True)
# Configuration
HS256_SECRET = secrets.token_hex(32)
API_AUDIENCES = ["user-api", "threat-api", "device-api"]
# In-memory user store (use database in production)
USERS = {"admin": {"password": "pass123", "role": "admin"}}
# RS256 key pair (generate on startup)
# See jwt-signing lesson for key generation
# === YOUR IMPLEMENTATION ===
# 1. POST /api/v1/login
# 2. POST /api/v1/refresh
# 3. POST /api/v1/logout
# 4. POST /api/v1/oauth/token
# 5. GET /.well-known/jwks.json
# 6. GET /api/v1/me
# 7. Token validation middleware
def require_jwt(allowed_audiences=None):
"""Middleware that validates JWT and checks audience."""
def decorator(f):
def wrapper(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"error": "Missing token"}), 401
token = auth[7:]
# Determine algorithm from header
header = jwt.get_unverified_header(token)
alg = header.get("alg")
try:
if alg == "HS256":
payload = jwt.decode(
token, HS256_SECRET,
algorithms=["HS256"]
)
elif alg == "RS256":
# Fetch key from JWKS
key = get_key_from_jwks(header.get("kid"))
if not key:
return jsonify({"error": "Unknown key"}), 401
payload = jwt.decode(
token, key, algorithms=["RS256"]
)
else:
return jsonify({"error": "Unsupported algorithm"}), 401
# Check blacklist
jti = payload.get("jti")
if jti and r.exists(f"blacklist:{jti}"):
return jsonify({"error": "Token revoked"}), 401
# Check audience
if allowed_audiences:
aud = payload.get("aud")
if aud not in allowed_audiences:
return jsonify({"error": "Wrong audience"}), 403
g.user = payload
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return f(*args, **kwargs)
return wrapper
return decorator
if __name__ == "__main__":
app.run(port=5000)
Testing Your Project
# Test user login
curl -X POST -H "Content-Type: application/json" \
-d '{"username":"admin","password":"pass123","audience":"user-api"}' \
http://localhost:5000/api/v1/login
# Test service auth
curl -X POST -H "Content-Type: application/json" \
-d '{"client_id":"threat-service","client_secret":"secret","audience":"threat-api"}' \
http://localhost:5000/api/v1/oauth/token
# Fetch JWKS
curl http://localhost:5000/.well-known/jwks.json
Common Mistakes
1. Using the Same Secret for HS256 and RS256
Never use the same key material for both algorithms. This enables algorithm confusion attacks.
2. Not Validating Token Type
Ensure access tokens cannot be used as refresh tokens and vice versa. Include token_type in claims.
3. Exposing Private Keys in JWKS
The JWKS endpoint must contain only public keys. Never expose private keys.
4. Not Handling Token Expiry in Middleware
The middleware must check exp and return a specific error so clients can refresh.
5. Missing Audience Validation
Without audience, a threat-api token could access the user-api. Always validate audience.
FAQ
What's Next
Congratulations on completing the JWT learning path! Continue to the OAuth2 Deep Dive guide for delegated authorization frameworks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro