Secure JWT Implementation Guide
This guide provides practical advice for implementing JWT authentication securely in your applications. It covers best practices, common pitfalls, and code examples for various programming languages.
While this guide aims to be comprehensive, security is a complex and evolving field. Always stay updated with the latest security recommendations and adapt these practices to your specific security requirements and threat model.
Table of Contents
JWT Authentication Flow
A typical JWT authentication flow consists of the following steps:
- User Login: User provides credentials (username/password)
- Token Generation: Server validates credentials and generates a JWT
- Token Storage: Client stores the JWT (localStorage, cookies, etc.)
- Authenticated Requests: Client includes the JWT in subsequent requests
- Token Verification: Server verifies the JWT for each protected request
- Response: Server processes the request if the JWT is valid
Recommended Flow with Refresh Tokens
For enhanced security, implement a refresh token system:
- User Login: User provides credentials
- Token Generation: Server generates both an access token (short-lived JWT) and a refresh token (longer-lived, stored securely)
- Token Usage: Client uses the access token for API requests
- Token Refresh: When the access token expires, client uses the refresh token to obtain a new access token without requiring re-authentication
- Token Revocation: Refresh tokens can be revoked to invalidate all derived access tokens
Secure JWT Configuration
Token Payload
Include only necessary claims in your JWT payload:
{
"sub": "user123", // Subject (user identifier)
"iat": 1516239022, // Issued at (timestamp)
"exp": 1516242622, // Expiration time (timestamp)
"aud": "my-api", // Audience (intended recipient)
"iss": "my-auth-server", // Issuer (who created the token)
"jti": "unique-id-123" // JWT ID (unique identifier for this token)
}
Security Parameters
Token Expiration
- Access tokens: 15-30 minutes
- Refresh tokens: 1-7 days (stored securely)
Algorithm Selection
- Prefer asymmetric algorithms (RS256, ES256) over symmetric ones (HS256)
- Avoid using "none" algorithm (disable it in your library)
Key Management
- For symmetric algorithms: Use a strong, randomly generated secret (at least 256 bits)
- For asymmetric algorithms: Use proper key lengths (RSA: 2048+ bits, EC: P-256 or higher)
- Rotate keys periodically
- Use a secure key management service when possible
Token Storage (Client-side)
- Prefer HttpOnly cookies with Secure and SameSite flags for web applications
- If using localStorage/sessionStorage, be aware of XSS risks
- Mobile apps: Use secure storage mechanisms provided by the platform
Implementation Examples
Node.js Implementation
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// Use a strong secret or asymmetric keys
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || crypto.randomBytes(32).toString('hex');
// Token blacklist (use Redis in production)
const tokenBlacklist = new Set();
const refreshTokens = new Map();
// Generate token pair
function generateTokens(user) {
const payload = {
sub: user.id,
username: user.username,
role: user.role,
permissions: user.permissions
};
const accessToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: '15m',
issuer: 'secure-app',
audience: 'secure-app-users',
jwtid: crypto.randomUUID()
});
const refreshToken = crypto.randomBytes(32).toString('hex');
refreshTokens.set(refreshToken, {
userId: user.id,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days
});
return { accessToken, refreshToken };
}
// Middleware to validate access tokens
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET, {
issuer: 'secure-app',
audience: 'secure-app-users'
});
// Check if token is blacklisted
if (tokenBlacklist.has(decoded.jti)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { username, password } = req.body;
try {
// Validate credentials (replace with your user validation)
const user = await validateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user);
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
accessToken: tokens.accessToken,
user: {
id: user.id,
username: user.username,
role: user.role
}
});
} catch (err) {
res.status(500).json({ error: 'Login failed' });
}
});
// Refresh token endpoint
app.post('/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken || !refreshTokens.has(refreshToken)) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const tokenData = refreshTokens.get(refreshToken);
if (Date.now() > tokenData.expiresAt) {
refreshTokens.delete(refreshToken);
return res.status(401).json({ error: 'Refresh token expired' });
}
// Generate new tokens
const user = getUserById(tokenData.userId);
const tokens = generateTokens(user);
// Remove old refresh token
refreshTokens.delete(refreshToken);
// Set new refresh token
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken: tokens.accessToken });
});
// Logout endpoint
app.post('/auth/logout', authenticateToken, (req, res) => {
// Add token to blacklist
tokenBlacklist.add(req.user.jti);
// Remove refresh token
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
refreshTokens.delete(refreshToken);
}
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
// Protected route example
app.get('/protected', authenticateToken, (req, res) => {
res.json({
message: 'This is a protected route',
user: req.user
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Python Implementation (Flask)
from flask import Flask, request, jsonify, make_response
import jwt
import bcrypt
from datetime import datetime, timedelta
import os
import secrets
import redis
app = Flask(__name__)
# Configuration
JWT_SECRET = os.environ.get('JWT_SECRET', secrets.token_hex(32))
JWT_ALGORITHM = 'HS256'
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=15)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7)
# Redis for token blacklist (use in-memory dict for demo)
redis_client = redis.Redis(host='localhost', port=6379, db=0)
token_blacklist = set()
refresh_tokens = {}
class TokenManager:
@staticmethod
def generate_tokens(user):
"""Generate access and refresh tokens"""
payload = {
'sub': user['id'],
'username': user['username'],
'role': user['role'],
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + JWT_ACCESS_TOKEN_EXPIRES,
'iss': 'secure-app',
'aud': 'secure-app-users',
'jti': secrets.token_hex(16)
}
access_token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
refresh_token = secrets.token_hex(32)
# Store refresh token
refresh_tokens[refresh_token] = {
'user_id': user['id'],
'expires_at': datetime.utcnow() + JWT_REFRESH_TOKEN_EXPIRES
}
return access_token, refresh_token
@staticmethod
def verify_token(token):
"""Verify and decode access token"""
try:
payload = jwt.decode(
token,
JWT_SECRET,
algorithms=[JWT_ALGORITHM],
issuer='secure-app',
audience='secure-app-users'
)
# Check if token is blacklisted
if payload['jti'] in token_blacklist:
raise jwt.InvalidTokenError('Token has been revoked')
return payload
except jwt.ExpiredSignatureError:
raise jwt.InvalidTokenError('Token has expired')
except jwt.InvalidTokenError:
raise jwt.InvalidTokenError('Invalid token')
@staticmethod
def blacklist_token(jti):
"""Add token to blacklist"""
token_blacklist.add(jti)
# In production, store in Redis with expiration
@staticmethod
def verify_refresh_token(refresh_token):
"""Verify refresh token"""
if refresh_token not in refresh_tokens:
return None
token_data = refresh_tokens[refresh_token]
if datetime.utcnow() > token_data['expires_at']:
del refresh_tokens[refresh_token]
return None
return token_data
def token_required(f):
"""Decorator to require valid JWT token"""
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Token is missing'}), 401
try:
token = token.split(' ')[1] # Remove 'Bearer ' prefix
payload = TokenManager.verify_token(token)
request.current_user = payload
except jwt.InvalidTokenError as e:
return jsonify({'error': str(e)}), 401
return f(*args, **kwargs)
decorated.__name__ = f.__name__
return decorated
@app.route('/auth/login', methods=['POST'])
def login():
"""User login endpoint"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': 'Username and password required'}), 400
# Validate user credentials (replace with your user validation)
user = validate_user(username, password)
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
access_token, refresh_token = TokenManager.generate_tokens(user)
response = make_response(jsonify({
'access_token': access_token,
'user': {
'id': user['id'],
'username': user['username'],
'role': user['role']
}
}))
# Set refresh token as httpOnly cookie
response.set_cookie(
'refresh_token',
refresh_token,
httponly=True,
secure=True, # Use HTTPS in production
samesite='Strict',
max_age=int(JWT_REFRESH_TOKEN_EXPIRES.total_seconds())
)
return response
@app.route('/auth/refresh', methods=['POST'])
def refresh():
"""Refresh access token"""
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
return jsonify({'error': 'Refresh token required'}), 401
token_data = TokenManager.verify_refresh_token(refresh_token)
if not token_data:
return jsonify({'error': 'Invalid or expired refresh token'}), 401
user = get_user_by_id(token_data['user_id'])
access_token, new_refresh_token = TokenManager.generate_tokens(user)
# Remove old refresh token
del refresh_tokens[refresh_token]
response = make_response(jsonify({
'access_token': access_token
}))
# Set new refresh token
response.set_cookie(
'refresh_token',
new_refresh_token,
httponly=True,
secure=True,
samesite='Strict',
max_age=int(JWT_REFRESH_TOKEN_EXPIRES.total_seconds())
)
return response
@app.route('/auth/logout', methods=['POST'])
@token_required
def logout():
"""User logout endpoint"""
# Blacklist the access token
TokenManager.blacklist_token(request.current_user['jti'])
# Remove refresh token
refresh_token = request.cookies.get('refresh_token')
if refresh_token and refresh_token in refresh_tokens:
del refresh_tokens[refresh_token]
response = make_response(jsonify({'message': 'Logged out successfully'}))
response.set_cookie('refresh_token', '', expires=0)
return response
@app.route('/protected', methods=['GET'])
@token_required
def protected():
"""Protected route example"""
return jsonify({
'message': 'This is a protected route',
'user': request.current_user
})
def validate_user(username, password):
"""Validate user credentials (replace with your implementation)"""
# This is a mock function - replace with your actual user validation
users = {
'admin': {'id': 1, 'username': 'admin', 'password': 'hashed_password', 'role': 'admin'},
'user': {'id': 2, 'username': 'user', 'password': 'hashed_password', 'role': 'user'}
}
user = users.get(username)
if user and bcrypt.checkpw(password.encode('utf-8'), user['password'].encode('utf-8')):
return user
return None
def get_user_by_id(user_id):
"""Get user by ID (replace with your implementation)"""
# Mock function - replace with your actual user retrieval
users = {
1: {'id': 1, 'username': 'admin', 'role': 'admin'},
2: {'id': 2, 'username': 'user', 'role': 'user'}
}
return users.get(user_id)
if __name__ == '__main__':
app.run(debug=True)
Java Implementation (Spring Boot)
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseEntity> login(@RequestBody LoginRequest loginRequest) {
try {
User user = userService.validateUser(
loginRequest.getUsername(),
loginRequest.getPassword()
);
if (user == null) {
return ResponseEntity.status(401)
.body(new AuthResponse("Invalid credentials"));
}
String accessToken = tokenProvider.generateAccessToken(user);
String refreshToken = tokenProvider.generateRefreshToken(user);
// Set refresh token as HttpOnly cookie
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(Duration.ofDays(7))
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.body(new AuthResponse("Login successful", accessToken, user));
} catch (Exception e) {
return ResponseEntity.status(500)
.body(new AuthResponse("Login failed"));
}
}
@PostMapping("/refresh")
public ResponseEntity> refresh(HttpServletRequest request) {
String refreshToken = getRefreshTokenFromCookie(request);
if (refreshToken == null || !tokenProvider.validateRefreshToken(refreshToken)) {
return ResponseEntity.status(401)
.body(new AuthResponse("Invalid refresh token"));
}
User user = tokenProvider.getUserFromRefreshToken(refreshToken);
String newAccessToken = tokenProvider.generateAccessToken(user);
String newRefreshToken = tokenProvider.generateRefreshToken(user);
// Revoke old refresh token
tokenProvider.revokeRefreshToken(refreshToken);
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", newRefreshToken)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(Duration.ofDays(7))
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
.body(new AuthResponse("Token refreshed", newAccessToken));
}
@PostMapping("/logout")
public ResponseEntity> logout(HttpServletRequest request) {
String accessToken = getAccessTokenFromHeader(request);
String refreshToken = getRefreshTokenFromCookie(request);
if (accessToken != null) {
tokenProvider.revokeAccessToken(accessToken);
}
if (refreshToken != null) {
tokenProvider.revokeRefreshToken(refreshToken);
}
ResponseCookie deleteRefreshTokenCookie = ResponseCookie.from("refreshToken", "")
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(0)
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, deleteRefreshTokenCookie.toString())
.body(new AuthResponse("Logged out successfully"));
}
}
Handling Token Lifecycle
Token Generation Best Practices
- Always include an expiration time (exp claim)
- Use short expiration times for access tokens (15-30 minutes)
- Include necessary claims only (principle of least privilege)
- Use strong, unique JWT IDs (jti) for each token
- Include issuer (iss) and audience (aud) claims
Token Storage on Client Side
Web Applications
| Storage Method | Pros | Cons | Best For |
|---|---|---|---|
| HttpOnly Cookies | XSS protection, automatic sending | CSRF vulnerability, size limits | Traditional web apps |
| localStorage | Large storage, explicit control | XSS vulnerable, persistent | SPAs with XSS protection |
| sessionStorage | Tab-scoped, automatic cleanup | XSS vulnerable, lost on close | Temporary sessions |
| Memory only | Most secure, no persistence | Lost on refresh, complex to manage | High-security applications |
Mobile Applications
- iOS: Use Keychain Services for secure storage
- Android: Use Android Keystore system
- React Native: Use @react-native-async-storage/async-storage with encryption
- Flutter: Use flutter_secure_storage package
Token Refresh Strategy
Automatic Refresh
// Automatic token refresh implementation
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.refreshTimer = null;
}
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.scheduleRefresh();
}
scheduleRefresh() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
// Decode token to get expiration
const payload = JSON.parse(atob(this.accessToken.split('.')[1]));
const expiresAt = payload.exp * 1000; // Convert to milliseconds
const now = Date.now();
// Refresh 5 minutes before expiration
const refreshAt = expiresAt - (5 * 60 * 1000);
const timeUntilRefresh = refreshAt - now;
if (timeUntilRefresh > 0) {
this.refreshTimer = setTimeout(() => {
this.refreshTokens();
}, timeUntilRefresh);
}
}
async refreshTokens() {
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include' // Include cookies
});
if (response.ok) {
const data = await response.json();
this.setTokens(data.accessToken, this.refreshToken);
} else {
// Refresh failed, redirect to login
this.logout();
}
} catch (error) {
console.error('Token refresh failed:', error);
this.logout();
}
}
async makeAuthenticatedRequest(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
if (response.status === 401) {
// Token might be expired, try to refresh
await this.refreshTokens();
// Retry the request
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
}
return response;
}
logout() {
this.accessToken = null;
this.refreshToken = null;
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
// Redirect to login page
window.location.href = '/login';
}
}
Token Revocation
Immediate Revocation
// Token blacklist implementation with Redis
const redis = require('redis');
const client = redis.createClient();
class TokenBlacklist {
static async addToBlacklist(jti, exp) {
const ttl = exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await client.setex(`blacklist:${jti}`, ttl, 'revoked');
}
}
static async isBlacklisted(jti) {
const result = await client.get(`blacklist:${jti}`);
return result === 'revoked';
}
static async revokeAllUserTokens(userId) {
// Add user to global revocation list
await client.set(`user_revoked:${userId}`, Date.now());
}
static async isUserRevoked(userId, tokenIssuedAt) {
const revokedAt = await client.get(`user_revoked:${userId}`);
if (revokedAt) {
return parseInt(revokedAt) > tokenIssuedAt * 1000;
}
return false;
}
}
// Middleware to check token validity
async function validateToken(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.verify(token, JWT_SECRET);
// Check if token is blacklisted
if (await TokenBlacklist.isBlacklisted(decoded.jti)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
// Check if user has been globally revoked
if (await TokenBlacklist.isUserRevoked(decoded.sub, decoded.iat)) {
return res.status(401).json({ error: 'User tokens have been revoked' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
Advanced Security Measures
1. JSON Web Encryption (JWE)
For highly sensitive data, use JWE to encrypt the entire JWT payload:
// JWE implementation using jose library
const { EncryptJWT, jwtDecrypt } = require('jose');
const secret = new TextEncoder().encode(process.env.JWE_SECRET);
async function createEncryptedJWT(payload) {
const jwt = await new EncryptJWT(payload)
.setProtectedHeader({ alg: 'A256KW', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime('2h')
.setIssuer('secure-app')
.setAudience('secure-app-users')
.encrypt(secret);
return jwt;
}
async function decryptJWT(encryptedJWT) {
const { payload } = await jwtDecrypt(encryptedJWT, secret, {
issuer: 'secure-app',
audience: 'secure-app-users'
});
return payload;
}
2. Rate Limiting
Implement rate limiting to prevent brute force attacks:
const rateLimit = require('express-rate-limit');
// Rate limiting for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true
});
// Apply rate limiting to auth routes
app.use('/auth/login', authLimiter);
app.use('/auth/refresh', authLimiter);
3. Device Fingerprinting
Bind tokens to specific devices for additional security:
// Device fingerprinting
function generateDeviceFingerprint(req) {
const userAgent = req.headers['user-agent'];
const acceptLanguage = req.headers['accept-language'];
const acceptEncoding = req.headers['accept-encoding'];
const fingerprint = crypto
.createHash('sha256')
.update(`${userAgent}${acceptLanguage}${acceptEncoding}`)
.digest('hex');
return fingerprint;
}
// Include device fingerprint in token
function generateTokenWithFingerprint(user, req) {
const deviceFingerprint = generateDeviceFingerprint(req);
const payload = {
sub: user.id,
username: user.username,
role: user.role,
device: deviceFingerprint
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
}
// Validate device fingerprint
function validateDeviceFingerprint(req, res, next) {
const currentFingerprint = generateDeviceFingerprint(req);
const tokenFingerprint = req.user.device;
if (currentFingerprint !== tokenFingerprint) {
return res.status(401).json({ error: 'Device fingerprint mismatch' });
}
next();
}
4. IP Address Validation
Bind tokens to specific IP addresses:
// IP address validation
function getClientIP(req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null);
}
// Include IP in token
function generateTokenWithIP(user, req) {
const clientIP = getClientIP(req);
const payload = {
sub: user.id,
username: user.username,
role: user.role,
ip: clientIP
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
}
// Validate IP address
function validateIPAddress(req, res, next) {
const currentIP = getClientIP(req);
const tokenIP = req.user.ip;
if (currentIP !== tokenIP) {
return res.status(401).json({ error: 'IP address mismatch' });
}
next();
}
5. Multi-Factor Authentication (MFA)
Implement MFA for sensitive operations:
// MFA implementation
const speakeasy = require('speakeasy');
// Generate MFA token
function generateMFAToken(user) {
const payload = {
sub: user.id,
username: user.username,
role: user.role,
mfa_verified: false
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: '5m' });
}
// Verify MFA and upgrade token
app.post('/auth/mfa-verify', authenticateToken, (req, res) => {
const { mfaCode } = req.body;
// Verify MFA code
const verified = speakeasy.totp.verify({
secret: req.user.mfa_secret,
encoding: 'base32',
token: mfaCode,
window: 2
});
if (!verified) {
return res.status(401).json({ error: 'Invalid MFA code' });
}
// Generate new token with MFA verified
const payload = {
...req.user,
mfa_verified: true
};
const newToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken: newToken });
});
// Middleware to require MFA for sensitive operations
function requireMFA(req, res, next) {
if (!req.user.mfa_verified) {
return res.status(403).json({ error: 'MFA verification required' });
}
next();
}
Common Implementation Mistakes
1. Storing Sensitive Data in JWT
Problem
Including sensitive information in JWT payloads.
Bad Example
{
"sub": "user123",
"password": "hashedPassword",
"creditCard": "1234-5678-9012-3456",
"ssn": "123-45-6789"
}
Good Example
{
"sub": "user123",
"role": "user",
"permissions": ["read", "write"]
}
2. Using Weak Secrets
Problem
Using predictable or weak secrets for HMAC signing.
Bad Example
const JWT_SECRET = 'secret';
const JWT_SECRET = 'myapp';
const JWT_SECRET = '123456';
Good Example
const JWT_SECRET = crypto.randomBytes(32).toString('hex');
// Or use environment variables with strong secrets
const JWT_SECRET = process.env.JWT_SECRET;
3. Not Validating Claims
Problem
Skipping validation of important claims like audience, issuer, and expiration.
Bad Example
// Minimal validation
const decoded = jwt.verify(token, secret);
Good Example
// Comprehensive validation
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'my-app',
audience: 'my-api',
maxAge: '15m'
});
4. Long Token Expiration
Problem
Setting very long expiration times for access tokens.
Bad Example
// Token valid for 30 days
const token = jwt.sign(payload, secret, { expiresIn: '30d' });
Good Example
// Short-lived access token with refresh mechanism
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
const refreshToken = crypto.randomBytes(32).toString('hex');
5. Ignoring Algorithm Validation
Problem
Not specifying allowed algorithms, leaving room for algorithm confusion attacks.
Bad Example
// Accepts any algorithm
const decoded = jwt.verify(token, secret);
Good Example
// Explicitly specify allowed algorithms
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256']
});
JWT in Microservices Architecture
Challenges in Microservices
- Token validation across multiple services
- Service-to-service authentication
- Token propagation and context sharing
- Consistent security policies
Architecture Patterns
1. Centralized Authentication Service
// API Gateway with JWT validation
const express = require('express');
const jwt = require('jsonwebtoken');
const httpProxy = require('http-proxy-middleware');
const app = express();
// JWT validation middleware
function validateJWT(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
// Add user context to forwarded request
req.headers['x-user-id'] = decoded.sub;
req.headers['x-user-role'] = decoded.role;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Proxy configuration
const services = {
'/user': 'http://user-service:3001',
'/product': 'http://product-service:3002',
'/order': 'http://order-service:3003'
};
// Apply JWT validation to all routes
app.use(validateJWT);
// Proxy requests to microservices
Object.keys(services).forEach(path => {
app.use(path, httpProxy({
target: services[path],
changeOrigin: true,
pathRewrite: { [`^${path}`]: '' }
}));
});
app.listen(3000, () => {
console.log('API Gateway running on port 3000');
});
2. Service-to-Service Authentication
// Service-to-service JWT
class ServiceAuthenticator {
constructor(serviceName, serviceSecret) {
this.serviceName = serviceName;
this.serviceSecret = serviceSecret;
}
generateServiceToken(targetService, scopes = []) {
const payload = {
sub: this.serviceName,
aud: targetService,
iss: 'service-mesh',
scope: scopes,
type: 'service'
};
return jwt.sign(payload, this.serviceSecret, { expiresIn: '5m' });
}
validateServiceToken(token, expectedAudience) {
try {
const decoded = jwt.verify(token, this.serviceSecret, {
audience: expectedAudience,
issuer: 'service-mesh'
});
if (decoded.type !== 'service') {
throw new Error('Invalid token type');
}
return decoded;
} catch (err) {
throw new Error('Invalid service token');
}
}
}
// Usage in microservice
const serviceAuth = new ServiceAuthenticator('user-service', process.env.SERVICE_SECRET);
// Making authenticated request to another service
async function callOrderService(userId) {
const token = serviceAuth.generateServiceToken('order-service', ['read:orders']);
const response = await fetch('http://order-service/orders', {
headers: {
'Authorization': `Bearer ${token}`,
'X-User-ID': userId
}
});
return response.json();
}
Best Practices for Microservices
1. Token Propagation
// Middleware to propagate user context
function propagateUserContext(req, res, next) {
if (req.user) {
// Add user information to request headers
req.headers['x-user-id'] = req.user.sub;
req.headers['x-user-role'] = req.user.role;
req.headers['x-user-permissions'] = JSON.stringify(req.user.permissions);
}
next();
}
// Service client with context propagation
class ServiceClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(path, options = {}, userContext = null) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
// Propagate user context if available
if (userContext) {
headers['x-user-id'] = userContext.sub;
headers['x-user-role'] = userContext.role;
headers['x-user-permissions'] = JSON.stringify(userContext.permissions);
}
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers
});
return response.json();
}
}
2. Distributed Token Validation
// Shared JWT validation service
class DistributedJWTValidator {
constructor(publicKey, redisClient) {
this.publicKey = publicKey;
this.redis = redisClient;
}
async validateToken(token) {
try {
// First check cache
const cacheKey = `jwt:${token}`;
const cachedResult = await this.redis.get(cacheKey);
if (cachedResult) {
return JSON.parse(cachedResult);
}
// Validate token
const decoded = jwt.verify(token, this.publicKey, {
algorithms: ['RS256'],
issuer: 'auth-service'
});
// Check if token is blacklisted
const isBlacklisted = await this.redis.sismember('blacklist', decoded.jti);
if (isBlacklisted) {
throw new Error('Token revoked');
}
// Cache the result
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await this.redis.setex(cacheKey, ttl, JSON.stringify(decoded));
return decoded;
} catch (err) {
throw new Error('Token validation failed');
}
}
}
// Usage in multiple services
const validator = new DistributedJWTValidator(publicKey, redisClient);
// Shared middleware
async function validateJWTMiddleware(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
const decoded = await validator.validateToken(token);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Unauthorized' });
}
}