JSON Web Tokens (JWTs) are the backbone of modern stateless authentication. But their flexibility comes with risk — misconfigured JWTs are a top source of authentication bypasses and data leaks.
How JWTs Work
A JWT consists of three Base64URL-encoded parts separated by dots:
// Header: algorithm and token type
{ "alg": "RS256", "typ": "JWT" }
// Payload: claims (the actual data)
{ "sub": "user123", "exp": 1745635200, "role": "admin" }
// Signature: cryptographic proof of integrity
HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)
Common Vulnerabilities
1. Algorithm Confusion Attack
Attackers change the header to "alg": "none" or switch from RS256 to HS256:
// Attack: Change algorithm to "none"
{ "alg": "none", "typ": "JWT" }.
{ "sub": "admin", "role": "superuser" }.
// Empty signature — server accepts it!
Fix: Always explicitly whitelist allowed algorithms on the server.
2. Weak Signing Secrets
Using short or predictable secrets enables brute-force attacks:
// BAD: Weak secrets
const secret = "mySecret"; // Too short
const secret = "password123"; // Dictionary word
// GOOD: Strong secrets
const secret = crypto.randomBytes(64).toString('hex');
// Or use asymmetric keys (RS256) for better security
3. Token Expiration Issues
- Missing
expclaim → tokens valid forever - Setting
expin the past → broken auth - Very long expiration → larger attack window
Security Best Practices
Token Storage
| Method | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| localStorage | High | Low | Not recommended |
| Cookie (no HttpOnly) | High | High | Never use |
| Cookie (HttpOnly + Secure) | Low | Medium | Recommended |
| In-memory + refresh token | Low | Low | Best for SPAs |
Implementation Checklist
- ✅ Use RS256 (asymmetric) for distributed systems, HS256 for monoliths
- ✅ Set short access token expiration (15-30 minutes)
- ✅ Use refresh tokens with rotation for long-lived sessions
- ✅ Validate all claims:
iss,aud,exp,nbf - ✅ Include a unique
jti(JWT ID) for token revocation - ✅ Store tokens in HttpOnly, Secure, SameSite cookies
- ✅ Implement token blocklist for logout and revocation
Secure Implementation Example
import jwt from 'jsonwebtoken';
// Verify token with strict validation
function verifyToken(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Whitelist algorithms
issuer: 'https://api.myapp.com',
audience: 'myapp-frontend',
clockTolerance: 5, // 5 second leeway
maxAge: '30m' // Max age even if exp allows
});
}
// Generate secure access token
function generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
role: user.role,
jti: crypto.randomUUID() // Unique token ID
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://api.myapp.com',
audience: 'myapp-frontend'
}
);
}
Refresh Token Pattern
// Access token: short-lived (15 min), stored in memory
// Refresh token: long-lived (7 days), stored in HttpOnly cookie
app.post('/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refresh_token;
// Verify refresh token
const payload = verifyRefreshToken(refreshToken);
// Rotate: invalidate old, issue new
revokeToken(refreshToken);
const newAccess = generateAccessToken(payload.user);
const newRefresh = generateRefreshToken(payload.user);
res.cookie('refresh_token', newRefresh, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ access_token: newAccess });
});
Key Takeaways
- Always whitelist allowed algorithms — never trust the header
- Use strong signing secrets (256-bit minimum for HS256)
- Keep access tokens short-lived and use refresh token rotation
- Store tokens in HttpOnly + Secure cookies, not localStorage
- Validate all standard claims on every request