Home/Blog/Development/JWT Security Best Practices: Token Signing, Validation, and Common Vulnerabilities
Development
JWT Security Best Practices: Token Signing, Validation, and Common Vulnerabilities
Master JWT security with this comprehensive guide covering token structure, signing algorithms, validation best practices, secure storage, and common vulnerabilities like algorithm confusion and token leakage.
JSON Web Tokens (JWTs) are the de facto standard for API authentication and authorization. However, their apparent simplicity hides numerous security pitfalls. This guide covers JWT security best practices, common vulnerabilities, and how to implement tokens correctly.
// User record includes token version
const user = {
id: '12345',
email: '[email protected]',
tokenVersion: 3 // Increment to invalidate all tokens
};
// Include version in token
const token = jwt.sign({
sub: user.id,
tokenVersion: user.tokenVersion
}, secret);
// Validate version matches
async function validateToken(decoded) {
const user = await getUser(decoded.sub);
if (decoded.tokenVersion !== user.tokenVersion) {
throw new Error('Token version mismatch - please re-authenticate');
}
}
// Revoke all tokens for a user
async function revokeAllTokens(userId) {
await db.query(
'UPDATE users SET token_version = token_version + 1 WHERE id = $1',
[userId]
);
}
A JWT (JSON Web Token) is a compact, URL-safe token format for securely transmitting claims between parties. It has three Base64URL-encoded parts separated by dots: Header (algorithm and token type), Payload (claims like user ID, expiration), and Signature (cryptographic verification). Example: xxxxx.yyyyy.zzzzz. The payload is NOT encrypted—it's just encoded, so anyone can read it. The signature proves the token hasn't been tampered with.
HS256 (HMAC-SHA256) uses a shared secret—the same key signs and verifies tokens. Simple but requires secure key distribution to all verifiers. RS256 (RSA-SHA256) uses asymmetric keys—private key signs, public key verifies. More complex but safer for distributed systems since only the auth server needs the private key. Use HS256 for single-service scenarios; use RS256/ES256 when multiple services verify tokens or for public key distribution.
Access tokens should be short-lived: 5-15 minutes for high-security scenarios, up to 1 hour for lower-risk APIs. Short expiration limits damage from token theft. Pair with refresh tokens (hours to days) for session continuity. Never use long-lived access tokens (days/weeks)—if compromised, attackers have extended access. The right balance depends on your security requirements, user experience needs, and refresh token implementation.
Include standard claims: iss (issuer), sub (subject/user ID), aud (audience/intended recipient), exp (expiration), iat (issued at), jti (unique token ID for revocation). Add custom claims as needed: roles, permissions, tenant ID. Minimize claims—every claim increases token size and potential data exposure. Never include sensitive data (passwords, SSN, full credit card numbers). Remember: payload is encoded, not encrypted.
Never store JWTs in localStorage or sessionStorage—they're vulnerable to XSS attacks. For web apps, use HttpOnly, Secure, SameSite=Strict cookies. For SPAs without a backend, keep tokens in memory only (JavaScript variables) and use silent refresh. For mobile apps, use platform secure storage (iOS Keychain, Android Keystore). Consider the Backend-for-Frontend (BFF) pattern where your backend manages tokens and issues session cookies.
Algorithm confusion occurs when an attacker changes the alg header from RS256 to HS256 and signs with the public key (treating it as the HMAC secret). If the server naively uses the alg header to select verification, it verifies using the public key as an HMAC secret—which succeeds. Prevention: Never trust the alg header. Configure your JWT library with an explicit, expected algorithm. Always specify allowedAlgorithms during verification.
JWTs are stateless, so revocation requires additional infrastructure:
Token blacklist—store revoked jti values in fast storage (Redis) and check on each request
Short expiration—keep access tokens short-lived so revocation happens naturally
Token versioning—store a version in user record, embed in token, reject if mismatched
Refresh token revocation—revoke refresh tokens so new access tokens can't be issued.
Choose based on your consistency vs. performance tradeoffs.
The "none" algorithm (alg: "none") means no signature—the token is unverified. Vulnerable libraries might accept unsigned tokens if not properly configured. Attackers craft tokens with alg: "none" and no signature, gaining arbitrary access. Prevention: Always reject tokens with alg: "none". Configure your library to require signatures. Use an explicit allowlist of algorithms (e.g., only RS256). Modern libraries are patched, but always verify your configuration.
JWTs work for stateless authentication but have tradeoffs for sessions: Pros—scalable (no session store), works across services, contains user context. Cons—can't revoke instantly, larger than session IDs, payload visible to clients. For traditional web apps, server-side sessions with cookies may be simpler and more secure. JWTs excel in microservices, APIs, and scenarios requiring stateless authentication. Consider hybrid: JWT for auth, sessions for sensitive state.
Validation steps:
Split token into header.payload.signature
Verify algorithm matches your expected algorithm (don't trust header)
For RSA/ECDSA: verify signature using public key from JWKS
For HMAC: verify using your secret
Check exp (expiration) isn't past
Check iat (issued at) isn't future
Verify iss (issuer) matches expected
Verify aud (audience) includes your service.
Use established libraries—don't implement verification yourself.
Building Something Great?
Our development team builds secure, scalable applications. From APIs to full platforms, we turn your ideas into production-ready software.