How JWTs work and the security mistakes developers make with them
JWT structure, signing algorithms, HS256 vs RS256, none algorithm attack, algorithm confusion, expiry claims, JWT secret strength, token storage
JSON Web Tokens
A JWT is a base64url-encoded JSON object containing claims, signed by the server. The signature lets the server verify the token was issued by it without a database lookup. The payload is NOT encrypted by default—anyone can decode and read it.
JWT Vulnerabilities
The none algorithm attack: Some libraries allow the algorithm to be set to none in the token header, bypassing signature verification. An attacker modifies claims (like role: admin) and sets alg: none.
Algorithm confusion (RS256 to HS256): If your server uses RS256 (asymmetric), an attacker can change the header to HS256 and sign the token with your public key (which is public). If the library picks up the algorithm from the token header, it will use the public key as the HMAC secret.
Secure JWT Implementation
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // Min 256-bit (32+ chars) random string
// Sign — include expiry always
const token = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
// Verify — specify algorithm explicitly, never trust the header
try {
const payload = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}Token Storage
Store tokens in httpOnly cookies (inaccessible to JavaScript) to protect against XSS exfiltration. localStorage and sessionStorage are readable by any script on your page.
