Use this checklist to ensure your JWT implementation follows security best practices. Each item is critical for production security.
π Quick Checklist
| Category | Item | Status |
|---|---|---|
| Key Management | 256+ bit cryptographically random secret for HS256 | β |
| 2048+ bit RSA keys for RS256 | β | |
| Different secrets per environment (dev/staging/prod) | β | |
| Secrets stored in secure storage (not in code) | β | |
| Token Security | Token expiration set (exp claim) | β |
| Expiration time reasonable (15 min - 24 hours) | β | |
| Issuer claim set (iss) | β | |
| Audience claim set (aud) | β | |
| No sensitive data in payload | β | |
| Verification | Algorithm explicitly specified in verify() | β |
| "none" algorithm rejected | β | |
| Expiration always verified | β | |
| Issuer and audience validated | β | |
| Transport | Tokens sent via Authorization header (not URL) | β |
| HTTPS enforced | β | |
| HttpOnly cookies if storing in browser | β | |
| Key Rotation | Rotation plan documented | β |
| kid claim used for multi-key support | β | |
| Rotation tested in staging | β |
π Key Management
// GOOD: Strong key generation
const crypto = require('crypto');
// HS256: 256 bits minimum
const hs256Secret = crypto.randomBytes(32).toString('hex');
// RS256: 2048 bits minimum
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});Generate secure keys: JWT Secret Generator
Environment Separation
# Development
JWT_SECRET_DEV=dev_secret_abc123...
JWT_ISSUER_DEV=myapp-dev
# Staging
JWT_SECRET_STAGING=staging_secret_def456...
JWT_ISSUER_STAGING=myapp-staging
# Production
JWT_SECRET_PROD=prod_secret_ghi789...
JWT_ISSUER_PROD=myapp-prodπ« Token Security
Minimum Payload
// GOOD: Minimal payload
const token = jwt.sign({
sub: '12345', // Subject (user ID)
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + 3600, // Expires in 1 hour
iss: 'myapp', // Issuer
aud: 'myapp-api' // Audience
}, secret);
// BAD: Sensitive data in payload
const badToken = jwt.sign({
sub: '12345',
password: 'hashed_password', // NEVER!
email: 'user@example.com', // Consider carefully
ssn: '123-45-6789' // NEVER!
}, secret);Recommended Expiration Times
| Token Type | Recommended TTL | Use Case |
|---|---|---|
| Access Token | 15 min - 1 hour | API access |
| Refresh Token | 7 - 30 days | Get new access tokens |
| Session Token | 24 hours | Web sessions |
| Email Verification | 24 - 48 hours | One-time actions |
| Password Reset | 15 - 60 minutes | One-time actions |
β Verification
Secure Verification Pattern
const jwt = require('jsonwebtoken');
function verifyToken(token, secret, issuer, audience) {
try {
return jwt.verify(token, secret, {
algorithms: ['HS256'], // Explicit algorithm
issuer: issuer, // Validate issuer
audience: audience, // Validate audience
ignoreExpiration: false, // Always check exp
clockTolerance: 10 // 10 second clock skew
});
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (err.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
} else if (err.name === 'NotBeforeError') {
throw new Error('Token not active');
}
throw err;
}
}Express Middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
req.user = verifyToken(token, process.env.JWT_SECRET, 'myapp', 'myapp-api');
next();
} catch (err) {
return res.status(401).json({ error: err.message });
}
}
// Usage
app.get('/protected', authMiddleware, (req, res) => {
res.json({ user: req.user });
});π Transport
Client-Side (Browser)
// GOOD: Store in HttpOnly cookie (set by server)
// Server:
res.cookie('jwt', token, {
httpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});
// BAD: Store in localStorage
localStorage.setItem('jwt', token); // Vulnerable to XSS!API Requests
// GOOD: Authorization header
fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// BAD: Query parameter
fetch(`/api/users?token=${token}`); // Leaks to logs!π Key Rotation
Rotation Checklist
- Generate new key with new kid
- Add new key to verification (but don't activate)
- Deploy with both keys valid for verification
- Activate new key for signing
- Wait for grace period (longer than max token lifetime)
- Remove old key from verification
See detailed guide: JWT Secret Rotation Without Downtime
π Monitoring
What to Monitor
- Token verification failures - Spike could indicate attack
- Token age distribution - Many old tokens? Check client implementation
- Key usage - Which keys are being used after rotation
- Failed algorithm checks - Potential algorithm confusion attack
π Testing
Test your implementation with:
- JWT Validator - Decode and verify tokens
- JWT Fuzzer - Test for vulnerabilities
- JWT Encoder - Create test tokens
Summary
Production JWT checklist:
- Strong keys (256+ bits) from secure random source
- Token expiration (15 min - 24 hours)
- Explicit algorithm verification
- No sensitive data in payload
- HTTPS + Authorization header
- HttpOnly cookies in browser
- Key rotation plan with kid support
- Monitoring and alerting
Generate secure keys: JWT Secrets Homepage