JWT / JWS
A JWT (JSON Web Token) is three base64-url-encoded parts: header, payload, signature. Used for authentication, API access, password resets. Easy to use badly — pin the algorithm, validate everything.
Sign, verify, common mistakes
EXAMPLE
import jwt from 'jsonwebtoken';
import crypto from 'node:crypto';
const SECRET = process.env.JWT_SECRET; // at least 32 random bytes
// 1) SIGN — pin algorithm + expiry + audience + issuer
function signSession(userId) {
return jwt.sign(
{ sub: String(userId) }, // payload claims
SECRET,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp.web',
jwtid: crypto.randomUUID(),
},
);
}
// 2) VERIFY — REQUIRE the algorithm, audience, issuer
function verifySession(token) {
return jwt.verify(token, SECRET, {
algorithms: ['HS256'], // CRITICAL — pin
issuer: 'myapp',
audience: 'myapp.web',
clockTolerance: 5, // seconds
});
}
// 3) Asymmetric — RSA or EdDSA when the verifier shouldn't have the signing key
import { generateKeyPairSync } from 'node:crypto';
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
const token = jwt.sign({ sub: '42' }, privateKey, { algorithm: 'EdDSA', expiresIn: '15m' });
const payload = jwt.verify(token, publicKey, { algorithms: ['EdDSA'] });
// 4) COMMON MISTAKES
// a) alg=none accepted — set algorithms explicitly
// b) Algorithm confusion — server expects RS256 but library treats HMAC
// Always pass `algorithms: [...]`.
// c) Putting passwords / sensitive PII in payload — it's BASE64, not encrypted.
// d) Long-lived access tokens — keep them SHORT (5-15 min) + refresh in HttpOnly cookies.
// e) Storing JWTs in localStorage — XSS reads them. Prefer HttpOnly cookies.
// f) No revocation strategy — once issued, can't recall. Keep TTL short + a revocation list (jti → revoked) for emergencies.
// 5) Two-token pattern
// Access token — JWT, 15 min, sent in Authorization header (or HttpOnly cookie)
// Refresh token — random opaque string, 30 days, server-side stored, HttpOnly cookie
// On refresh: rotate the refresh token, invalidate the old one.
// 6) Revocation list — Redis with jti as key, TTL = remaining token life
await redis.setex(\`revoked:${decoded.jti}\`, decoded.exp - now(), '1');
if (await redis.exists(\`revoked:${decoded.jti}\`)) throw new Error('revoked');
// 7) NEVER trust the alg field from the token — always specify what YOU expect
jwt.verify(token, KEY, { algorithms: ['HS256'] }); // REJECT anything else
Why it matters
algorithms: […] is the most-skipped JWT argument. Without it, alg=none attacks and HMAC / RSA confusion are open doors. Pin the algorithm everywhere you verify.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// JWT: header.payload.signature. Use HS256 (HMAC) for symmetric, EdDSA for asymmetric.
// Set exp, iat, iss, aud. Reject alg=none. Validate signature BEFORE inspecting claims.
const jwt = require('jsonwebtoken');
const token = jwt.sign({ sub }, secret, { algorithm: 'HS256', expiresIn: '15m' });
Try it Yourself »
Exercise
JWT algorithm value to REJECT.
if (header.alg === '
') reject();
Four letters lowercase.
Discussion
Loading…