JWT Security Best Practices: Complete Guide for 2026
Published February 4, 2026 • 12 min read
JSON Web Tokens (JWTs) are everywhere in modern web development, but implementing them securely requires careful attention to detail. A single mistake can expose your entire application to serious vulnerabilities. This comprehensive guide covers everything you need to know about JWT security.
What is a JWT?
A JWT is a compact, URL-safe token format that contains three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
HEADER.PAYLOAD.SIGNATURE
While JWTs are convenient for authentication and data exchange, they introduce security challenges that developers must understand and address.
Critical JWT Security Best Practices
1. Always Use Strong Signing Algorithms
Use: RS256, ES256, or HS256 (with a strong secret)
Never use: None algorithm
The "none" algorithm vulnerability is famous—attackers can create unsigned tokens that bypass authentication entirely. Always validate the algorithm:
// ✅ Good - Explicit algorithm whitelist
const options = {
algorithms: ['RS256', 'ES256']
};
jwt.verify(token, publicKey, options);
// ❌ Bad - Allows any algorithm
jwt.verify(token, publicKey);
2. Keep Secrets Secret
Your signing secret is the keys to the kingdom. Never:
- Hardcode secrets in your source code
- Commit secrets to version control
- Use weak secrets like "secret" or "password123"
- Share the same secret across multiple applications
- Expose secrets in client-side code
Best practices:
- Generate cryptographically strong secrets (minimum 256 bits for HS256)
- Store secrets in environment variables or secret management systems (AWS Secrets Manager, HashiCorp Vault)
- Rotate secrets regularly
- Use asymmetric algorithms (RS256) when possible—public key can be shared safely
// Generate strong secret (Node.js)
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
// Use environment variables
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('JWT_SECRET environment variable not set');
}
3. Set Short Expiration Times
JWTs can't be invalidated once issued (they're stateless). The only defense is expiration:
- Access tokens: 5-15 minutes
- Refresh tokens: 7-30 days (stored server-side for revocation)
- Never: Tokens without expiration
// ✅ Good - Short-lived access token
const accessToken = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '15m' }
);
// ✅ Good - Longer-lived refresh token (store in database)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
4. Validate All Claims
Always validate these critical claims:
exp(expiration): Token is not expirednbf(not before): Token is valid nowiss(issuer): Token came from your serveraud(audience): Token is for your applicationiat(issued at): Token isn't too old
const options = {
algorithms: ['RS256'],
issuer: 'https://your-app.com',
audience: 'your-api',
maxAge: '15m'
};
try {
const decoded = jwt.verify(token, publicKey, options);
} catch (err) {
// Token invalid, expired, or claims don't match
return res.status(401).json({ error: 'Invalid token' });
}
5. Secure Token Storage
Never store JWTs in:
- localStorage (vulnerable to XSS)
- sessionStorage (vulnerable to XSS)
- Unencrypted cookies
Best options:
Option 1: HttpOnly Cookies (Recommended for SPAs)
res.cookie('accessToken', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 900000 // 15 minutes
});
Option 2: Memory (for SPAs)
Store tokens in JavaScript variables/React state. They're lost on refresh, but that's acceptable for short-lived access tokens. Use refresh tokens in httpOnly cookies to get new access tokens.
Option 3: Secure Storage (Mobile Apps)
Use platform-specific secure storage: iOS Keychain, Android Keystore.
6. Protect Against Common Attacks
Algorithm Confusion Attack
Attacker changes "RS256" to "HS256" and signs with the public key. Defense:
// Explicitly specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Cross-Site Request Forgery (CSRF)
If using cookies, implement CSRF protection:
- Use
sameSite: 'strict'or'lax'cookie attribute - Implement double-submit cookie pattern
- Use custom headers (X-Requested-With) for API calls
Cross-Site Scripting (XSS)
XSS can steal tokens from localStorage or cookies. Defense:
- Use httpOnly cookies
- Implement Content Security Policy (CSP)
- Sanitize all user input
- Use frameworks with built-in XSS protection
Token Replay Attacks
Use short expiration times and implement token rotation:
// Include jti (JWT ID) for one-time use tokens
const token = jwt.sign(
{
userId: user.id,
jti: crypto.randomUUID() // Unique token ID
},
JWT_SECRET,
{ expiresIn: '15m' }
);
// Check jti against blacklist/database for revocation
7. Implement Token Revocation
While JWTs are stateless, you need revocation for:
- User logout
- Password changes
- Suspicious activity
- Admin-initiated lockouts
Approaches:
Blacklist (for emergencies)
// Store revoked token IDs in Redis with TTL
await redis.setex(`blacklist:${jti}`, 900, '1');
// Check on verification
const isBlacklisted = await redis.exists(`blacklist:${jti}`);
Token Versioning
// Include version in token
const token = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
JWT_SECRET
);
// Increment version on logout/password change
await User.update({ tokenVersion: user.tokenVersion + 1 });
Refresh Token Rotation
Most secure: Store refresh tokens server-side, issue new ones on each use, invalidate old ones.
8. Don't Store Sensitive Data in Tokens
JWTs are encoded (Base64), not encrypted. Anyone can decode them:
// ❌ Bad - Sensitive data exposed
const token = jwt.sign({
userId: user.id,
email: user.email,
password: user.password, // NEVER!
creditCard: user.cc // NEVER!
}, JWT_SECRET);
// ✅ Good - Minimal claims
const token = jwt.sign({
sub: user.id,
role: user.role
}, JWT_SECRET);
If you need encrypted tokens, use JWE (JSON Web Encryption) instead.
9. Use Standard Libraries
Never implement JWT yourself. Use battle-tested libraries:
- Node.js: jsonwebtoken, jose
- Python: PyJWT, python-jose
- Java: jjwt, auth0 java-jwt
- Go: golang-jwt/jwt
- PHP: firebase/php-jwt
10. Monitor and Log JWT Usage
Implement logging and monitoring:
- Failed verification attempts (possible attack)
- Tokens used after expiration
- Unusual token usage patterns
- Geographic anomalies
try {
const decoded = jwt.verify(token, publicKey, options);
} catch (err) {
logger.warn('JWT verification failed', {
error: err.message,
ip: req.ip,
userAgent: req.get('user-agent'),
tokenIssued: decoded?.iat
});
throw err;
}
Complete JWT Security Checklist
Use this checklist for every JWT implementation:
- ✅ Using RS256, ES256, or HS256 with strong secret
- ✅ Explicitly whitelisting allowed algorithms
- ✅ Secret stored in environment variables
- ✅ Access tokens expire in 15 minutes or less
- ✅ Validating exp, iss, aud claims
- ✅ Tokens stored in httpOnly cookies or secure storage
- ✅ CSRF protection implemented
- ✅ XSS protection (CSP, input sanitization)
- ✅ Token revocation mechanism in place
- ✅ No sensitive data in token payload
- ✅ Using standard, well-maintained libraries
- ✅ Logging failed verification attempts
- ✅ HTTPS enforced in production
- ✅ Regular secret rotation schedule
When NOT to Use JWTs
JWTs aren't always the right choice:
- Session management in traditional web apps: Server-side sessions are simpler and more secure
- High-security applications: Inability to immediately revoke is problematic
- Large payloads: JWTs are sent with every request; large tokens impact performance
- When you need instant revocation: Consider opaque tokens with server-side lookup
Testing JWT Security
Use our JWT Decoder tool to inspect and debug tokens during development. Never paste production tokens into online tools—they may log your data.
Additional Resources
- JWT.io - Official JWT resources
- OWASP Web Security Testing Guide
- OWASP JWT Cheat Sheet
Conclusion
JWT security requires diligence at every step—from choosing the right algorithm to storing tokens securely and implementing proper expiration. Following these best practices will protect your application from the most common JWT vulnerabilities.
Remember: the convenience of JWTs comes with responsibility. When in doubt, choose security over convenience, and never assume tokens are safe by default.
Debug JWTs Securely
Use our free JWT Decoder to inspect tokens locally. All processing happens in your browser—no data is sent to servers.