Lesson 1 of 5
Authentication Fundamentals
"Identity is the first thing a server should establish and the last thing it should assume." — a security engineer, after a long incident review
Every request that arrives at your backend brings an implicit claim: "I am someone, and I am allowed to do this." Both halves of that claim are lies until you prove otherwise. The frontend is untrusted — we established that in the very first chapter — so a request that says "delete account 42" or "transfer $5,000" is just bytes on a wire until the server answers two questions. First: who is this, really? Then: is this who-they-are allowed to do that?
This chapter is about the first question. It is the doorway to the whole part, and one of the most consequential pieces of code you will ever write, because when authentication is wrong it is wrong catastrophically — accounts taken over, data leaked, money moved. By the end you'll know the difference between authentication and authorization, why a stateless protocol like HTTP forces a genuine design choice to "stay logged in," the two dominant answers to that choice with their trade-offs, and the anatomy of a login flow from credentials to logout. You will have run both approaches in the playground.
HTTP is stateless — every request arrives with no memory of the one before it. So after you type your password once, how does the server know it's still you on the next request, and the next, without asking for the password every single time?
Two words people constantly confuse: authN and authZ
They sound alike, they're written alike, and mixing them up produces real security holes. Pin them down now and never conflate them again.
| Authentication (authN) | Authorization (authZ) | |
|---|---|---|
| Question it answers | Who are you? | What are you allowed to do? |
| Happens | First — at the door | After — for each protected action |
| Deals in | Identity, credentials, proof | Permissions, roles, ownership, policy |
| Failure code | 401 Unauthorized |
403 Forbidden |
| Example | "This request is from user 42, verified" | "User 42 may read invoice 9 but not delete it" |
The names of the HTTP status codes are a historical cruelty: 401 Unauthorized actually means un-authenticated ("I don't know who you are — log in"), while 403 Forbidden means authenticated but not permitted ("I know exactly who you are, and the answer is still no"). Memorize that inversion; interviewers love it and production code gets it wrong constantly.
Analogy A hotel. Authentication is the front desk checking your ID and confirming you have a reservation — establishing who you are. Authorization is what your key card actually opens: your room, the gym, maybe the executive lounge, but never the manager's office or someone else's room. The clerk knowing your name (authN) tells them nothing about which doors should unlock (authZ). This chapter is the front desk. The next is the key card.
This chapter is entirely about authentication. Authorization — roles, permissions, ownership checks, least privilege — is the chapter that follows, and it's where most access-control bugs actually live.
Why "logged in" is a hard problem at all
Here is the crux. HTTP is stateless: the server treats every request as a fresh stranger. Request N+1 carries no built-in memory that request N — the one where you typed your password — ever happened. That statelessness is a feature (it's why HTTP scales to billions of requests across pools of interchangeable servers), but it means "staying logged in" is not free. The server cannot simply remember you, because the next request might land on a different machine entirely, or arrive an hour later after your first server has restarted.
So authentication is really two problems glued together:
- Verify credentials once — check the password (or the OAuth grant, or the passkey signature) and confirm you are who you claim.
- Carry proof forward — hand the client something it will present on every subsequent request, which the server can check cheaply to re-establish "this is user 42" without re-checking the password.
That second piece is the design decision. There are two dominant answers, and choosing between them is a genuine architectural fork with real trade-offs — a favorite interview topic precisely because there's no single right answer.
The two big approaches: sessions vs tokens
After you log in, the server issues one of two things. Either it remembers you itself (session-based) or it hands you a self-describing pass (token-based).
Session-based (stateful). The server creates a session record — "session a1b2c3 belongs to user 42, created now, expires in 2 weeks" — and stores it server-side (in memory, Redis, or a database). It sends the client only an opaque session id: a random, meaningless string. On each later request the client sends that id back, the server looks it up in its session store, and finds the user. The id itself contains nothing; it's a coat-check ticket. All the information lives on the server.
Token-based (stateless). The server creates a signed token — most famously a JWT (JSON Web Token) — that contains the claims ("user 42, expires at ...") and is cryptographically signed with a secret only the server knows. It hands the whole token to the client. On each later request the client sends the token back, and the server verifies the signature to trust the contents — no lookup, no database. The token is a self-carrying, tamper-evident passport. (JWTs are signed, not encrypted — the contents are readable by anyone; the signature only guarantees they weren't altered. We devote a full chapter to JWTs shortly.)
Here is the trade-off table to hold in your head:
| Session-based (stateful) | Token-based (stateless) | |
|---|---|---|
| Where the truth lives | Server-side store (Redis/DB) | Inside the signed token itself |
| What the client holds | Opaque, meaningless session id | Self-describing signed token (readable claims) |
| Verifying a request | Look up the id in the store (a read) | Verify the signature locally (no I/O) |
| Scaling across servers | Needs a shared store all servers reach | Any server can verify alone — scales beautifully |
| Revocation (logout / ban) | Easy — delete the session record | Hard — a valid signed token works until it expires |
| Typical transport | Cookie | Cookie or Authorization header |
| Main risk to mind | CSRF (the cookie rides along automatically) | Token theft + no easy kill switch; secret leakage |
| Good fit | Classic web apps, when instant logout matters | APIs, microservices, mobile, cross-service auth |
The essential tension: sessions trade scalability for control (easy revocation, but every request touches a shared store), while tokens trade control for scalability (verify anywhere with zero I/O, but you can't easily un-issue one). Real systems often blend them — short-lived tokens plus a server-side refresh/blocklist — which is how you get "scales well" and "can log people out." We'll build toward that in the JWT chapter.
Where JWTs came from Session cookies are as old as dynamic web apps — the server-remembers-you model dates to the 1990s. JWTs (RFC 7519, 2015) rose with two forces: single-page apps talking to APIs, and microservices where a request hops between many services that all need to know who's calling without every one of them phoning a central session store. A signed, self-contained token that any service can verify solved that. The cost — you gave up the easy "delete the session" kill switch — is a debt the industry is still paying down with refresh tokens and blocklists.
Cookies: the transport, not the mechanism
People say "cookie-based auth" as if cookies were an authentication method. They aren't — a cookie is just a transport: a small string the server sets and the browser automatically sends back on every request to that site. You can carry either a session id or a token in a cookie. What makes cookies matter for auth are three flags you must set:
| Flag | What it does | Why it matters |
|---|---|---|
HttpOnly |
JavaScript cannot read the cookie (document.cookie won't see it) |
Blocks token/session theft via XSS — the single most important flag |
Secure |
Sent only over HTTPS | Stops the credential from leaking over plaintext HTTP |
SameSite |
Controls whether the cookie rides along on cross-site requests (Strict / Lax / None) |
The primary defense against CSRF for cookie auth |
Set-Cookie: sid=a1b2c3d4e5; HttpOnly; Secure; SameSite=Lax; Max-Age=1209600; Path=/
The alternative to a cookie is the client storing the token itself and sending it explicitly in a header — Authorization: Bearer <token> — which is the norm for mobile apps and APIs. The key security difference: a cookie is sent automatically by the browser (convenient, but that's exactly what enables CSRF), whereas a header must be attached deliberately by client code (immune to CSRF, but if you stash the token in localStorage it's readable by any XSS). There is no free lunch; there is only choosing which risk you defend against and defending it well.
The login flow, end to end
Strip away the framework and every authentication scheme is the same four beats:
- Verify credentials. The client sends proof (password, OAuth grant, passkey signature). The server checks it against what it stored — for passwords, by comparing against a hash, never a plaintext password (its own chapter is coming). Wrong credentials →
401, and say as little as possible about why. - Issue proof of session. On success, mint either a session (store record, hand back opaque id) or a token (sign claims, hand back the token). Attach it — usually via
Set-Cookie. - Attach on later requests. The client sends the id/token back on every subsequent request (cookie automatically, or header deliberately).
- Verify on each request. The server re-establishes identity cheaply — look up the session, or verify the token's signature — and now knows "this is user 42" for the rest of the request. If it's missing, expired, or invalid →
401.
Let's make both mechanisms real. This first block models the session-based approach: an in-memory session store you can create, look up, and destroy, driving a full login → request → logout flow.
Notice the two things that define session auth: the client only ever held sid (an opaque string that means nothing on its own), and logout was instant — one delete and the credential is dead server-side.
Now the token-based approach. There is no store to look up; the server proves the token is genuine by re-computing and comparing a signature. We simulate the signature with a simple deterministic function — real HMAC-SHA256 does not run in this sandbox, but the shape is exactly this: sign = f(payload, secret), and verify = recompute and compare.
Run both and the trade-off becomes visceral. The session version had a store to delete from, so logout was one line — but every whoAmI had to consult that store. The token version needed no store at all to verify, so it scales across any number of servers — but there was nothing to delete on logout, which is exactly the revocation problem the table warned about.
Technique — never leak which credential was wrong: Both a bad email and a bad password must return the identical 401 "Invalid credentials", as the session block does. If "unknown email" and "wrong password" differ, an attacker can enumerate which emails have accounts (user enumeration). For the same reason, avoid timing side-channels: verify the password hash even for a non-existent user (against a dummy hash) so a missing account doesn't return conspicuously faster.
Where credentials actually come from
Step 1 — "verify credentials" — has several front doors, and modern apps offer more than one:
- Password. The classic: the user picks a secret, you store its hash. Simple and universal, but users reuse and leak passwords, so it demands hashing, rate limiting, and ideally a second factor. Its own chapter follows.
- OAuth / social login ("Sign in with Google/GitHub"). You delegate the credential check to a provider the user already trusts; they hand you a proof that "this is Ada's Google account." You never see the password. Great UX and one less secret to guard — a full OAuth/OIDC chapter is coming.
- Magic link. No password at all: the user enters an email, you send a one-time signed link, and clicking it proves control of the inbox. Simple and phishing-resistant-ish, but only as secure as the email account.
- Passkeys / WebAuthn. The modern gold standard. The device holds a private key in secure hardware and signs a challenge; the server stores only the public key. Nothing shared or reusable can be stolen, and it's inherently phishing-resistant. Increasingly the default for new apps.
Whichever front door verifies identity, steps 2–4 (issue proof, attach, verify) are identical. The credential source is pluggable; the session/token machinery underneath is the same.
MFA: proving it twice
A single factor — something you know (a password) — falls to a single leak. Multi-factor authentication (MFA / 2FA) requires proof from two different categories:
- Something you know — a password, a PIN.
- Something you have — your phone (a TOTP code from an authenticator app, or a push approval), a hardware key.
- Something you are — a fingerprint, a face.
The security comes from combining categories: a stolen password is useless without the phone, and a stolen phone is useless without the password. (Two passwords aren't MFA — same category, same single point of failure.) In the flow, MFA slots into step 1: verify the password, then — before issuing any session or token — demand and verify the second factor. Note that SMS codes are the weakest form (SIM-swap attacks); app-based TOTP and hardware keys are meaningfully stronger.
Logout and expiry: ending a session on purpose and by default
A session must end two ways. On purpose (logout): with sessions, delete the server record — done, as you saw. With tokens, you can't delete what you didn't store, so you rely on short expiry plus, if you need instant kill, a server-side blocklist (a bit of state that buys back revocation — a pragmatic hybrid). By default (expiry): every credential carries a lifetime so an abandoned or stolen one dies on its own. Short-lived is safer but nags the user to re-login; the standard resolution is a short-lived access credential paired with a longer-lived refresh mechanism — the balance we'll build carefully in the JWT chapter.
Common Mistakes
- Confusing 401 and 403. Returning
403when the user isn't logged in (should be401), or401when they're logged in but not permitted (should be403). AuthN failure is401; authZ failure is403. - Storing tokens in
localStorageand forgetting XSS.localStorageis readable by any JavaScript on the page — one XSS bug and every token is stolen. PreferHttpOnlycookies so scripts can't read the credential at all. - Leaving cookies unflagged. A session cookie without
HttpOnly,Secure, andSameSiteis a credential begging to be stolen or ridden by CSRF. Set all three, always. - Treating a JWT as revocable. Believing "logout" invalidates a JWT. It doesn't — a signed token is valid until it expires unless you add explicit blocklist state. Design for that from day one, or keep tokens very short-lived.
- Putting secrets in the token. JWTs are signed, not encrypted — the payload is plainly readable. Never put a password, card number, or anything sensitive in the claims.
- Leaking which credential was wrong. Different responses (or timings) for "no such user" vs "wrong password" hand attackers a user-enumeration oracle. Return one identical
401. - Rolling your own crypto. Hand-writing signature or hashing code (like our simulated
fakeSign) in production. Use vetted libraries — never invent the primitives.
Thought experiment You run a bank API on 50 servers behind a load balancer, and any request can land on any server. You must support instant "log out everywhere" the moment fraud is detected. Pure stateless JWTs make verification free but can't be revoked; pure server-side sessions revoke instantly but put a store read on the hot path of all 50 servers. Sketch a hybrid: what would you keep stateless, what minimal state would you add back, and how short would you make the access token's lifetime? (There's a reason the industry converged on short access tokens + a refresh token + a revocation list — you're rediscovering it.)
401; authZ comes after and fails with 403. 401 literally means un-authenticated despite its name "Unauthorized."HttpOnly; Secure; SameSite cookie or an Authorization: Bearer header; each request re-establishes identity by store-lookup (sessions) or signature-verify (tokens). Statelessness makes revocation the hard part: with tokens you keep the access token short-lived (say 5–15 min), pair it with a refresh token, and maintain a small server-side blocklist / token-version check for immediate "log out everywhere" — trading a little state back for control while keeping the hot path mostly stateless.Hands-on Take the token block and add expiry + refresh thinking. Modify issueToken to expire in a very short window, then (1) verify a token, (2) advance "now" past its expiry (hint: temporarily wrap Date.now, or set exp to a past timestamp) and confirm verifyToken now returns expired, and (3) add a blocklist = new Set<string>() plus a revoke(token) function, and make verifyToken reject any token in the blocklist — you've just bought back the revocation that pure statelessness gave away.
Summary
- Authentication (authN) = who you are; authorization (authZ) = what you may do. AuthN is first and fails with
401; authZ is next and fails with403. This chapter is authN; the next is authZ. - HTTP is stateless, so "staying logged in" is a real design choice: verify credentials once, then hand the client something to replay on every request that the server can cheaply re-verify.
- Session-based keeps state server-side and hands out an opaque id — easy revocation, but a store read per request and a shared store to scale. Token-based puts signed claims in a self-verifying token — stateless scale, but hard revocation.
- Cookies are transport, not mechanism; secure them with
HttpOnly,Secure, andSameSite. Headers (Authorization: Bearer) are the alternative, with different XSS/CSRF trade-offs. - Credentials can come from passwords, OAuth/social, magic links, or passkeys/WebAuthn; MFA combines two different factor categories. The session/token machinery underneath is identical regardless of the front door.
- Every credential should expire, and you should be able to end it on purpose — trivial for sessions, a deliberate design (short lifetimes + refresh + blocklist) for tokens.
Knowing who is calling is only half the door. Next we turn to authorization — roles, permissions, ownership, and the principle of least privilege — where we decide what that verified identity is actually allowed to do, and where most real access-control bugs are born.
Editorial reflection Authentication is the one place in a system where being almost right is the same as being wrong. A rate limiter that's 95% correct still mostly works; an auth check that's 95% correct is a breach waiting for the other 5%. That's why the discipline here is humility — reach for vetted libraries, assume every input is hostile, leak nothing, expire everything, and design revocation before you need it. The engineers who build systems people trust with their money and their secrets are not the ones who wrote the cleverest crypto. They're the ones who respected how little room for error the door allows.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.