Token-Based Auth & JWTs
Chapter 4 introduced the stateless (client-side) session and promised to return to it. Here it is. Token-based authentication — most commonly with JWTs — replaces the server-side session lookup with a self-contained, signed token the client carries. It's the backbone of APIs and SPAs, and it's powerful, but it has sharp edges: signing pitfalls, storage dilemmas, and the revocation problem from Chapter 4.
What a JWT Is
A JSON Web Token is three Base64URL parts joined by dots: header.payload.signature. The header names the signing algorithm, the payload holds claims (who the user is, when it expires), and the signature proves the token was issued by the server and not tampered with:
jwt.io and see). The signature only guarantees integrity (it wasn't altered) and authenticity (the server issued it) — it does not hide anything. So never put secrets in a JWT payload (passwords, card numbers, anything sensitive). What stops tampering is that changing a claim invalidates the signature, which the attacker can't recompute without the signing key — but they can always read it.
Access Tokens & Refresh Tokens
The standard pattern splits responsibilities across two tokens to balance the stateless-vs-revocation trade-off (Chapter 4):
| Access token | Refresh token | |
|---|---|---|
| Purpose | authorize each API request | obtain a new access token when it expires |
| Lifetime | short (minutes) | long (days/weeks) |
| Sent | with every request | only to the token-refresh endpoint |
| If stolen | limited window (expires soon) | serious — but can be revoked server-side |
The logic: the access token is short-lived so a stolen one is only useful briefly (mitigating the revocation problem), while the refresh token is long-lived but tightly guarded and revocable — typically tracked server-side so it can be invalidated. This gives you most of stateless scaling with a practical revocation path.
JWT Signing Pitfalls
JWTs have a notorious history of implementation flaws. The classics:
alg: none and algorithm-confusion attacksalg: none — the JWT spec includes a "none" algorithm meaning "unsigned." A naive verifier that trusts the token's own alg header can be handed a token with {"alg":"none"} and no signature — and accept it. The attacker forges any claims they like (e.g. "role":"admin"). Fix: the server must enforce the expected algorithm, never trust the token's header to choose it. (2) Algorithm confusion (RS256 → HS256) — if the server uses RSA (public/private) but a flawed verifier lets an attacker switch alg to HS256 (symmetric), the attacker can sign a forged token using the public key (which is, well, public) as the HMAC secret. Fix: pin the exact algorithm and key type. (3) Weak HMAC secrets — a short/guessable HS256 secret can be brute-forced offline, letting the attacker mint valid tokens. Use a long, random secret. The umbrella rule: use a vetted JWT library, configure the exact algorithm explicitly, and never let the token decide how it's verified.
The Revocation Problem — and How Tokens Address It
A pure JWT is valid until it expires; you can't "delete" a self-contained token (Chapter 4). For logout, stolen-token response, or "ban this user now," you need a strategy:
- Short access-token lifetimes — the simplest mitigation; a stolen access token dies in minutes. The window, not the token, is what you control.
- Revocable refresh tokens — store refresh tokens server-side (a small amount of state) so you can revoke them; on logout/compromise, delete them and the user can't get new access tokens.
- A denylist / blocklist — keep a list of revoked token IDs (the
jticlaim) checked on each request — but this reintroduces server-side state and a lookup, partly defeating statelessness. - Token versioning — store a per-user token version; bump it to invalidate all that user's tokens at once (e.g. on password change).
Where to Store Tokens (the Cookie vs localStorage Debate)
This connects straight back to the CSRF and XSS courses — there's a genuine trade-off, not a single right answer:
| Storage | CSRF risk | XSS risk | Notes |
|---|---|---|---|
| HttpOnly cookie | yes — needs CSRF defences | token not readable by script | auto-sent; safest vs XSS theft |
| localStorage / JS memory | no — not auto-sent | readable by any XSS | must attach via Authorization header |
Storing the token in an HttpOnly cookie protects it from XSS theft but reintroduces CSRF (the cookie auto-sends) — so you add SameSite + CSRF tokens. Storing it in localStorage and sending it in an Authorization: Bearer header avoids classic CSRF but exposes the token to any XSS. As the CSRF course concluded: there's no "JWT = safe" — the risk tracks where you store it. For browser apps, an HttpOnly cookie (plus CSRF defences) is often the safer default, because XSS theft of a bearer token is full account takeover with no expiry help.
JWT vs Server-Side Sessions — When to Use Which
Don't reach for JWTs reflexively. For a single monolithic web app, classic server-side sessions (Chapter 4) are simpler, instantly revocable, and perfectly scalable with a shared store — often the better choice. JWTs shine for stateless APIs, microservices, and cross-service auth where avoiding a shared session lookup genuinely matters. Choose by your architecture, not by fashion.
Hands-On Exercises
Decode the three parts of a JWT and explain what each does. Then explain why you must never store sensitive data in the payload, and what the signature does and doesn't protect (integrity/authenticity vs confidentiality).
📄 View solutionExplain the alg: none and RS256→HS256 algorithm-confusion attacks, including how each forges a valid-looking token. Give the fix for both, and explain why a verifier must never trust the token's own alg header.
Explain the revocation problem for stateless JWTs and how the access/refresh split plus short lifetimes mitigate it. Then describe refresh-token rotation with reuse detection and what it catches. Finally, recommend a token-storage approach for a browser SPA and justify it against CSRF and XSS.
📄 View solutionChapter 7 Quick Reference
- JWT =
header.payload.signature(Base64URL); self-contained, signed, stateless - Signed, not encrypted — payload is readable by anyone; signature gives integrity + authenticity, not confidentiality; never store secrets in it
- Access token (short-lived, every request) + refresh token (long-lived, guarded, revocable) — balances scaling vs revocation
- Signing pitfalls:
alg:none(unsigned accepted), RS256→HS256 confusion (sign with the public key), weak HMAC secret — pin the exact algorithm, never trust the token'salg - Revocation problem: can't delete a self-contained token — use short TTLs, revocable refresh tokens, denylist (
jti), or token versioning - Refresh rotation + reuse detection — new refresh token each use; replay of an old one → revoke the whole family (catches theft)
- Storage: HttpOnly cookie (XSS-safe, needs CSRF defences) vs localStorage (CSRF-free, XSS-exposed) — risk tracks storage, no "JWT = safe"
- JWT vs server-side sessions: sessions simpler & instantly revocable for monoliths; JWTs for stateless APIs/microservices — choose by architecture
- Next chapter: OAuth 2.0 & OpenID Connect — delegated authorization, social login, and the flows