Engineer's toolkit
JWTs and token validation
A JWT is a signed claim set, not a secret. How to validate one safely, the alg confusion and none attacks, and when a JWT is the wrong tool.
A JSON Web Token (JWT) is three base64url parts joined by dots: a header, a payload of claims, and a signature. The payload might say sub (who), aud (which service it's for) and exp (when it expires). Any service that holds the verification key can check the signature without calling the issuer. That is why JWTs are popular for APIs.
The payload is readable by anyone. Base64url is encoding, not encryption. Never put a secret in a JWT. Only the signature stops tampering.
Walking through it
The validator has to check several things. Skipping any one of them has caused real breaches.
def verify(token, key, audience):
head, body, sig = token.split(b".") # sketch: b64url helpers and sign() omitted
if json.loads(unb64u(head))["alg"] != "HS256":
raise ValueError("alg not allowed") # pin the algorithm
expected = b64u(hmac.new(key, head + b"." + body, hashlib.sha256).digest())
if not hmac.compare_digest(expected, sig):
raise ValueError("bad signature")
claims = json.loads(unb64u(body))
if claims["aud"] != audience or claims["exp"] < time.time():
raise ValueError("bad claims")
return claimsRun against a valid token for orders-api, plus two bad ones, the full version of this code prints:
user_42
rejected: alg not allowed
rejected: bad signatureThe first line is the valid token's sub. The none token is rejected by the algorithm pin. The token signed with the wrong key is rejected by the signature check.
The known attacks
alg: none. Some early libraries accepted an unsigned token whenever the header saidnone. The attacker just edits the claims.- Algorithm confusion. A service expects
RS256, verifying with the public RSA key. The attacker sends anHS256token and signs it with that public key. The library treats the public key as an HMAC secret, and the forgery passes. Fix it by pinning the expected algorithm in code, never by reading it from the header. - Missing
aud,issorexpchecks. A token issued for a billing API gets accepted by the orders API. Expired tokens are accepted forever. - Weak HMAC secrets. An
HS256secret that is short or guessable can be brute-forced offline, because the attacker has the whole token.
Tip
Use a maintained library and pass the expected algorithm, issuer and audience explicitly. Don't hand-roll verification in production. The sketch above shows what the library should be doing.
When a JWT is the wrong tool
A JWT can't be revoked before it expires. If a user is banned or a laptop is stolen, the token stays valid until exp. Two fixes are common:
- Keep access tokens short-lived, around minutes, and use a refresh token that the server can revoke.
- Check a revocation list or the session store on every request, which removes most of the benefit of stateless tokens.
For a single web app with one backend, a server-side session is usually simpler and easier to revoke. JWTs earn their place when many independent services must verify identity without a shared session store.
Common mistakes
- Trusting the
algheader, or acceptingnone. - Putting personal data or secrets in the payload.
- Using long-lived tokens with no revocation path.
- Skipping
audandisschecks, so tokens from one service work on another.
Check yourself
- Why does pinning the algorithm stop algorithm confusion? Answer: the verifier decides the algorithm, so an attacker can't turn an RSA public key into an HMAC secret.
- Why is a JWT payload not private? Answer: it is base64url-encoded, which anyone can decode. Only the signature provides integrity.
Key takeaways
- A JWT is signed, not encrypted. Its contents are readable by anyone.
- Pin the algorithm, and check the signature,
exp,audandiss. - Access tokens are hard to revoke, so keep them short-lived.
- Use a session when one backend holds all the state.
Further learning
The OAuth 2.0 Threat Model and Security Best Current Practice (RFC 9700) covers token misuse in detail.
RFC 9700: OAuth 2.0 Security Best Current PracticeThis document describes best current security practice for OAuth 2.0. It updates and extends the threat model and security advice given in RFCs 6749, 6750, and 6819 to incorporate practical experiences gathered since …rfc-editor.orgNext, see how these tokens are issued in OAuth and OpenID Connect.
Related
Authentication and authorization
Authentication proves who you are, authorization decides what you can do, and service-to-service calls need their own identity. The map for the rest of this topic.
HMAC and webhook signing
A keyed hash proves a message came from a key holder and was not changed. It protects webhooks, signed URLs and TOTP codes.
Sessions and cookies
How a browser login stays alive. Cookie attributes that matter, session fixation, CSRF, and why server-side sessions are easier to revoke than signed tokens.