Common JWT Implementation Mistakes
Learn from the most dangerous JWT implementation mistakes that developers make. Avoid critical security vulnerabilities and protect your applications with proper JWT practices.
Critical Security Alert
JWT implementation errors are among the most common causes of authentication bypass attacks. Even a small mistake can expose your entire user base to attackers.
๐จ Critical Security Mistakes
๐จ Using Weak or Predictable Secrets
criticalMany developers use simple, short, or predictable secrets for signing JWTs. This makes tokens trivially easy to forge.
โ Wrong Implementation
// โ NEVER do this const secret = 'secret'; const secret = 'myapp'; const secret = '123456'; const secret = 'your-secret-key'; // Default examples const token = jwt.sign(payload, secret);
โ Correct Implementation
// โ
Always do this
const crypto = require('crypto');
const secret = process.env.JWT_SECRET ||
crypto.randomBytes(64).toString('hex');
// Minimum 256-bit (32 bytes) entropy
if (secret.length < 32) {
throw new Error('JWT secret too weak');
}
const token = jwt.sign(payload, secret);๐ฅ Security Impact
Attackers can forge any JWT token, impersonate any user, and gain complete access to your application. This is a complete authentication bypass.
๐ ๏ธ How to Fix
- โข Generate secrets with at least 256 bits of entropy
- โข Use cryptographically secure random generators
- โข Store secrets in environment variables, never in code
- โข Use different secrets for different environments
- โข Rotate secrets regularly
๐จ Algorithm Confusion Attacks
criticalNot specifying or validating the signing algorithm allows attackers to change the algorithm and bypass security.
โ Wrong Implementation
// โ Accepts any algorithm
jwt.verify(token, publicKey);
// โ Vulnerable to 'none' algorithm
jwt.verify(token, secret, { algorithms: ['HS256', 'none'] });
// โ Using public key for HMAC
jwt.verify(token, publicKey, { algorithms: ['HS256', 'RS256'] });โ Correct Implementation
// โ
Always specify expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });
// โ
For RSA keys, only allow RS algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// โ
Never allow 'none' algorithm in production
const allowedAlgorithms = process.env.NODE_ENV === 'test'
? ['HS256', 'none'] : ['HS256'];๐ฅ Security Impact
Complete authentication bypass. Attackers can create tokens with no signature or trick servers into using public keys as HMAC secrets.
๐ ๏ธ How to Fix
- โข Always specify the algorithms parameter
- โข Never include 'none' in production algorithms
- โข Use different validation for symmetric vs asymmetric keys
- โข Validate algorithm in JWT header matches expectation
- โข Implement algorithm allowlist, not blocklist
๐จ Storing Sensitive Data in JWTs
criticalJWTs are encoded (base64), not encrypted. Any sensitive data in the payload is visible to anyone who has the token.
โ Wrong Implementation
// โ NEVER put sensitive data in JWTs
const payload = {
userId: 123,
password: 'user-password', // Visible to anyone!
ssn: '123-45-6789', // Major privacy violation
creditCard: '1234-5678-9012-3456', // Financial data exposed
apiSecret: 'internal-api-key', // Internal secrets leaked
email: '[email protected]',
role: 'admin'
};โ Correct Implementation
// โ
Only include non-sensitive, verifiable data
const payload = {
sub: 'user123', // User identifier (non-sequential)
username: 'johndoe', // Public information
role: 'admin', // Authorization info
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 15), // 15 min
jti: randomUUID() // Token ID for revocation
};
// Store sensitive data server-side, reference by user ID๐ฅ Security Impact
Immediate exposure of passwords, personal data, and internal secrets to anyone with token access. Massive privacy violations and regulatory compliance issues.
๐ ๏ธ How to Fix
- โข Never include passwords or password hashes
- โข Don't put SSN, PII, or financial data in tokens
- โข Avoid API keys or internal secrets
- โข Use non-sequential user IDs
- โข Keep payload minimal - only authorization data
- โข Encrypt JWTs if sensitive data is required (JWE)
๐จ Ignoring Token Expiration
criticalNot setting expiration times or not validating them properly allows tokens to live forever, dramatically increasing attack windows.
โ Wrong Implementation
// โ No expiration set
const token = jwt.sign(payload, secret);
// โ Not validating expiration
const decoded = jwt.decode(token); // Doesn't verify!
console.log(decoded.userId);
// โ Extremely long expiration
const token = jwt.sign(payload, secret, {
expiresIn: '10y' // 10 years!
});โ Correct Implementation
// โ
Always set appropriate expiration
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15 minutes for access tokens
});
const refreshToken = jwt.sign(refreshPayload, secret, {
expiresIn: '7d' // 7 days for refresh tokens
});
// โ
Always verify, which includes expiration check
try {
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256']
});
// Token is valid and not expired
} catch (error) {
// Handle expired or invalid token
return res.status(401).json({ error: 'Token invalid' });
}๐ฅ Security Impact
Stolen tokens remain valid indefinitely. Account takeovers persist even after users change passwords or administrators detect breaches.
๐ ๏ธ How to Fix
- โข Set short expirations (15-60 minutes) for access tokens
- โข Use refresh tokens for longer sessions
- โข Always use jwt.verify(), never jwt.decode() for auth
- โข Implement token refresh flow
- โข Consider JTI-based token revocation for immediate invalidation
โ ๏ธ High-Risk Implementation Errors
โ ๏ธ Insecure Token Storage
highStoring JWTs in localStorage or other client-side storage vulnerable to XSS attacks.
โ Wrong Implementation
// โ Vulnerable to XSS attacks
localStorage.setItem('jwt', token);
sessionStorage.setItem('jwt', token);
// โ Accessible to malicious scripts
document.cookie = `jwt=${token}`;
// โ In URL parameters - logged everywhere
window.location = `/dashboard?token=${token}`;โ Correct Implementation
// โ
Secure httpOnly cookie (preferred)
res.cookie('jwt', token, {
httpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000 // 15 minutes
});
// โ
Authorization header (for SPAs)
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// โ
Store in memory only (most secure for SPAs)
let authToken = null; // In-memory storage๐ฅ Security Impact
XSS attacks can steal tokens and impersonate users. Tokens exposed in logs, referrer headers, and browser history.
๐ ๏ธ How to Fix
- โข Use httpOnly, secure, sameSite cookies when possible
- โข For SPAs, store tokens in memory, not localStorage
- โข Implement token refresh flow for short-lived tokens
- โข Never include tokens in URLs
- โข Use CSP headers to prevent XSS
โ ๏ธ Missing HTTPS/TLS Protection
highTransmitting JWTs over unencrypted HTTP connections exposes tokens to interception.
โ Wrong Implementation
// โ HTTP transmission exposes tokens
fetch('http://api.example.com/login', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
// โ Mixed content vulnerabilities
const API_URL = 'http://api.example.com'; // HTTP API
const FRONTEND_URL = 'https://app.example.com'; // HTTPS frontendโ Correct Implementation
// โ
Always enforce HTTPS
const API_URL = 'https://api.example.com';
// โ
HSTS headers
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
// โ
Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});๐ฅ Security Impact
Man-in-the-middle attacks can intercept tokens. Complete authentication bypass over insecure networks.
๐ ๏ธ How to Fix
- โข Enforce HTTPS in production everywhere
- โข Set HSTS headers with long max-age
- โข Use secure cookie flags
- โข Implement certificate pinning for mobile apps
- โข Monitor for mixed content warnings
โ ๏ธ Insufficient Token Validation
highNot validating all required claims or accepting tokens from unexpected sources.
โ Wrong Implementation
// โ Minimal validation
const decoded = jwt.verify(token, secret);
const userId = decoded.sub; // Trusting without validation
// โ Not checking issuer/audience
// Accepts tokens from any issuer
// โ Not validating custom claims
if (decoded.role === 'admin') {
// Allowing any role claim without verification
}โ Correct Implementation
// โ
Comprehensive validation
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'your-app.com',
audience: 'api.your-app.com',
maxAge: '15m'
});
// โ
Validate required claims
if (!decoded.sub || !decoded.role) {
throw new Error('Missing required claims');
}
// โ
Validate claim values
const allowedRoles = ['user', 'admin', 'moderator'];
if (!allowedRoles.includes(decoded.role)) {
throw new Error('Invalid role claim');
}
// โ
Additional security checks
if (decoded.role === 'admin' && !isFromTrustedIP(req.ip)) {
throw new Error('Admin access from untrusted location');
}๐ฅ Security Impact
Token confusion attacks, privilege escalation, and acceptance of malicious tokens from other applications.
๐ ๏ธ How to Fix
- โข Validate issuer (iss) and audience (aud) claims
- โข Check all required claims are present
- โข Validate claim values against allowlists
- โข Implement additional context validation
- โข Log and monitor validation failures
โ ๏ธ No Token Revocation Strategy
highOnce issued, JWTs cannot be revoked without additional infrastructure, making it impossible to immediately invalidate compromised tokens.
โ Wrong Implementation
// โ No way to revoke tokens
app.post('/logout', (req, res) => {
// JWT tokens remain valid until expiration
res.json({ message: 'Logged out' });
});
// โ Compromised account with no immediate remedy
app.post('/change-password', async (req, res) => {
await updatePassword(userId, newPassword);
// Old JWT tokens still work!
res.json({ message: 'Password changed' });
});โ Correct Implementation
// โ
Implement token blacklist
const blacklistedTokens = new Set(); // Or use Redis
app.post('/logout', authenticateToken, (req, res) => {
blacklistedTokens.add(req.token.jti); // Blacklist by JWT ID
res.json({ message: 'Logged out successfully' });
});
// โ
Check blacklist on each request
function authenticateToken(req, res, next) {
const token = extractToken(req);
const decoded = jwt.verify(token, secret);
if (blacklistedTokens.has(decoded.jti)) {
return res.status(401).json({ error: 'Token revoked' });
}
req.user = decoded;
next();
}
// โ
Implement refresh token rotation
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
// Validate refresh token and issue new access token
// Invalidate old refresh token (rotation)
});๐ฅ Security Impact
Unable to immediately invalidate compromised tokens. Logout doesn't actually log users out. Security incidents persist until token expiration.
๐ ๏ธ How to Fix
- โข Implement JWT ID (jti) based blacklisting
- โข Use Redis or database for scalable blacklist storage
- โข Implement refresh token rotation
- โข Consider short-lived tokens with refresh flow
- โข Provide emergency token revocation endpoints
โก Common Implementation Pitfalls
โก Clock Skew and Timing Issues
mediumNot accounting for clock differences between servers can cause valid tokens to be rejected or invalid tokens to be accepted.
โ Wrong Implementation
// โ Strict timestamp validation
const decoded = jwt.verify(token, secret);
if (decoded.iat > Date.now() / 1000) {
throw new Error('Token from future');
}
// โ No clock skew tolerance
if (decoded.nbf && decoded.nbf > Date.now() / 1000) {
throw new Error('Token not yet valid');
}โ Correct Implementation
// โ
Allow for clock skew
const clockSkew = 60; // 60 seconds tolerance
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
clockTolerance: clockSkew
});
// โ
Manual validation with tolerance
const now = Math.floor(Date.now() / 1000);
if (decoded.nbf && (decoded.nbf - clockSkew) > now) {
throw new Error('Token not yet valid');
}
// โ
Server time synchronization
// Ensure all servers use NTP for time sync๐ฅ Security Impact
Legitimate users get randomly rejected due to server clock differences. Valid tokens may be incorrectly accepted.
๐ ๏ธ How to Fix
- โข Set clockTolerance in JWT libraries (30-60 seconds)
- โข Synchronize server clocks with NTP
- โข Monitor time drift between servers
- โข Use UTC for all timestamp calculations
- โข Log timing validation failures for debugging
โก Oversized JWT Tokens
mediumIncluding too much data in JWTs creates large tokens that impact performance and may hit browser/server limits.
โ Wrong Implementation
// โ Including large data sets
const payload = {
userId: 123,
preferences: { /* 2KB of user preferences */ },
permissions: [ /* 100 permission objects */ ],
profile: {
bio: "Very long biography...", // Large strings
avatar: "data:image/jpeg;base64,..." // Base64 image!
},
auditLog: [ /* Last 50 user actions */ ]
};
// Results in 8KB+ tokens!โ Correct Implementation
// โ
Minimal payload
const payload = {
sub: 'user123',
role: 'admin',
permissions: ['read', 'write'], // Just permission names
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (15 * 60)
};
// โ
Store large data server-side
const userPreferences = await getUserPrefs(userId);
const userProfile = await getUserProfile(userId);
// โ
Reference by ID, fetch when needed
const tokenPayload = {
sub: userId,
role: userRole,
prefVersion: userPrefVersion // Version for cache invalidation
};๐ฅ Security Impact
Poor performance, HTTP header size limits exceeded, mobile app crashes, and increased network usage.
๐ ๏ธ How to Fix
- โข Keep payloads under 1KB when possible
- โข Store large data server-side, reference by ID
- โข Use version numbers for cache invalidation
- โข Paginate permissions instead of including all
- โข Consider JWE (encryption) for larger payloads
โก Poor Error Handling
mediumNot properly handling JWT validation errors can leak information to attackers or create poor user experience.
โ Wrong Implementation
// โ Exposing internal error details
try {
const decoded = jwt.verify(token, secret);
} catch (error) {
// Leaks implementation details
res.status(401).json({
error: error.message, // "jwt signature is invalid"
stack: error.stack, // Reveals code structure
secret: secret // NEVER expose secrets!
});
}
// โ Generic error handling
app.use((error, req, res, next) => {
res.status(500).json({ error: 'Something went wrong' });
// No logging, can't debug issues
});โ Correct Implementation
// โ
Secure error handling
try {
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256']
});
req.user = decoded;
next();
} catch (error) {
// โ
Log detailed error internally
logger.warn('JWT validation failed', {
error: error.message,
ip: req.ip,
userAgent: req.get('User-Agent'),
tokenPrefix: token ? token.substring(0, 20) : 'missing'
});
// โ
Return generic error to client
return res.status(401).json({
error: 'Authentication failed',
code: 'INVALID_TOKEN'
});
}
// โ
Differentiate error types for better UX
function handleJWTError(error, res) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
}
// Generic response for other errors
return res.status(401).json({
error: 'Invalid token',
code: 'INVALID_TOKEN'
});
}๐ฅ Security Impact
Information disclosure to attackers, poor debugging capabilities, and confusing user experience during authentication failures.
๐ ๏ธ How to Fix
- โข Log detailed errors server-side only
- โข Return generic error messages to clients
- โข Use error codes for client-side handling
- โข Implement proper monitoring and alerting
- โข Never expose secrets or internal paths
๐งช Testing Your JWT Implementation
๐ Security Test Cases
- โขExpired Token Test: Verify expired tokens are rejected
- โขInvalid Signature Test: Modify token signature and verify rejection
- โขAlgorithm Confusion: Try changing algorithm in header
- โขNone Algorithm: Test tokens with "alg": "none"
- โขMalformed Token: Send invalid base64 or JSON
- โขMissing Claims: Remove required claims and test
๐ ๏ธ Security Tools
jwt.io
Decode and verify JWTs online
OWASP ZAP
Security testing proxy with JWT support
Burp Suite
Professional security testing platform
jwt-cracker
Test JWT secret strength
Implement JWT Security Correctly
Now that you know the common mistakes, generate secure JWT tokens with proper configuration. Use our tool to create tokens with strong secrets and correct settings.