HTTP forgets you after every request. So every auth system — passwords, OAuth, magic links, passkeys — exists to answer one question: once you've proven who you are, how does the server recognize you on the next request? There are only two answers. Get this and the whole field snaps into a grid.
🎯 Mission: a mental model of modern auth + the ability to wire it into a real appAfter you log in, the server hands your browser a credential it sends back on every later request. That credential is either a session ID (a meaningless ticket — the server holds the real data) or a token (a self-contained, signed pass — the server holds nothing). Stateful vs stateless. That single fork — where does the truth live, server-side or in the credential itself — is the spine of every auth system you'll ever read.
HTTP is stateless: each request arrives with no memory of the last one. Logging in is just one request that proves identity. The hard part is the millions of requests after it — the server must re-recognize you without making you re-type a password each time.
So split every auth system into two phases. Authentication (the login event — "prove it's you") is where passwords, OAuth, magic links and passkeys differ wildly. But they all converge on the same second phase: the session (staying logged in), and there you have only two designs.
Here's the move that makes auth click: OAuth2, JWT, passwordless, passkeys aren't competing alternatives on the same axis. They live on different phases. OAuth is a way to do the login; a session ID or token is how you stay logged in afterward. This lesson owns the second phase so the rest of the course can focus on the first.
Whatever the login method, the steady state looks identical. You hold a credential; you attach it to each request; the server checks it and decides if it knows you.
The credential at step ② is almost always carried in a cookie (sent automatically by the browser) or an Authorization header (attached manually by JS / an app). Which carrier you use is a separate choice from which kind of credential it is — we'll untangle that next lesson. For now: focus on what's inside the credential.
On login the server creates a record — { session: "a1b2c3…", user: 42, expires: … } — in its own store (memory, Redis, a DB). It sends back only the random ID. The ID is a meaningless ticket: it carries no information, it just points to the server's record. Every request, the server looks up the ID to find out who you are.
# What the browser receives — just an opaque pointer: Set-Cookie: sid=8f4e2a91c7b6d05f; HttpOnly; Secure; SameSite=Lax # What the SERVER stores (the real truth): sessions["8f4e2a91c7b6d05f"] = { user_id: 42, role: "admin", exp: 1718900000 }
On login the server builds a credential that contains your identity (user 42, role admin, expires …) and cryptographically signs it. It stores nothing. On each request it just verifies the signature — if the math checks out, the token is genuine and unaltered, so the server trusts the claims inside it. The most common format is a JWT (JSON Web Token — a whole future lesson).
# A token (JWT) — three base64 parts: header.payload.signature Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjo0Miwicm9sZSI6ImFkbWluIn0.k3rR…sig └ header ┘ └──────── payload (readable!) ────────┘ └ signature ┘ # Server stores: NOTHING. It re-checks the signature with its secret key each time.
Because one design keeps the truth on the server and the other puts it in the credential, every difference between them flows from that. The big one: revocation. A session is killed instantly — delete the server record and the ticket is dead. A token can't be un-signed; it's valid until it expires, which is why tokens are kept short-lived and paired with a "refresh" mechanism (a later lesson).
| Question | Session ID (stateful) | Token / JWT (stateless) |
|---|---|---|
| Where's the truth? | Server store (Redis/DB) | Inside the credential |
| Server stores per-user? | Yes — a row per session | No — verifies a signature |
| Log out / ban instantly? | Yes — delete the record | Hard — valid until expiry (need a denylist) |
| Scales across many servers? | Needs shared store | Easy — any server can verify |
| Works great for… | Classic single-domain web apps | APIs, mobile, microservices, cross-domain |
| Usually carried in… | A cookie (automatic) | Authorization: Bearer header |
| Size on the wire | Tiny (an ID) | Bigger (carries all the claims) |
Now place the scary words. Notice they're not alternatives to each other — they sit on different phases:
All are login methods — phase ①. They differ in how you prove identity, then they all hand you a session or token.
The two answers to "stay logged in" — phase ③. This lesson. Everything else plugs into one of these.
A protocol to get a token from another service ("Sign in with Google"). It's a fancy way to obtain the credential in this lesson.
How the credential is carried and protected in the browser. Next lesson.
You already have one of these in your browser right now. Let's look at it.
⌥⌘I → Application tab → Cookies → pick the site's domain.xxx.yyy.zzz)? That's a JWT — copy the middle chunk into jwt.io and you'll read your own user id. Just one opaque blob with no dots? That's almost certainly a session ID — meaningless to you, a lookup key to them. You just classified a real-world auth system using only this lesson. (Most big sites use session IDs in httpOnly cookies — exactly the "boring safe default" above.)
Tip: an HttpOnly cookie is invisible to document.cookie in the JS console — that's the point, it's why XSS can't steal it. You can only see it in the Application tab.
Q1. A coworker says "we use JWTs so we don't have to store sessions in the database — but we also need to ban abusive users instantly." What's the tension?
Statelessness and instant revocation are in direct conflict. A signed JWT is valid until it expires no matter what the server wants — there's no record to delete. To ban instantly you must add server state back (a token denylist / short expiry + refresh), which partly gives up the "no storage" benefit they wanted. This trade-off is the heart of §04.
Q2. True or false: a JWT is safe to put a user's private data in, because it's signed.
False. Signed ≠ encrypted. The payload is base64-encoded plaintext that anyone holding the token can read. Signing only guarantees it wasn't altered. Private data does not belong in a JWT payload (§03 warning).
Q3. Where does "Sign in with Google" fit — is it an alternative to sessions/tokens?
No — it's a login method (phase ①). OAuth/OIDC is how you prove identity via Google; after it succeeds your app still issues you a session ID or token to stay logged in (phase ③). They live on different phases, not in competition. (§05.)
You now have the spine. Next lesson: Cookies done right — the carrier almost everything rides in. We'll make HttpOnly, Secure, and especially SameSite concrete, and see exactly how a CSRF attack works and why one cookie flag mostly kills it.
💬 I'm your teacher for this — ask me anything that's fuzzy. Good questions to throw back at me: "show me a real Express/FastAPI example of each", "what's a refresh token then?", or "which should I use for the app I'm building?" (tell me the stack and I'll ground the next lesson in it). · See also: course resources.