JWT Security Best Practices: Protect Your Authentication System
JSON Web Tokens are widely used for authentication, but improper implementation can lead to serious security vulnerabilities. This guide covers essential security practices to protect your JWT-based authentication system.
Why JWT Security Matters
JWT tokens are self-contained credentials. Anyone who possesses a valid token can access protected resources until the token expires. Unlike session-based authentication, there's no central revocation mechanism—making security practices critical.
Token Storage Security
The Storage Dilemma
| Storage Method | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| localStorage | High | None | ❌ Avoid |
| sessionStorage | High | None | ❌ Avoid |
| HttpOnly Cookie | None | High | ✅ Use with CSRF protection |
| Memory | None | None | ✅ Best for sensitive apps |
Recommended Approach
For Web Applications: Use HttpOnly cookies with SameSite attribute:
Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Path=/
For Mobile/SPA: Store in memory, refresh on app load:
// Store in memory (not localStorage)
let accessToken = null;
function setToken(token) {
accessToken = token;
}
function getToken() {
return accessToken;
}
Signing Algorithm Security
Algorithm Selection
| Algorithm | Security Level | When to Use |
|---|---|---|
| RS256 | High | Public APIs, third-party access (asymmetric) |
| ES256 | High | Modern apps, mobile, IoT (smaller signatures) |
| HS256 | High (with strong secret) | Internal services only (symmetric) |
Algorithm Confusion Attack Prevention
Vulnerability: Attackers can change the algorithm to "none" or switch from RS256 to HS256.
Prevention: Always specify allowed algorithms:
// ❌ Vulnerable
const decoded = jwt.verify(token, publicKey);
// ✅ Secure
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256']
});
Key Management
// ❌ Bad: Hardcoded secret
const secret = 'my-secret-key';
// ✅ Good: Environment variable
const secret = process.env.JWT_SECRET;
// ✅ Better: Key rotation support
const keyStore = new KeyStore();
const key = await keyStore.getCurrentKey();
Token Lifetime Management
Access Token Expiration
| Token Type | Recommended Lifetime | Reason |
|---|---|---|
| Access Token | 15-60 minutes | Limit exposure window |
| Refresh Token | 7-30 days | Balance UX and security |
| Password Reset | 5-15 minutes | Single-use, time-sensitive |
Implementing Token Refresh
// Token refresh with rotation
async function refreshTokens(refreshToken) {
// 1. Verify refresh token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// 2. Check if token is blacklisted
if (await isTokenBlacklisted(refreshToken)) {
throw new Error('Token revoked');
}
// 3. Generate new tokens
const newAccessToken = generateAccessToken(decoded.userId);
const newRefreshToken = generateRefreshToken(decoded.userId);
// 4. Invalidate old refresh token
await blacklistToken(refreshToken);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
Common JWT Vulnerabilities
1. None Algorithm Attack
Attack: Attacker sets alg: "none" to bypass signature verification. Some JWT libraries treat "none" as valid and skip verification.
Prevention:
// Always explicitly specify allowed algorithms
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256', 'RS256'] // Explicitly allow only these
});
// Never accept "none" algorithm
if (header.alg === 'none' || !header.alg) {
throw new Error('Invalid algorithm: none is not allowed');
}
2. Weak Secret Attack
Attack: Brute force weak HMAC secrets.
Prevention:
// Use strong secrets (256+ bits)
const secret = crypto.randomBytes(64).toString('hex');
// For RS256, use proper key lengths
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048
});
3. Token Leakage via URL
Attack: Tokens logged in server logs when passed in URL.
Prevention:
// ❌ Bad: Token in URL
fetch(`/api/user?token=${token}`);
// ✅ Good: Token in header
fetch('/api/user', {
headers: { 'Authorization': `Bearer ${token}` }
});
4. Missing Claim Validation
Attack: Attacker reuses token from different context.
Prevention:
function validateToken(token, expectedAudience, expectedIssuer) {
const decoded = jwt.verify(token, secret, {
audience: expectedAudience,
issuer: expectedIssuer,
clockTimestamp: Math.floor(Date.now() / 1000)
});
// Additional validation
if (decoded.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
return decoded;
}
Token Revocation Strategies
Since JWT tokens are stateless, revocation requires additional infrastructure:
1. Token Blacklist
// Redis-based blacklist
async function revokeToken(token) {
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blacklist:${token}`, ttl, '1');
}
}
async function isTokenRevoked(token) {
return await redis.exists(`blacklist:${token}`);
}
2. Token Version
// Store token version in user record
const token = jwt.sign({
userId,
version: user.tokenVersion
}, secret);
// Verify version
const decoded = jwt.verify(token, secret);
if (decoded.version !== user.tokenVersion) {
throw new Error('Token revoked');
}
// Revoke all tokens
await user.update({ tokenVersion: user.tokenVersion + 1 });
3. Short Lifetime + Refresh
The simplest approach: use very short-lived access tokens (5-15 minutes) with refresh tokens.
Security Headers for JWT
# Prevent token leakage via referrer
Referrer-Policy: no-referrer
# Prevent clickjacking
X-Frame-Options: DENY
# Prevent MIME sniffing
X-Content-Type-Options: nosniff
# Enable HSTS
Strict-Transport-Security: max-age=31536000; includeSubDomains
JWT Security Checklist
Implementation Checklist
- Use HTTPS for all token transmission
- Use strong secrets (256+ bits for HMAC, 2048+ bits for RSA)
- Specify allowed algorithms explicitly
- Validate all claims (iss, aud, exp, iat)
- Set appropriate token expiration
- Implement refresh token rotation
- Store tokens securely (HttpOnly cookies or memory)
- Never store sensitive data in JWT payload
- Implement token revocation mechanism
- Add rate limiting to authentication endpoints
Production Checklist
- Rotate signing keys periodically
- Monitor for suspicious token usage
- Log authentication events
- Implement account lockout after failed attempts
- Use different secrets for different environments
- Set up alerts for token-related errors
Testing Your JWT Security
Use our JWT Decoder to:
- Verify token structure and claims
- Check expiration times
- Inspect algorithm used
- Debug authentication issues
Conclusion
JWT security requires careful implementation. By following these best practices—proper storage, strong algorithms, claim validation, and revocation mechanisms—you can build a secure authentication system that protects your users and your application.
Key Security Principles:
- Defense in depth: Multiple layers of protection
- Least privilege: Minimal token permissions
- Fail secure: Reject tokens on any validation failure
- Audit everything: Log and monitor token usage
For quick JWT inspection and debugging, try our JWT Decoder tool.