JWT Security Checklist
Essential security checklist for JWT implementation. Follow these proven practices to protect your applications from common JWT vulnerabilities and attacks.
Security Implementation Progress
0%🚨 Critical Security Priorities
1. Always Use HTTPS
Never transmit JWTs over unencrypted connections
2. Set Short Expiration
Limit token lifetime to reduce attack window
3. Validate Everything
Verify signature, expiration, and all claims
🔐 Token Generation Security
Enforce HTTPS Only
criticalNever generate, transmit, or accept JWT tokens over unencrypted HTTP connections.
// ❌ Never do this
http://api.example.com/login
// ✅ Always use HTTPS
https://api.example.com/loginUse Cryptographically Strong Secrets
criticalGenerate secrets with at least 256 bits of entropy. Never use predictable or weak secrets.
// ✅ Generate strong secret
const secret = crypto.randomBytes(32).toString('hex');
// Result: 64-character hex string (256 bits)Set Short Token Expiration
highUse 15-60 minutes for access tokens. Implement refresh tokens for longer sessions.
Validate and Specify Algorithm
criticalAlways specify and validate the expected signing algorithm. Prevent algorithm confusion attacks.
// ❌ Accepts any algorithm
jwt.verify(token, secret)
// ✅ Specify expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })Never Store Sensitive Data in JWT
criticalJWTs are encoded, not encrypted. Never include passwords, SSNs, or other sensitive information.
❌ Never Include
- • Passwords or password hashes
- • Social Security Numbers
- • Credit card information
- • API secrets or keys
- • Personal identification numbers
✅ Safe to Include
- • User ID (non-sequential)
- • Username or email
- • User roles and permissions
- • Non-sensitive metadata
- • Application preferences
🔍 Token Validation Security
Always Verify Token Signature
criticalNever skip signature verification. This is your primary defense against token tampering.
// ✅ Proper verification
try {
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
// Token is valid and verified
} catch (error) {
// Invalid token - reject request
return res.status(401).json({ error: 'Invalid token' });
}Validate Token Expiration
criticalAlways check the 'exp' claim. Expired tokens should be immediately rejected.
// ✅ Expiration is automatically checked by most libraries
// But you can also manually verify:
if (decoded.exp && Date.now() >= decoded.exp * 1000) {
throw new Error('Token has expired');
}Validate Token Issuer (iss)
highCheck that tokens come from expected issuers to prevent token confusion attacks.
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'your-app-name',
audience: 'your-api'
});Validate Token Audience (aud)
highEnsure tokens are intended for your application by validating the audience claim.
// Token should specify intended audience
const payload = {
sub: 'user123',
aud: 'api.yourapp.com', // Your API endpoint
iss: 'yourapp.com'
};Validate 'Not Before' Claim (nbf)
mediumIf using nbf claim, ensure tokens are not used before their valid time.
if (decoded.nbf && Date.now() < decoded.nbf * 1000) {
throw new Error('Token not yet valid');
}📦 Storage and Transmission Security
Use Secure Storage Methods
highStore tokens in httpOnly cookies or secure storage. Avoid localStorage for sensitive tokens.
✅ Secure Options
- • httpOnly cookies
- • Secure cookies
- • SameSite cookies
- • Memory (temporary)
⚠️ Use with Caution
- • localStorage (XSS risk)
- • sessionStorage
- • Regular cookies
❌ Never Use
- • URL parameters
- • Referer headers
- • Unencrypted storage
- • Plain text files
Use Authorization Header
highSend tokens in Authorization header with Bearer scheme, not in URL or body.
// ✅ Correct way
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// ❌ Never in URL
https://api.example.com/data?token=eyJhbGc...Implement CSRF Protection
highUse CSRF tokens or double-submit cookie pattern when storing JWTs in cookies.
// Set secure cookie with CSRF protection
res.cookie('jwt', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});Configure CORS Properly
mediumSet specific origins in CORS, never use wildcards (*) for credentials.
// ✅ Specific origins
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true
// ❌ Never with credentials
Access-Control-Allow-Origin: *🛡️ Application Security
Implement Refresh Token Strategy
highUse refresh tokens for long-lived sessions to minimize exposure of access tokens.
// Access token: short-lived (15-60 minutes)
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// Refresh token: longer-lived (7-30 days)
const refreshToken = jwt.sign({ type: 'refresh' }, secret, { expiresIn: '7d' });Implement Token Blacklisting
highMaintain a blacklist for revoked tokens, especially for logout and security incidents.
// Store revoked token IDs in Redis/database
await redis.setex('blacklist:' + jti, expTime, 'true');
// Check blacklist on each request
const isBlacklisted = await redis.get('blacklist:' + decoded.jti);Implement Rate Limiting
mediumAdd rate limiting to token endpoints to prevent brute force and abuse.
Login Endpoint
- • 5 attempts per minute per IP
- • 10 attempts per hour per user
- • Progressive delays
Token Refresh
- • 10 requests per minute
- • Monitor for unusual patterns
- • Implement circuit breakers
Implement Security Logging
mediumLog security events for monitoring and incident response.
// Log security events
logger.warn('Invalid JWT signature', { ip, userAgent, token: token.slice(0,20) });
logger.info('Token refreshed', { userId, ip });
logger.error('Expired token used', { userId, expired: decoded.exp });Set Security Headers
mediumConfigure security headers to protect against common attacks.
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'🧪 Development and Testing
Secure Environment Configuration
criticalUse environment variables for secrets, never hardcode in source code.
❌ Never Do This
const secret = 'mysecretkey123';
const secret = 'hardcoded-in-repo';✅ Always Do This
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('Missing JWT_SECRET');Use Different Secrets Per Environment
highDevelopment, staging, and production should use completely different secrets.
# Development .env
JWT_SECRET=dev_secret_32_char_minimum_12345
# Production .env
JWT_SECRET=prod_different_secret_67890_xyz
# Never reuse secrets across environmentsTest Security Scenarios
mediumWrite tests for token validation, expiration, and attack scenarios.
// Test cases to include:
// ✓ Expired token rejection
// ✓ Invalid signature detection
// ✓ Malformed token handling
// ✓ Algorithm confusion prevention
// ✓ Missing claims validationRegular Security Audits
mediumConduct regular security reviews and dependency updates.
Monthly Tasks
- • Review token lifetimes
- • Check for library updates
- • Analyze security logs
- • Review access patterns
Quarterly Tasks
- • Rotate signing secrets
- • Security penetration testing
- • Update security policies
- • Team security training
Document Security Decisions
mediumDocument your JWT configuration, security policies, and incident procedures.
- • Document algorithm choices and rationale
- • Record token lifetime decisions
- • Maintain incident response procedures
- • Document key rotation schedule
- • Keep security contact information current
Your JWT Security Score
🚨 Critical vulnerabilities present. Address red items immediately.