HTTP is stateless. Every request arrives with no memory of the one before it, which creates the fundamental problem: a user logs in once, then makes a thousand requests, and each must somehow prove it belongs to that same authenticated person.
There are three answers in common use, and choosing between them is mostly a question about revocation.
The LLD security lesson covered authentication versus authorization and how to store passwords. This lesson is about what happens after a successful login.
Sessions: state on the server
The server creates a record when you log in and gives you an opaque identifier in a cookie.
Login -> server stores { sessionId: "a8f3...", userId: 42, expires: ... }
-> Set-Cookie: session=a8f3...; HttpOnly; Secure; SameSite=Lax
Request -> Cookie: session=a8f3...
-> server looks up a8f3... -> user 42The cookie contains no information — it is a random string that means nothing without the server's record. That is the important property.
Strengths:
- Instant revocation. Delete the record and the session is dead on the next request. Logout, password change, account suspension and "sign out all devices" all work immediately.
- Small requests. A short id rather than a payload on every call.
- Data stays server-side, so nothing sensitive is exposed even if the cookie is read.
Costs:
- Every request needs a lookup, so you need a fast shared store — Redis, typically.
- Shared state across servers, which is the statelessness constraint from the scalability lesson. Sessions in one server's memory produce the random-logout bug.
JWTs: state in the token
A JSON Web Token carries the claims themselves, signed so they cannot be altered.
Three base64 parts separated by dots:
header.payload.signature
{ "alg": "HS256", "typ": "JWT" } header
{ "sub": "42", "role": "admin", "exp": ... } payload
HMAC(header + payload, secret) signatureThe server verifies the signature and trusts the contents. No lookup required — which is the entire appeal.
Strengths:
- No shared session store, so any service can verify independently — genuinely valuable across separately deployed services.
- Self-contained, carrying user id and roles, so downstream services need no lookup.
The decisive weakness: revocation.
Verification is self-contained, so there is nothing to delete. A JWT remains valid until it expires, regardless of what happens meanwhile. If a user logs out, changes their password after a breach, or is dismissed, their token keeps working.
The workarounds all reintroduce state:
- Short expiry (5–15 minutes) shrinks the window but does not close it.
- A denylist of revoked tokens requires a lookup on every request — the exact cost you adopted JWTs to avoid.
- A version number on the user record, checked per request. Same problem.
Refresh tokens: the standard compromise
The pattern most production systems settle on, because it recovers revocation without a lookup on every request.
Two tokens with different jobs:
Access token — a JWT, short-lived (15 minutes), sent with every request, verified with no lookup.
Refresh token — long-lived (days or weeks), stored server-side, used only to obtain a new access token.
Diagram, as text
- Login leads to Access token 15 min, stateless
- Login leads to Refresh token 30 days, in database
- Access token 15 min, stateless leads to Call refresh endpoint (expires)
- Call refresh endpoint leads to Access token 15 min, stateless (checks stored token)
Most requests are stateless and fast. Revocation works, because deleting the stored refresh token stops renewal — the user is locked out within one access-token lifetime.
You have chosen a bounded staleness window rather than pretending revocation is instant. That honesty is what an interviewer is listening for.
OAuth 2.0: authorisation for third parties
OAuth is frequently misunderstood, so be precise: OAuth is an authorisation framework, not an authentication protocol.
It answers "may this application access that user's data?" — not "who is this user?"
The problem it solves: an app wants your Google Calendar. The disastrous approach is asking for your Google password. OAuth instead lets you authorise limited access without ever revealing your credentials to the app.
The result is a token that is scoped (calendar read only), revocable independently, and expiring — none of which is true of a shared password.
OpenID Connect (OIDC) is the thin layer on top that does provide authentication, adding an ID token containing verified identity. When you click "Sign in with Google", you are using OIDC, not raw OAuth — a distinction worth stating in an interview.
The detailed flows are covered in the Advanced Security lesson.
Where to store tokens in a browser
A question with a genuinely contested answer, and the trade-off matters.
localStorage is readable by any JavaScript on the page. One XSS vulnerability, anywhere — including in a third-party script — and your token is stolen.
Cookies with HttpOnly cannot be read by JavaScript at all, which contains the damage from XSS. They are sent automatically, which enables CSRF, mitigated by SameSite and CSRF tokens.
The trade is real: localStorage is vulnerable to XSS; cookies are vulnerable to CSRF. But CSRF has robust, well-understood mitigations, while XSS token theft is total and silent. Prefer HttpOnly cookies unless you have a specific reason not to.
Choosing
| Sessions | JWT + refresh | |
|---|---|---|
| Revocation | Instant | Within access-token lifetime |
| Per-request lookup | Yes | No |
| Shared state | Required | Only for refresh |
| Cross-service verification | Needs shared store | Independent |
| Complexity | Low | Moderate |
Default to sessions for a normal web application. They are simpler, revocation is immediate, and modern stores scale comfortably. The "sessions don't scale" objection is largely folklore.
Use JWTs with refresh tokens when several independently deployed services must verify identity without a shared session store — the case they genuinely solve.
Check your understanding
Common mistakes
Long-lived JWTs with no revocation path. A 30-day token is a 30-day window for anyone who steals it.
Sensitive data in the payload. It is publicly readable.
Accepting the alg header from the token. Some libraries historically honoured alg: none and skipped verification entirely. Pin the expected algorithm server-side.
Tokens in localStorage. One XSS and every session is compromised.
No expiry at all. A token that never expires is a permanent credential.
Confusing OAuth with authentication. OAuth grants access to resources; OIDC establishes identity.
Practice
Design session handling for a product with a web app, a mobile app and a public API. Say what each uses and why they differ.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
The threats differ by client. Match the mechanism to the threat, not to a preference for consistency.
Show a strong answer
Web app — a cookie holding an opaque session ID, with HttpOnly, Secure and SameSite=Lax.
The browser is the environment where cross-site scripting is a live risk, and HttpOnly is the only thing that stops a script reading the credential. An opaque session ID rather than a JWT because a browser session is exactly the case where you want server-side revocation — logging out, disabling an account, or invalidating every session after a password change should be immediate.
Mobile app — a short-lived access token plus a refresh token in the platform's secure storage.
There is no browser, so no CSRF and no DOM to inject into, and the cookie's advantages largely disappear. Mobile sessions are expected to last weeks, which rules out a server-side session that must be kept alive indefinitely. Rotate the refresh token on every use: if the old one is ever presented again, both copies are in circulation and the session should be terminated.
Public API — API keys or OAuth client credentials, never user sessions.
Machines do not log in interactively, and a session that expires overnight is an outage for an integration. Keys are long-lived, scoped, individually revocable and auditable per client. Support multiple active keys per account so rotation does not require downtime.
Why they differ rather than being unified: the threats differ. A browser has XSS and CSRF; a mobile app has device theft; an API client has credential sprawl in configuration files. Forcing one mechanism on all three means adopting the weakest fit twice.
What should be shared: the identity itself, the permission model, and the audit trail. Unify the authorisation decisions, not the credential format.
A team wants 'log out from all devices' and 'sessions expire after 30 days of inactivity'. They currently use stateless JWTs with no server-side state. Explain the conflict and resolve it.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Both features are questions about state. Ask where that state can live.
Show a strong answer
The conflict, stated plainly: both features require the server to know something about sessions that the token cannot carry. "Log out everywhere" means invalidating tokens already issued; "expire after inactivity" means tracking when the session was last used. A stateless token is, by definition, valid on its own until it expires — no server knowledge is consulted, so no server decision can intervene.
Resolution — introduce exactly the state needed, and no more:
- Keep access tokens stateless and short, say 5–15 minutes. They are validated by signature alone on every request, which preserves the performance property that motivated JWTs.
- Make refresh tokens stateful. Store a row per session: user, device, issued time, last used time, revoked flag. Refresh is infrequent — once every few minutes at most — so a database lookup there is affordable in a way that one per request would not be.
- "Log out from all devices" becomes revoking every session row for that user. Existing access tokens remain valid for their few remaining minutes, which is the residual window and should be stated as such rather than described as instant.
- "Expire after 30 days of inactivity" becomes a check on refresh: if
last_usedis older than 30 days, refuse and require a fresh login. Updatelast_usedon each refresh.
The general principle: stateless tokens are not an all-or-nothing choice. Put the statelessness where the volume is (per-request validation) and the state where the control is (per-session management). Teams get into trouble by treating "stateless" as an architectural identity rather than as a performance technique applied where it pays.
If the residual window is unacceptable — a compromised account must lose access this second — then access tokens must be checked against a revocation list, and you should shorten their lifetime rather than pretend the window is not there.
Key takeaways
- HTTP is stateless, so every scheme is a way of proving continuity between requests.
- Sessions store state server-side and revoke instantly, at the cost of a lookup and shared state.
- JWTs carry signed claims and need no lookup — but are signed, not encrypted, and cannot be revoked.
- Access token plus stored refresh token is the standard compromise: stateless in the common path, revocable within a bounded window.
- Rotate refresh tokens so reuse of a stolen one is detectable.
- OAuth is authorisation; OIDC is authentication. "Sign in with Google" is OIDC.
- Prefer
HttpOnlycookies over localStorage: CSRF has good mitigations, XSS token theft does not. - Default to sessions; reach for JWTs when independent services must verify without shared state.
Further reading
- RFC 6749, The OAuth 2.0 Authorization Framework, and RFC 9700, Best Current Practice for OAuth 2.0 Security.
- RFC 7519, JSON Web Token (JWT), and RFC 8725, JSON Web Token Best Current Practices.
- RFC 6265bis, Cookies: HTTP State Management Mechanism —
SameSite,HttpOnly,Secure. - NIST Special Publication 800-63B, Digital Identity Guidelines: Authentication and Lifecycle Management.
- OWASP, Session Management Cheat Sheet — rotation, fixation and invalidation.