Auth is where juniors copy a tutorial and seniors ask "but how do I log someone out?" The whole subject becomes clear once you internalise a single distinction: a session is state the server stores and looks up; a JWT is a signed token the server can verify without storing anything. Every trade-off below falls out of that one difference.
Authentication is not authorization
First, separate the two ideas, because mixing them causes real bugs. Authentication answers "who are you?" (verifying identity). Authorization answers "what are you allowed to do?" (checking permission). You can be authenticated but not authorized (you're logged in, but you can't delete that invoice: a 403). You can be unauthenticated hitting a public page (a 200). This deep dive is about authentication; authorization is its own topic.
Sessions: the server remembers you
The oldest model, and still excellent. On login, the server creates a session (a record of who you are and when it expires) and gives the client an opaque random session ID, usually in a cookie. On every request the client sends the cookie back, and the server looks up the session to know who you are.
Because the server holds the truth, revocation is trivial: delete the session row and the user is logged out everywhere, instantly. The cost is that the server must store sessions and do a lookup on every request, so at scale the session store is usually Redis (fast, shared across all your servers, with TTLs that auto-expire sessions).
JWTs: the server verifies you without remembering
A JSON Web Token is self-contained. It has three base64url parts joined by dots: a header (the algorithm), a payload (claims like user ID, role, expiry), and a signature. The server signs the token with a secret key on login. On later requests it verifies the signature with that key, and if it's valid, the server trusts the payload, with no database lookup.
Crucially, the header and payload are only encoded, not encrypted. Anyone can read them. Decode one yourself and see.
The JWT footguns that cause real breaches
Never put secrets in the payload; it's readable by anyone holding the token. Never store JWTs in localStorage, because any XSS on your page can read and steal them; use HttpOnly cookies the JavaScript can't touch. Never allow the none algorithm, and never trust the algorithm named in the token's own header (an attacker controls it). Always verify with a vetted library like jose. Most JWT breaches are one of these four mistakes.
The trade-off that decides everything
DecisionSessions for easy revocation; JWTs for stateless scale.
A JWT's strength (no server-side state, verify anywhere) is exactly its weakness: because the server stores nothing, it cannot revoke a JWT before it expires without building a blocklist, which reintroduces the very state it was trying to avoid. A session's lookup-per-request cost is exactly what makes instant logout and "log out all devices" trivial. For a banking app where immediate revocation is non-negotiable, that cost is worth paying. For a massive stateless API where you can tolerate a short window after logout, the JWT's scale is worth the weaker revocation.
The access + refresh token dance
The popular middle path keeps most of the JWT's benefit while limiting the revocation problem. You issue two tokens:
Short-lived access token
A JWT that lives 5 to 15 minutes. Used on every API request, verified statelessly with no lookup. Because it expires fast, a stolen one is useful only briefly.
Long-lived refresh token
An opaque token stored server-side, living days or weeks. Its only job is to mint new access tokens.
Refresh when the access token expires
The client trades the refresh token for a fresh access token. The server can revoke the refresh token (it's stored), so logout is bounded to the short access-token lifetime.
Rotate refresh tokens
Each refresh issues a new refresh token and invalidates the old one. If an old one is reused, you know it was stolen and can kill the whole chain.
This gives you stateless verification on the hot path (every API call) and revocability on the cold path (refresh), with the exposure window of a stolen access token capped at minutes.
Where to store tokens in the browser
This is a security decision, not a convenience one. The options and their risks:
HttpOnly cookie
The cookie is sent automatically and JavaScript cannot read it, which defeats XSS token theft. The risk it introduces is CSRF (the browser attaches the cookie even on forged cross-site requests), which you counter with SameSite cookie settings and CSRF tokens. The standard, safer choice for browser apps.
localStorage
Easy to use from JavaScript, and that's the problem: any XSS, including one in a third-party script, can read the token and exfiltrate it. There is no CSRF risk, but the XSS exposure is usually the worse trade. Avoid for anything sensitive.
"Log in with Google": OAuth and OIDC
You don't want to store passwords if you can avoid it, so you delegate to a provider. OAuth 2.1 is an authorization-delegation protocol; OIDC (OpenID Connect) adds an identity layer on top so you also learn who the user is. The flow modern apps use is Authorization Code with PKCE:
Redirect to the provider
The user clicks "Log in with Google." Your app sends them to Google with a one-time code challenge (PKCE).
User authenticates with Google
They log in on Google's site. Your app never sees their Google password.
Google redirects back with a code
A short-lived authorization code lands on your callback URL.
Exchange the code for tokens
Your server swaps the code plus the PKCE verifier for tokens. PKCE proves the same client that started the flow is finishing it, blocking code interception.
Do not build this yourself
OAuth/OIDC has a dozen subtle failure modes (redirect URI validation, state parameter for CSRF, PKCE, token validation, key rotation) and getting any of them wrong is a breach. Use a vetted library like openid-client or a provider (Auth0, Clerk, Stytch). This site uses Clerk. "Roll your own auth" is one of the few places where the senior move is to write less code, not more.
The one idea to take away
A session is something the server remembers (easy to revoke, costs a lookup); a JWT is something the server can verify without remembering (scales statelessly, hard to revoke). The access + refresh pattern blends them: short stateless access tokens on every request, a revocable refresh token to renew them. Store tokens in HttpOnly cookies, never localStorage, and never hand-roll OAuth. Pick based on whether instant revocation or stateless scale matters more for your app.
Test yourself
Questions· say the answer out loud before you open it. If you can't, the chapter isn't done.
QWhat's the core difference between sessions and JWTs?+
A session is state the server stores and looks up on each request, so the server holds the truth and can revoke instantly. A JWT is a signed, self-contained token the server verifies without storing anything, so it scales statelessly but can't be revoked before expiry without a blocklist. Every other trade-off follows from this.
QWhy can't you simply 'log out' a JWT?+
Because the server stores nothing about it; a valid signature is enough to be trusted until the token expires. To revoke early you'd maintain a blocklist of revoked tokens, which reintroduces the server-side state JWTs exist to avoid. This is why short-lived access tokens plus a revocable refresh token are the common compromise.
QIs a JWT payload secret?+
No. The header and payload are base64url-encoded, not encrypted, so anyone holding the token can read them. The signature only proves the token is genuine and untampered, not that its contents are hidden. Never put secrets in a JWT payload.
QExplain the access + refresh token pattern and what it buys you.+
You issue a short-lived (minutes) stateless access token used on every request, and a long-lived revocable refresh token stored server-side whose only job is minting new access tokens. You get stateless verification on the hot path plus bounded revocation: a stolen access token is useful only briefly, and revoking the refresh token stops renewal. Rotating refresh tokens on each use detects theft.
QWhere should you store auth tokens in a browser, and why?+
In HttpOnly cookies, which JavaScript can't read, defeating XSS token theft; counter the resulting CSRF risk with SameSite and CSRF tokens. Avoid localStorage, because any XSS (including a compromised third-party script) can read and exfiltrate tokens from it. The XSS exposure is usually worse than the CSRF risk.
QWalk through Authorization Code with PKCE.+
Redirect the user to the provider with a one-time code challenge; the user authenticates there (you never see their password); the provider redirects back with a short-lived authorization code; your server exchanges that code plus the PKCE verifier for tokens. PKCE proves the client finishing the flow is the one that started it, blocking authorization-code interception.
QWhy shouldn't you implement OAuth yourself?+
Because it has many subtle, security-critical details (redirect URI validation, the state parameter against CSRF, PKCE, token validation, key rotation), and getting any one wrong is a breach. Use a vetted library like openid-client or a provider such as Clerk or Auth0. Here the senior move is writing less auth code, not more.
Comments
Loading comments…