JWT Security: Best Practices and Common Pitfalls
Introduction
JSON Web Tokens (JWT) have become the de facto standard for API authentication and authorization. However, improper implementation can lead to severe security vulnerabilities. This article explores common JWT security issues and demonstrates how to implement them correctly.
What is JWT?
A JWT is a compact, URL-safe token that contains claims about a user. It consists of three parts:
- Header: Token type and algorithm
- Payload: Claims (user data)
- Signature: Cryptographic signature
Common Security Pitfalls
1. Algorithm Confusion Attack
javascript// VULNERABLE: Accepting any algorithm const jwt = require('jsonwebtoken'); function verifyTokenVulnerable(token, secret) { // Attacker can change algorithm to 'none' return jwt.verify(token, secret); } // SECURE: Explicitly specify allowed algorithms function verifyTokenSecure(token, secret) { return jwt.verify(token, secret, { algorithms: ['HS256'] // Only allow HMAC SHA-256 }); }
2. Weak Secrets
javascript// VULNERABLE: Weak secret const weakSecret = 'secret123'; const token = jwt.sign({ userId: 42 }, weakSecret); // SECURE: Strong, random secret const crypto = require('crypto'); const strongSecret = crypto.randomBytes(64).toString('hex'); const secureToken = jwt.sign({ userId: 42 }, strongSecret, { algorithm: 'HS256', expiresIn: '15m' });
3. Missing Expiration
javascript// VULNERABLE: No expiration const badToken = jwt.sign({ userId: 42 }, secret); // SECURE: Short-lived access tokens const accessToken = jwt.sign( { userId: 42, type: 'access' }, secret, { expiresIn: '15m' } ); // Longer-lived refresh tokens (stored securely) const refreshToken = jwt.sign( { userId: 42, type: 'refresh' }, refreshSecret, { expiresIn: '7d' } );
Complete Secure Implementation
Token Generation (Node.js)
javascriptconst jwt = require('jsonwebtoken'); const crypto = require('crypto'); class TokenService { constructor() { this.accessSecret = process.env.JWT_ACCESS_SECRET; this.refreshSecret = process.env.JWT_REFRESH_SECRET; if (!this.accessSecret || !this.refreshSecret) { throw new Error('JWT secrets not configured'); } } generateTokenPair(userId, roles = []) { const payload = { userId, roles, iat: Math.floor(Date.now() / 1000) }; const accessToken = jwt.sign( { ...payload, type: 'access' }, this.accessSecret, { algorithm: 'HS256', expiresIn: '15m', issuer: 'your-app-name', audience: 'your-app-api' } ); const refreshToken = jwt.sign( { ...payload, type: 'refresh', jti: crypto.randomUUID() }, this.refreshSecret, { algorithm: 'HS256', expiresIn: '7d', issuer: 'your-app-name' } ); return { accessToken, refreshToken }; } verifyAccessToken(token) { try { return jwt.verify(token, this.accessSecret, { algorithms: ['HS256'], issuer: 'your-app-name', audience: 'your-app-api' }); } catch (error) { throw new Error('Invalid access token'); } } verifyRefreshToken(token) { try { return jwt.verify(token, this.refreshSecret, { algorithms: ['HS256'], issuer: 'your-app-name' }); } catch (error) { throw new Error('Invalid refresh token'); } } } module.exports = new TokenService();
Authentication Middleware
javascriptconst tokenService = require('./tokenService'); function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN if (!token) { return res.status(401).json({ error: 'Access token required' }); } try { const payload = tokenService.verifyAccessToken(token); // Check token type if (payload.type !== 'access') { return res.status(403).json({ error: 'Invalid token type' }); } req.user = payload; next(); } catch (error) { return res.status(403).json({ error: 'Invalid or expired token' }); } } module.exports = { authenticateToken };
Token Refresh Endpoint
javascriptconst express = require('express'); const tokenService = require('./tokenService'); const router = express.Router(); router.post('/refresh', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { return res.status(401).json({ error: 'Refresh token required' }); } try { // Verify refresh token const payload = tokenService.verifyRefreshToken(refreshToken); if (payload.type !== 'refresh') { return res.status(403).json({ error: 'Invalid token type' }); } // Check if refresh token is revoked (check against database/Redis) const isRevoked = await checkTokenRevocation(payload.jti); if (isRevoked) { return res.status(403).json({ error: 'Token has been revoked' }); } // Generate new token pair const tokens = tokenService.generateTokenPair( payload.userId, payload.roles ); // Optionally: revoke old refresh token await revokeToken(payload.jti); res.json(tokens); } catch (error) { return res.status(403).json({ error: 'Invalid refresh token' }); } }); module.exports = router;
Best Practices Summary
- Use Strong Secrets - Generate cryptographically secure random secrets (minimum 256 bits)
- Specify Algorithms - Explicitly define allowed algorithms, avoid ‘none’
- Set Expiration - Use short-lived access tokens (15 minutes) and longer refresh tokens
- Implement Token Refresh - Use refresh token rotation for better security
- Store Tokens Securely - Never store tokens in localStorage; use httpOnly cookies when possible
- Add Claims Validation - Validate issuer, audience, and other claims
- Implement Token Revocation - Maintain a blacklist/whitelist for critical operations
- Use HTTPS Only - Always transmit tokens over encrypted connections
Token Storage: Client-Side
javascript// VULNERABLE: localStorage localStorage.setItem('token', accessToken); // Vulnerable to XSS // SECURE: httpOnly cookie (set from server) res.cookie('accessToken', accessToken, { httpOnly: true, // Not accessible via JavaScript secure: true, // HTTPS only sameSite: 'strict', // CSRF protection maxAge: 15 * 60 * 1000 // 15 minutes });
Conclusion
JWT security requires careful implementation of multiple layers of protection. By following these best practices—using strong secrets, validating algorithms, implementing proper expiration, and securing token storage—you can build a robust authentication system.
For a comprehensive security review of your authentication implementation, contact the Whitespots team for professional assessment and recommendations.


