Hotshard
Security fundamentals

AuthN vs AuthZ, Sessions & JWT

How a website remembers you're logged in — without asking for your password again.

You type your password once. Then you click around the site for an hour — a hundred more requests, and not one of them asks for your password again. Yet every one of those requests somehow knows it's still you. That's the trick this page is about. HTTP has no memory: each request arrives as a stranger, and the server has forgotten you the instant the last reply went out. So 'stay logged in' has to be built on top. The server hands you a token when you log in, and you show that token on every request after. This page builds that token two ways. The first keeps the truth on the server and gives you a claim check — a session. The second packs the truth into the token itself and signs it so it can't be forged — a JWT. Which one you pick decides whether every request costs a lookup, and whether you can actually log someone out. Along the way: authentication versus authorization, where to keep the token without handing it to an attacker, and how a server decides what you're allowed to do.

~14 min read

Start here: HTTP forgets you between requests#

TL;DRthe 30-second version
  • HTTP is stateless — each request arrives with no memory of the ones before it. So after you log in, the very next request has to prove all over again that it's you.
  • The naive fix — send your username and password on every request — is a disaster: your password is exposed constantly, the server pays for a slow password check every time, and you can never really log out.
  • Instead the server authenticates you once, then hands you a token. You attach that token to every later request, and the server trusts the token instead of re-checking your password.
  • Two ways to build the token. A session keeps the real data on the server (a random id in a cookie, the user record in a store), so every request costs a store lookup — but you can delete the session to log someone out. A JWT packs the data into the token itself and signs it, so the server verifies it with no lookup — but you can't easily take it back before it expires.
  • Authentication (who are you?) and authorization (what may you do?) are two different questions. The token proves the first; a separate check answers the second.

You sign in to a website. You type your email and password, the server checks them, and you're in. Now you click to your dashboard, open a report, edit your profile — a hundred requests over the next hour. Here's the awkward part: the server has no memory of your login. HTTP treats every request as brand new, from a stranger it has never seen. So how does request number fifty know it's still you?

The obvious answer is to send your password on every request. It works, and it's a bad idea for three separate reasons. Your password now travels across the network hundreds of times instead of once, so every request is a fresh chance to leak it. Checking a password is deliberately slow — a good login hashes it with a function tuned to burn real CPU time (see /hashing-passwords) — and you do not want to pay that cost on every click. And there's no way to log out: as long as the password is valid, anything holding it is you, forever.

What we actually needThe server should do the expensive check — is this really you? — exactly once, at login. After that, it hands you something cheaper to prove you already passed: a token. Every later request carries the token, and the server trusts it instead of re-checking your password. The rest of this page is about what that token should be. One rule sits under all of it: the token is only as safe as the connection it rides on. Everything here assumes the request travels over TLS (see /tls/sim); over plain HTTP, anyone on the path can copy your token and become you.

Two questions: who are you, and what may you do#

Before we build the token, split the problem in two, because people constantly blur these together. There are two different questions a server asks about a request.

  • Authentication (authn) — who are you? This is proving identity. Typing your password, scanning your fingerprint, tapping a login link in your email: all authentication. The answer is a name — this request is Alice.
  • Authorization (authz) — what are you allowed to do? This is deciding permission. Alice is logged in, but may she delete this repository, or read another user's invoice? The answer is yes or no, per action.

Authentication happens first and once; authorization happens on every protected action. You can be perfectly authenticated and still not authorized — logged in as yourself, but told 'no' when you try to open someone else's account. The token we're about to build answers the first question: it carries proof of who you are. The second question — what you may do — is a separate check the server runs using that identity, and we'll come back to it at the end.

Build it: the session#

Start with the simplest thing that works. At login, once the password checks out, the server writes down what it now knows: this person is Alice, she signed in at 2pm. It saves that record in a store — a fast key-value store, often Redis, which is really just a cache (see /cache/sim) — under a fresh, random id. That id is the session id: a long, unguessable string like sess_7f3a9c… that means nothing on its own. It's a claim check — the coat-check ticket, not the coat.

The server sends that session id back to the browser in a cookie — a small piece of data the browser stores and automatically attaches to every future request to that site. From now on the browser sends the session id along with each request, without you doing anything. The server reads the id, looks it up in the store, finds 'this is Alice,' and serves the request as her.

Login requestemail + password
check password
Server verifiesslow password check — once
remember her
Store: sess_7f3a → Alicesaved server-side
hand back the id
Set-Cookie: sess_7f3abrowser keeps it
…an hour of clicks…
Every later requestcookie sent automatically
one lookup each
Store lookup → Aliceserved as her, no password
One login, then every request after

Logging out is now trivial. The server deletes the session record from the store. The next request still carries the cookie, but the lookup finds nothing, so the server treats it as a stranger and sends you back to login. That delete is the whole reason sessions are easy to revoke: the truth lives on the server, so the server can throw it away whenever it wants.

The cost of rememberingThe session store is now on the critical path. Every single request does a lookup in it before anything else happens. With one server that's fine. With fifty servers behind a load balancer, they all need to see the same sessions, so the store becomes shared state that every request depends on — and if it's slow or down, nobody is logged in. You made login easy to revoke by making every request pay a lookup. Avoiding that cost is exactly what the next design is for.

Build it: the signed token (JWT)#

What if the token carried the truth itself, so the server didn't need a store to look anything up? Instead of a meaningless id, put the actual facts in the token: this is Alice, she's an admin, this expires at 3pm. Now any server can read the token and know who you are — no lookup, no shared store. But there's an obvious hole. If the token just says 'I'm Alice, I'm an admin' in plain text, anyone can write their own that says the same — or edit yours to add 'admin.' The token has to be tamper-proof.

That's what a signature does, and it's the heart of a JSON Web Token, or JWT (say 'jot'). A JWT is three parts joined by dots: header.payload.signature. The header says how the token was signed. The payload holds the claims — the facts about you (sub: alice, role: admin, exp: 3pm). Both are just JSON, Base64url-encoded, which is a reversible text encoding, not encryption — so anyone can read them. The third part is the signature, and it's what makes the whole thing trustworthy.

headerhow it's signed
.
payloadclaims: who you are
.
signatureproof it wasn't changed
A JWT is three parts, dot-separated

Here's how the signature works, because 'it's signed' is the part people wave at. The server holds a secret key that only it knows. To make the signature, it takes the header and payload, mixes them together with the secret through a one-way function (HMAC with SHA-256, named HS256), and gets a short fingerprint. That fingerprint is the signature, and it depends on both the exact contents and the secret. When a token comes back, the server recomputes the fingerprint from the token's header and payload using its secret, then compares. If someone changed the payload to add 'admin,' the recomputed fingerprint won't match the one attached — because producing the matching fingerprint needs the secret, and they don't have it. Tampering is caught; forgery needs the key.

Notice what just happened. To trust the token, the server did some math with a secret it already holds. It never touched a store. Any server with the same secret can verify any token on its own — which is exactly the property sessions couldn't give us. Ten servers, no shared session store, every one of them able to check who you are from the token alone.

What it costs: the stateless trade#

So JWTs delete the lookup and the shared store. That sounds strictly better, so what's the catch? The catch is the mirror image of the session's strength. A session is easy to revoke because the truth lives on the server — delete it and it's gone. A JWT is hard to revoke for the same reason reversed: the truth lives in the token, out in the world, and the server isn't tracking it. Once you hand someone a signed token that says 'Alice, admin, valid until 3pm,' every server will honor it until 3pm. There's nothing to delete.

This is the JWT revocation problem, and it bites in real situations. Alice logs out — but her token is still valid, so a copy of it still works. You fire an employee at 2:05 and cut their database access — but their token is good until 3pm, so for 55 minutes they still get in. You can't cancel a token you aren't keeping track of.

PredictYour JWTs are valid for 24 hours. A user's laptop is stolen at 9am; you notice and 'revoke' their access at 10am by deleting them from your database. Using plain JWTs with no extra machinery, how long can the stolen token still be used — and what's the fix?

Hint: What does the server actually check on each request — the database, or the token's own expiry?

Up to 24 hours from when the token was issued — here, potentially until 9am tomorrow. Deleting the user from your database does nothing, because a stateless JWT is never checked against the database; every server just verifies the signature and reads the exp claim, which still says tomorrow morning. That 24-hour exposure window (issued 9am, expires 9am next day, and you cannot shorten it) is the whole problem with long-lived JWTs. The fix is to make the access token short-lived — minutes, not a day — so the window is tiny, and pair it with a refresh token you can revoke. That's the next paragraph.

The fix is to stop choosing between 'no lookup' and 'can revoke' and take both, by splitting the token in two. Hand out a short-lived access token — a JWT that expires in, say, 15 minutes. That's the one sent on every request, verified with no lookup, fast. Because it dies in 15 minutes, a stolen one is only useful for 15 minutes. Alongside it, hand out a long-lived refresh token, whose only job is to get a new access token when the old one expires.

The refresh token is checked against a store, the way a session is, so you can revoke it. When the access token expires, the client sends the refresh token to a single endpoint; the server looks it up, confirms it's still valid, and issues a fresh 15-minute access token. Revoke someone by deleting their refresh token: their current access token still works for its last few minutes, but they can never get another. You've pushed the expensive, revocable lookup off the every-request path and onto the once-every-15-minutes refresh — cheap verification almost always, a real check occasionally.

Where do you keep the token?#

You have a token. The browser has to store it somewhere and attach it to requests, and there are two common choices. Each one opens a different attack — this is where a lot of real auth bugs live.

Option one is a cookie. The browser stores it and sends it automatically. Mark the cookie HttpOnly and JavaScript on the page can't read it — which matters, because the main threat to a token is XSS (cross-site scripting: an attacker sneaks JavaScript onto your page, and it runs as you). An HttpOnly cookie is invisible to that script, so the script can't steal it. The catch is that 'sent automatically' cuts both ways, and leads to CSRF (cross-site request forgery), covered just below.

Option two is localStorage — a JavaScript-accessible store in the browser — with the app reading the token and attaching it to each request by hand. This dodges CSRF, because nothing is sent automatically. But it's fully exposed to XSS: any script that runs on your page can read localStorage and walk off with the token. Given the choice, most guidance prefers the HttpOnly cookie, because XSS is the more common and more damaging attack.

CSRF — the cookie's catchThe browser attaches your cookie to every request to your site, automatically. So it will attach it even to a request that a different, malicious site triggers while you're visiting there — say a hidden form on that site that quietly POSTs to your bank. That request rides your logged-in cookie, and the server can't tell it wasn't you. That's the CSRF attack. The standard defense is the SameSite cookie attribute, which tells the browser not to send the cookie on requests that come from other sites, plus the Secure attribute so the cookie only ever travels over HTTPS.
Authorization: RBAC vs ABAC

Back to the second question — what may you do? Once the token proves who you are, the server still has to decide permission on each action. Two models dominate.

  • RBAC (role-based access control): give each user roles (admin, editor, viewer), attach permissions to roles, and check the role. Alice is an editor, editors may publish, so Alice may publish. Simple, easy to reason about, the default for most apps. It gets awkward when a permission depends on more than a role — 'editors, but only for their own team's documents.'
  • ABAC (attribute-based access control): decide from attributes of the user, the resource, and the context, evaluated by a rule. 'Allow if user.department equals document.department, and it's business hours.' Far more expressive — what you reach for when access is fine-grained or data-dependent — at the cost of rules that are harder to audit and reason about.

The trade is the usual one: RBAC is simpler and coarser, ABAC is more powerful and more complex. Most systems start with RBAC and add attribute checks only where a role isn't enough.

Sessions or tokens: which to reach for

The honest answer is that sessions are the safe default, and JWTs earn their place when you actually need statelessness. A few rules that hold up.

  • A normal web app with one backend or a few: server-side sessions. They're simple, revocation is free, and the store lookup is cheap. Don't reach for JWTs to solve a problem you don't have.
  • Many services that each need to check identity without a shared session store, or a token that has to cross service boundaries (a microservices backend, an API for third parties): JWTs. Stateless verification is the whole point.
  • Instant logout / revocation as a hard requirement (banking, admin consoles): lean toward sessions, or short-lived access tokens plus revocable refresh tokens. Never long-lived JWTs alone.
  • Don't put secrets in a JWT payload: it's readable by anyone holding the token (Base64url, not encryption). Identity and roles, yes; a password or a card number, never.
The decision, in one table

Side by side, the two designs are mirror images — every strength of one is the other's cost.

Server-side sessionJWT (stateless)
Where the truth livesIn a server-side storeIn the token itself
Cost per requestA store lookupVerify a signature — no lookup
State across serversA shared session storeNone — any server verifies alone
Revoke / log outEasy — delete the recordHard — valid until it expires
A leaked tokenUseless once the record is deletedWorks until expiry
Best fitMost web apps; instant revocationMany services / no shared store; short-lived
In the wild
  • Rails, Django, and PHP have shipped server-side (or signed-cookie) sessions for decades — the boring, reliable default behind a huge fraction of the web.
  • OAuth 2.0 and OpenID Connect (how 'Log in with Google' works) hand your app a bearer token — often a JWT — carrying the user's identity and scopes (the specific permissions the token grants). RFC 6750's 'bearer token' is exactly 'whoever holds it, is them,' which is why it must ride over TLS.
  • Auth0, Clerk, and Firebase Auth are managed identity providers: they run login, issue short-lived access tokens plus refresh tokens, and hand you the JWT verification so you don't build it yourself.
  • Kubernetes and most microservice backends use JWTs (service-account tokens) so any component can verify a caller's identity without calling a central auth server on every request — the stateless property, put to work.
Pitfalls & gotchas
If anyone can read a JWT's payload, isn't that a security hole?

Only if you put secrets in it. A JWT is signed, not encrypted — the payload is Base64url-encoded plain JSON, and anyone holding the token can decode and read it. That's fine for identity and roles, which aren't secret. It's a real leak if you stuff a password, a card number, or private data in there. Rule: treat a JWT payload as public. Sign it to stop tampering, but never rely on it to hide anything.

What is the alg:none attack?

A JWT's header names the algorithm used to sign it, and early libraries trusted that field. An attacker sets alg to 'none,' strips the signature off, and some libraries accepted the token as a validly 'unsigned' one — instant forgery of any claims they like. The defense: never let the library read the algorithm from the token. Pin the expected algorithm on the server and reject anything else. (Some libraries even had to block case tricks like nOnE.)

What's the RS256-to-HS256 confusion attack?

First, the two algorithms use different kinds of keys. HS256 uses one shared secret for both signing and checking. RS256 uses a key pair: a private key that only the server holds signs the token, and a matching public key — safe to publish — checks it. The public key is not a secret; anyone can have it. Now the attack. If the server will verify with whatever algorithm the token names, the attacker switches the header from RS256 to HS256, then signs a forged token using the server's public key as the HMAC shared secret. When the server verifies, it reaches for that same public key and runs HS256 with it — and the signature matches, because the attacker used the exact same key. A key meant only for checking just got used to sign. Same defense: pin the algorithm on the server; don't let the token choose it.

Should I just store the session data in the cookie itself?

You can — a signed cookie puts the data in the cookie and signs it, which is basically a JWT by another name and shares the same revocation problem. The classic server-side session keeps only a random id in the cookie and the data in a store. If you need easy logout, keep the data server-side. If you need statelessness, accept the revocation trade and go short-lived.

QuizYou're building an internal admin console where a fired employee's access must be cut off the instant IT clicks 'revoke.' Which design fits, and which one quietly fails this requirement?

  1. Long-lived JWTs (24h) — they're stateless and fast.
  2. Server-side sessions (or short-lived access tokens + revocable refresh tokens) — the server drops the record immediately; a plain 24h JWT would keep working until it expires.
  3. localStorage JWTs — storing them client-side makes revocation instant.
  4. Either works — revocation isn't affected by the token design.
Show answer

Server-side sessions (or short-lived access tokens + revocable refresh tokens) — the server drops the record immediately; a plain 24h JWT would keep working until it expires.Instant revocation is the requirement, and it's exactly what stateless JWTs are bad at. A plain 24-hour JWT is honored until it expires no matter what IT clicks, because no server checks it against any store — so a fired employee keeps access for up to a day. Server-side sessions revoke for free (delete the record), and short-lived access tokens plus a revocable refresh token get you most of the statelessness while capping exposure to a few minutes. Where the token is stored (cookie vs localStorage) is a separate question about XSS/CSRF; it does nothing for revocation.

In an interview

Auth comes up in almost every system-design interview, usually as 'how do users stay logged in, and how does that scale?' The strong answer names the stateful-versus-stateless trade and its consequence — revocation — instead of just saying 'use JWTs.'

  • Split the two questions first: authentication (who you are) vs authorization (what you may do). Interviewers notice when you conflate them.
  • Describe the session: a random id in a cookie, data in a store, a lookup per request, delete to log out. Then name its scaling cost — a shared session store on the critical path.
  • Describe the JWT: signed claims in the token, verified with no lookup, stateless across services. Then name its cost, unprompted: the revocation problem.
  • Land the resolution: short-lived access tokens plus revocable refresh tokens — the design that gets you stateless verification and real logout. This is the trap most people miss.
  • Mention token storage (HttpOnly cookie vs localStorage → XSS/CSRF) and the JWT-payload-is-readable gotcha. Naming those signals you've actually shipped auth, not just read about it.
References
References

Feedback on this topic →