Home/Blog/JWT Security Best Practices: Token Signing, Validation, and Common Vulnerabilities
Development
JWT Security Best Practices: Token Signing, Validation, and Common Vulnerabilities
Master JWT security with this comprehensive guide covering token structure, signing algorithms, validation best practices, secure storage, and common vulnerabilities like algorithm confusion and token leakage.
JSON Web Tokens (JWTs) are the de facto standard for API authentication and authorization. However, their apparent simplicity hides numerous security pitfalls. This guide covers JWT security best practices, common vulnerabilities, and how to implement tokens correctly.
┌─────────────────────────────────────────────────────────────────┐
│ TOKEN LIFETIME STRATEGY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Access Token: 5-15 minutes │
│ ├─► Sent with every API request │
│ ├─► Short-lived limits breach impact │
│ └─► Refresh when expired or about to expire │
│ │
│ Refresh Token: 7-30 days │
│ ├─► Used only to get new access tokens │
│ ├─► Stored more securely than access tokens │
│ ├─► Rotated on each use (one-time use) │
│ └─► Revocable server-side │
│ │
│ ID Token (OIDC): Same as access token │
│ ├─► Contains user identity claims │
│ ├─► Validated once at login │
│ └─► Not sent with API requests │
│ │
│ Timeline Example: │
│ ───────────────────────────────────────────────────────────── │
│ 0min 15min 30min 45min ... 7days │
│ │ │ │ │ │ │
│ │ AT1 │ AT2 │ AT3 │ AT4 │ │
│ └────────┴───────┴───────┴────────────┘ │
│ └──────────── Refresh Token ──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Token Revocation Strategies
Strategy 1: Token Blacklist
const redis = require('redis');
const client = redis.createClient();
// Revoke a token
async function revokeToken(jti, exp) {
const ttl = exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await client.setEx(`blacklist:${jti}`, ttl, '1');
}
}
// Check if token is revoked
async function isRevoked(jti) {
const result = await client.get(`blacklist:${jti}`);
return result === '1';
}
// Middleware
async function validateToken(req, res, next) {
const decoded = jwt.verify(req.token, secret);
if (await isRevoked(decoded.jti)) {
return res.status(401).json({ error: 'Token revoked' });
}
req.user = decoded;
next();
}
Strategy 2: Token Versioning
// User record includes token version
const user = {
id: '12345',
email: '[email protected]',
tokenVersion: 3 // Increment to invalidate all tokens
};
// Include version in token
const token = jwt.sign({
sub: user.id,
tokenVersion: user.tokenVersion
}, secret);
// Validate version matches
async function validateToken(decoded) {
const user = await getUser(decoded.sub);
if (decoded.tokenVersion !== user.tokenVersion) {
throw new Error('Token version mismatch - please re-authenticate');
}
}
// Revoke all tokens for a user
async function revokeAllTokens(userId) {
await db.query(
'UPDATE users SET token_version = token_version + 1 WHERE id = $1',
[userId]
);
}
Security Checklist
Token Generation
Use strong signing algorithm (RS256, ES256, or HS256 with strong secret)
Generate cryptographically random secrets (256+ bits)