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.
JWT Decoder & Validator
Decode and inspect JWT tokens instantly. View header, payload, and verify signatures with security validation.
Open the full JWT Decoder & Validator tool →JWT Structure
┌─────────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Header Payload Signature │
│ ────── ─────── ───────── │
│ eyJhbGci... . eyJzdWIi... . SflKxwRJ... │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ { │ │ { │ │ HMACSHA256( │ │
│ │ "alg": │ │ "sub":"123",│ │ base64(header) + │ │
│ │ "RS256",│ │ "name":"Jo",│ │ "." + │ │
│ │ "typ": │ │ "exp":12345,│ │ base64(payload), │ │
│ │ "JWT" │ │ "roles":[] │ │ secret │ │
│ │ } │ │ } │ │ ) │ │
│ └──────────┘ └──────────────┘ └──────────────────────┘ │
│ │
│ Base64URL Base64URL Cryptographic │
│ Encoded Encoded Signature │
│ │
│ ⚠️ NOT ENCRYPTED - Anyone can read header and payload! │
│ │
└─────────────────────────────────────────────────────────────────┘
Signing Algorithm Selection
| Algorithm | Type | Key | Use Case | Security |
|---|---|---|---|---|
| HS256 | HMAC | Shared secret | Single service | Good (if secret is strong) |
| HS384 | HMAC | Shared secret | Single service | Better |
| HS512 | HMAC | Shared secret | Single service | Best HMAC |
| RS256 | RSA | Public/Private | Distributed systems | Good |
| RS384 | RSA | Public/Private | Distributed systems | Better |
| RS512 | RSA | Public/Private | Distributed systems | Best RSA |
| ES256 | ECDSA | Public/Private | Mobile, performance | Recommended |
| ES384 | ECDSA | Public/Private | High security | Better |
| ES512 | ECDSA | Public/Private | Highest security | Best ECDSA |
| none | None | None | ❌ NEVER USE | Vulnerable |
When to Use Each
Single Backend Service?
│
├─► Yes ─► HS256/HS384/HS512
│ (Shared secret, simpler)
│
└─► No ─► Multiple services verify tokens?
│
├─► Yes ─► RS256 or ES256
│ (Asymmetric, public key distribution)
│
└─► Performance critical?
│
├─► Yes ─► ES256 (smaller signatures)
│
└─► No ─► RS256 (widest compatibility)
Standard Claims Reference
{
// REGISTERED CLAIMS (RFC 7519)
"iss": "https://auth.example.com", // Issuer - who created the token
"sub": "user_12345", // Subject - who the token is about
"aud": "https://api.example.com", // Audience - intended recipient(s)
"exp": 1735689600, // Expiration - Unix timestamp
"nbf": 1735686000, // Not Before - token not valid before
"iat": 1735686000, // Issued At - when token was created
"jti": "unique-token-id-abc123", // JWT ID - unique identifier
// CUSTOM CLAIMS (your application)
"roles": ["user", "admin"],
"permissions": ["read:data", "write:data"],
"tenant_id": "tenant_abc",
"email": "[email protected]"
}
Token Validation Implementation
Correct Validation (Node.js Example)
const jwt = require('jsonwebtoken');
// CORRECT: Explicit algorithm, all claims validated
function validateToken(token) {
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // NEVER use the alg from header
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 30, // 30 seconds for clock skew
});
// Additional custom validation
if (!decoded.sub) {
throw new Error('Missing subject claim');
}
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
What NOT to Do
// ❌ DANGEROUS: Trusts algorithm from token header
const decoded = jwt.verify(token, secret); // Don't do this!
// ❌ DANGEROUS: No algorithm specified
jwt.verify(token, secret, {});
// ❌ DANGEROUS: Allows "none" algorithm
jwt.verify(token, secret, { algorithms: ['RS256', 'none'] });
// ❌ DANGEROUS: No issuer/audience validation
jwt.verify(token, secret, { algorithms: ['RS256'] });
// Attacker's valid token from different issuer would be accepted
Secure Token Storage
Web Applications
// ❌ WRONG: localStorage is XSS vulnerable
localStorage.setItem('token', accessToken);
// ❌ WRONG: sessionStorage is also XSS vulnerable
sessionStorage.setItem('token', accessToken);
// ✅ CORRECT: HttpOnly cookies (set by server)
// Server response:
res.cookie('access_token', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 900000, // 15 minutes
path: '/api' // Only sent to API routes
});
// ✅ CORRECT: Memory only for SPAs
class TokenStore {
#accessToken = null; // Private, in-memory only
setToken(token) {
this.#accessToken = token;
}
getToken() {
return this.#accessToken;
}
clear() {
this.#accessToken = null;
}
}
Backend-for-Frontend (BFF) Pattern
┌─────────────┐ Session ┌─────────────┐ JWT ┌─────────────┐
│ Browser │◄───────────────────│ BFF │◄───────────────►│ API │
│ (SPA) │ Cookie (ID) │ (Backend) │ Bearer │ Services │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Token │
│ Store │
└─────────────┘
Benefits:
• Tokens never reach browser (no XSS risk)
• Session cookies are HttpOnly
• BFF handles token refresh
• Can implement additional security layers
Common Vulnerabilities
1. Algorithm Confusion Attack
ATTACK SCENARIO:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. Server expects RS256 (asymmetric) │
│ Public key: MIIBIjANBg... │
│ │
│ 2. Attacker changes header to HS256 (symmetric) │
│ {"alg":"HS256","typ":"JWT"} │
│ │
│ 3. Attacker signs with PUBLIC KEY as HMAC secret │
│ HMACSHA256(header.payload, publicKey) │
│ │
│ 4. Vulnerable server: │
│ - Reads alg: "HS256" from header │
│ - Uses stored key for HMAC verification │
│ - Verification succeeds! (same key used both sides) │
│ │
│ PREVENTION: NEVER trust the alg header! │
│ Always specify expected algorithm explicitly. │
│ │
└─────────────────────────────────────────────────────────────────┘
2. Token Leakage Vectors
| Vector | Risk | Mitigation |
|---|---|---|
| URL query strings | Logged in server logs, browser history | Never send tokens in URLs |
| Referrer headers | Token leaked to external sites | Use Referrer-Policy: no-referrer |
| Browser storage | XSS can steal tokens | Use HttpOnly cookies |
| Error messages | Token exposed in stack traces | Never log full tokens |
| Client-side code | Visible in source/DevTools | Keep tokens in memory |
3. Weak Secret Keys
// ❌ WEAK: Too short, predictable
const secret = 'secret123';
const secret = 'password';
const secret = 'jwt-secret';
// ❌ WEAK: Common patterns
const secret = process.env.APP_NAME + '-jwt';
// ✅ STRONG: Cryptographically random, sufficient length
// Generate with: openssl rand -base64 64
const secret = process.env.JWT_SECRET;
// Value: 'x8kH2nQ9...(64+ random bytes base64 encoded)'
// For HS256: minimum 256 bits (32 bytes) of entropy
// For HS512: minimum 512 bits (64 bytes) of entropy
Token Expiration Strategy
┌─────────────────────────────────────────────────────────────────┐
│ 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)
- Include jti (unique ID) for revocation support
- Set appropriate expiration (exp)
- Include issuer (iss) and audience (aud)
- Minimize payload size and sensitive data
Token Validation
- Explicitly specify expected algorithm (never trust alg header)
- Validate signature before trusting claims
- Check expiration (exp) with clock tolerance
- Validate issuer (iss) matches expected value
- Validate audience (aud) includes your service
- Validate not-before (nbf) if present
Token Storage
- Never store in localStorage/sessionStorage
- Use HttpOnly, Secure, SameSite cookies for web
- Use platform secure storage for mobile
- Keep access tokens in memory for SPAs
- Implement BFF pattern for sensitive applications
Token Lifecycle
- Use short-lived access tokens (5-15 min)
- Implement refresh token rotation
- Support token revocation mechanism
- Clear tokens on logout
- Handle token refresh failures gracefully
Tools for JWT Security
- JWT Decoder - Inspect token structure (never use with production tokens online)
- jwt.io - Debugger for development tokens
- jose - Comprehensive JWT library for Node.js
- PyJWT - JWT library for Python
Next Steps
- OAuth 2.0 & OIDC Guide - Complete OAuth implementation
- API Authentication Comparison - JWT vs other methods
- API Security Complete Guide - Comprehensive security overview