Sessions, Tokens, and JWT
After you authenticate once, something has to keep you logged in without re-proving your identity on every request — a session or a token. The choice between a server-side session (an opaque ID pointing to state the server holds) and a self-contained token like a JWT (claims the server signs and trusts on sight) shapes revocation, scale, and a whole class of vulnerabilities.
This topic makes the tradeoff concrete and catalogs the JWT footguns — alg:none, secrets in the payload, tokens in localStorage — that turn a login system into an open door on api.meridian.example.
Sessions vs Self-Contained Tokens
A session ID is a reference the server looks up in its own store — trivial to revoke, but it needs that shared state to scale across servers. A JWT carries signed claims the server validates without any lookup — stateless and easy to scale, but hard to revoke before it expires, because any server that trusts the signature accepts it. That is the core tradeoff: instant revocation versus stateless scale.
Anatomy of a JWT
A JWT has three parts — a header, a payload of claims, and a signature — joined by dots and base64-encoded. The signature is what makes the claims trustworthy; the payload is merely encoded, not encrypted, so anyone can read it. A JWT authenticates its claims but does not hide them, which means a secret placed in the payload is a secret published to every holder of the token.
# a JWT is three base64url parts: header.payload.signature
# anyone can read the payload without the key:
echo "$JWT" | cut -d. -f2 | base64 -d
# {"sub":"user-91","role":"admin","exp":1735689600}
# the SIGNATURE is what must be verified — against a server-pinned algorithm and key,
# never the algorithm the token's own header claims
Decoding the middle segment shows the claims in the clear — proof that a JWT hides nothing. The only part that matters for trust is the signature, and the server must verify it against an algorithm and key it chose, never the algorithm the token itself names. That last point is the root of the most common JWT attacks.
The alg:none and Confusion Attacks
Two classic breaks come from trusting the token's own header. Accepting an alg:none token means accepting an unsigned token as valid. And an algorithm-confusion attack switches the header from RSA to HMAC and signs with the server's public key — which the server, if it trusts the header, uses as the HMAC secret. Both are defeated the same way: pin the expected algorithm and key on the verifier and reject anything else.
Revocation, Expiry, and Storage
Because a valid JWT is trusted until it expires, long-lived JWTs cannot be pulled after a compromise — the fix is short-lived access tokens paired with a revocable refresh token, or a server-side session where instant revocation matters. And where the browser keeps the token decides its exposure: an HttpOnly cookie is unreachable by JavaScript, while localStorage is readable by any XSS (Chapter 6), so one script injection exfiltrates every session.
Server-side session — an opaque ID with state on the server; trivially revocable, but needs a shared session store to scale. Use when instant revocation matters.
JWT — signed, stateless claims; scales horizontally, but cannot be revoked before expiry and leaks its payload to anyone who reads it. Use short-lived, with refresh tokens, for stateless APIs.
- Trusting the JWT's own
algheader — allowingnoneor an RSA-to-HMAC swap lets an attacker forge tokens; always verify against a server-pinned algorithm and key. - Putting secrets in the JWT payload — it is base64, not encryption, so anyone can read the claims.
- Issuing long-lived JWTs with no revocation path, so a stolen token stays valid for its whole lifetime after a breach.
- Storing tokens in
localStorage, where a single XSS exfiltrates every user's session. - Skipping validation of
exp,aud, andiss, so expired or wrong-audience tokens are accepted.
- Pin the expected signing algorithm and key on the verifier; never let the token pick its own verification.
- Keep access tokens short-lived and pair them with a revocable, rotating refresh token; use server-side sessions where you need an instant kill switch.
- Treat JWT payloads as public — put no secrets in them, and validate
exp,aud, andisson every request. - Store session and refresh tokens in
HttpOnly,Secure,SameSitecookies rather than JavaScript-reachable storage. - Regenerate the session or rotate the refresh token on privilege change, so a fixated token cannot be reused.
Knowledge Check
What is the core tradeoff between a server-side session and a JWT?
- Sessions are revocable but need state; JWTs scale statelessly but resist revocation
- JWTs are always encrypted end to end, while server sessions are stored only as plaintext
- Sessions cannot be used with more than one server
- JWTs are inherently more secure in every situation
Why must a JWT verifier pin the expected signing algorithm rather than trust the token's header?
- The header enables alg:none and RSA-to-HMAC forgery
- The header algorithm field is always missing in real tokens
- Pinning the algorithm makes verification faster
- The payload cannot be read unless the algorithm is pinned
Why is storing a session token in localStorage riskier than in an HttpOnly cookie?
- localStorage is readable by JavaScript, so any XSS can steal the token
- localStorage tokens are always transmitted in plaintext over the network
- Cookies cannot be stolen under any circumstances
- localStorage automatically shares tokens across domains
You got correct