Part 2 · 2 chapters · ~12 min
JWTs and Their Pitfalls
The structure of a JWT, the validation checklist, algorithm confusion and alg=none, keys and JWKS with key ids, rotation, short expiry and revocation strategies, token size, and when an opaque token with introspection is better.
5
Structure and validation
code
// validate with a maintained library, never by hand
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://auth.bank.example/.well-known/jwks.json'));
export async function verify(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.bank.example', audience: 'ledger-api',
algorithms: ['ES256'], clockTolerance: 30,
});
if (!String(payload.scope ?? '').split(' ').includes('transfers:write')) throw new Error('insufficient_scope');
return payload;
}A JWT, AND EVERY CHECK ON IT
header.payload.signature, and the validation list
swipe the figure sideways, or tap expand for full screen
1/4
header
The header names the algorithm and the key id. Never trust alg blindly: the classic attacks are alg=none and switching RS256 to HS256 using the public key as an HMAC secret.
alg and kidallowlist algorithms; never accept none
6
Revocation, size and opaque tokens
| need | approach |
|---|---|
| revoke quickly | access tokens of 5-15 minutes; revoke the refresh token; for emergencies a small denylist of token ids (jti) checked until expiry |
| sensitive operations | check a session version or "tokens issued after" timestamp per user, bumped on password change or compromise |
| large permission sets | do not stuff hundreds of permissions into the token; check them at an authorisation service (parts 4-5) |
| no shared verification desired | opaque tokens with introspection (RFC 7662): the API asks the auth server; easy revocation, a network hop per request (cache briefly) |