Authentication in Web Apps — The Complete Guide
HTTP has no memory. Every request arrives at your server as a stranger, and nothing in the protocol says who sent it. Authentication is the whole apparatus we build to answer that question once, cheaply, and then carry the answer forward. Almost every design argument you will have about login systems is really an argument about where that answer is stored.
Step one: proving identity at all
Before anything is carried anywhere, a user has to prove they are who they claim to be. For passwords that means one rule above all others: never store the password. Store the output of a slow, salted, memory-hard hash.
import argon2 from 'argon2';
// Registration — argon2 embeds the salt and parameters in the output string.
const hash = await argon2.hash(password, { type: argon2.argon2id });
// Login — constant-time comparison, no manual salt handling.
const ok = await argon2.verify(hash, submittedPassword);
Argon2id is the current default recommendation; bcrypt remains acceptable and is still everywhere. What is not acceptable is a fast general-purpose hash like SHA-256, with or without a salt. Fast is the enemy here: a GPU will try billions of SHA-256 candidates per second against a stolen table, and only a deliberately slow function makes that expensive.
Two more rules that cost almost nothing. Return the same error message and take roughly the same time whether the account does not exist or the password was wrong — otherwise the login form becomes an account enumeration oracle. And rate limit by IP and by account, because credential-stuffing attacks spread one attempt across thousands of accounts rather than hammering one.
Step two: carrying the answer — sessions
The classic approach is a server session. On successful login you generate a long random identifier, store the real state server-side against that key, and send only the key to the browser in a cookie.
Set-Cookie: sid=8f3a...c1; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600
Every attribute there is load-bearing. HttpOnly puts the cookie out of reach
of JavaScript, so an XSS bug cannot simply read it out. Secure keeps it off
plaintext HTTP. SameSite=Lax stops the browser attaching it to most
cross-site requests, which removes the majority of CSRF surface — though if you
accept state-changing form posts from other origins you still want a CSRF token.
The session identifier is opaque and meaningless on its own, which is the point. It reveals nothing if intercepted in a log, and revoking it is a single delete in whatever store holds the sessions. That store is usually Redis, because session lookup happens on every authenticated request and needs to be a sub-millisecond key read with a TTL attached.
The trade-off is that authentication now depends on shared state. Every service that needs to know who the caller is must reach the session store.
Step three: carrying the answer — tokens
The alternative is to hand the client a signed statement instead of a pointer. A JSON Web Token is three base64url segments — header, claims, signature — joined by dots. Any service holding the verification key can check the signature locally and trust the claims without a network call.
{ "sub": "user_8412", "role": "editor", "iat": 1780000000, "exp": 1780003600 }
That independence is the entire appeal, and it is also the entire problem. A
signed token is valid until it expires, and nothing you do server-side changes
that. Ban a user, and their access token keeps working until exp passes. The
standard mitigation is a short-lived access token — five to fifteen minutes —
paired with a long-lived refresh token that is stored server-side and can be
revoked. Rotate the refresh token on every use and keep the used ones: if an old
one comes back, the token was stolen and replayed, and you should invalidate the
whole family.
Three specific mistakes are worth naming. Do not accept the alg header from
the token itself — pin the expected algorithm, or a forged alg: none sails
straight through. Do not put anything secret in the claims; they are encoded,
not encrypted, and anyone can read them. And do not store tokens in
localStorage if you can avoid it: that is exactly the storage an XSS payload
can read, whereas an HttpOnly cookie is not.
Choosing
For a single application with a normal web front end, sessions are the right default. They are simpler, revocation is instant, and the cookie attributes give you real browser-enforced protection for free. Reach for tokens when you genuinely have several independent services that must verify a caller without a shared session store, or a mobile client where cookie handling is awkward.
The common failure is choosing tokens for a monolith because they sound modern, then reinventing revocation with a server-side denylist — which is a session store with extra steps and a worse security story.
Beyond passwords
Everything above assumes a password exists. Increasingly it should not. Passkeys (WebAuthn) replace the shared secret with a device-held key pair, so there is nothing on your server worth stealing and nothing phishable to type into a fake page. TOTP second factors remain a solid, cheap improvement over a password alone. Both slot into the same architecture: they change how identity is proven, not how the answer is carried afterwards.
Quick answers
- What is the difference between authentication and authorization?
- Authentication establishes who a request belongs to. Authorization decides what that identity is allowed to do. They fail differently: a broken authentication check lets a stranger in, a broken authorization check lets a real user reach someone else's data.
- Where should a session token be stored in a browser?
- In a cookie marked HttpOnly, Secure and SameSite. Anything in localStorage is readable by any script on the page, so a single cross-site scripting bug anywhere — including in a dependency — hands over the credential.
- How should passwords be stored?
- Hashed with a slow, memory-hard algorithm designed for the purpose — Argon2id, scrypt or bcrypt — never a general-purpose hash like SHA-256. The salt is handled by the algorithm; the point is that verification is deliberately expensive.
- Is multi-factor authentication worth adding?
- Yes, and it is the single highest-value addition for most applications, because it defeats credential stuffing and password reuse outright. Prefer TOTP or passkeys over SMS, which is vulnerable to SIM swapping.
References
Related Discoveries
Lumi's weekly note
A short email when we publish something useful. No spam, unsubscribe anytime.