Backend authentication, from sessions to OAuth
4 September 2026 · 5 min read · #backend #auth #security
written from this video
Authentication answers "who are you". Authorization answers "what can you do". Almost every messy auth system I've read is one that confused the two, or picked a mechanism whose failure mode it never thought about.
There are four mechanisms worth knowing as a backend engineer, and each one has a specific thing it is bad at.
Stateful: sessions in a store
HTTP is stateless by design. Every request arrives with no memory of the last one, which was fine when the web was documents and became a problem the moment anyone wanted a shopping cart.
Sessions are the fix. On successful login the server generates a session ID, writes the user's data next to it in Redis or Postgres, and hands the ID back in a cookie:
Set-Cookie: sid=9f2c...; HttpOnly; Secure; SameSite=Lax
HttpOnly matters — it means page JavaScript cannot read the value, so an XSS bug can't just exfiltrate the session. The browser attaches the cookie to every subsequent request to that origin automatically, and the server does one lookup to find out who's calling.
What you get: real-time control. You can list every active session, log a user out from another device, and revoke access the instant support gets a call. What it costs: a store lookup on every request, and pain when your servers are spread across regions and the session store isn't.
Stateless: JWTs
A JWT moves the state into the token. Three base64 segments — header (signing algorithm), payload (the claims), signature:
{ "sub": "u_8812", "role": "editor", "iat": 1757116800, "exp": 1757120400 }
The server signs it with a secret, the client sends it back in Authorization: Bearer ..., and the server verifies the signature. No lookup. Any instance in any region can verify the token as long as it holds the key, which is exactly why JWTs took over distributed and mobile backends.
The cost is the one everybody quotes and then forgets to design around: you cannot revoke a JWT. A leaked token is valid until exp. Your only global lever is rotating the signing secret, which logs out every user on the platform to deal with one compromised account.
The usual workaround is a blacklist — verify the signature, then check the token's ID against a revocation list in Redis. Which is a lookup on every request. Which is the thing you left sessions to avoid.
If your "stateless" auth ends with a Redis call on every request, you built a session with extra steps. That's fine — just know that's what you built.
The honest version of the hybrid is scoping it by client: sessions for the browser app where you control the cookie, JWTs for mobile clients and service-to-service calls where cookies aren't a thing.
API keys: machines, not people
Sessions and JWTs both assume a human at a login form. API keys exist for the case where there isn't one.
Your server calls someone else's server. There's no browser, no redirect, nowhere to type a password. So the platform generates a random string tied to an identity, a permission set, and a quota, and you put it in an env var and send it as a header. That's the whole model. The reason OpenAI ships API keys alongside a chat UI is that the two access patterns are genuinely different: one has a human in it, one doesn't.
Treat a key as a password that never rotates on its own. Scope it, expire it, and make revocation a single row update.
OAuth 2.0 and OIDC: delegation
The other three all assume the credential belongs to the party using it. OAuth exists for when it doesn't.
The problem it solves is delegation: a travel app wants to read flight confirmations out of your Gmail. The pre-2007 answer was that you gave the travel app your Google password — total access, no scoping, and revoking it meant changing that password everywhere you'd reused it.
OAuth replaces the shared password with a scoped token. Four parties:
- Resource owner — you
- Client — the app asking for access
- Resource server — where the data lives
- Authorization server — the thing that issues tokens after you consent
The client redirects you to the authorization server, you approve a specific scope, the client gets a token that does only that, and you can revoke it without touching anything else.
OAuth 2.0 dropped 1.0's per-request signatures for bearer tokens — simpler to implement, more dangerous to leak — and split the flow by client type: authorization code for server-side apps, client credentials for machine-to-machine, device code for TVs and anything without a keyboard. Implicit flow still shows up in old tutorials; it's discouraged, don't use it.
One thing OAuth explicitly does not do is tell you who the user is. It's an authorization protocol. OpenID Connect layers identity on top by adding an ID token — a JWT with sub, iss, iat, and profile claims. Every "Sign in with Google" button on the internet is OIDC.
Authorization is a separate system
Once you know who's calling, you still have to decide what they may do. The bad version of this is a magic admin string checked in a handler — one leak and it's game over, and the moment a second person needs the same access you're maintaining a list of magic strings.
RBAC is the boring answer. Roles carry permissions, users carry roles, and the check happens early in the request:
role deduced at the edge -> attached to request context -> handlers check it
403 Forbidden if it fails
403, not 401. 401 means we don't know who you are; 403 means we know and the answer is no.
Two details that leak information
Error messages. user not found tells an attacker to move on to the next email. incorrect password tells them they found a real account and only the password is left. Both narrow the search. Every failure in the auth path returns the same string — authentication failed — regardless of which step failed. Be helpful everywhere else.
Timing. Even with identical messages, the response times differ. A missing user fails at the lookup and returns fast. A valid user with a wrong password goes on to hash the candidate and compare, and hashing is deliberately slow — that's a measurable gap, tens to hundreds of milliseconds, and it's enough to enumerate accounts. Use constant-time comparison for the hash check, and hash a dummy password even when the user doesn't exist so both paths do the same work.
For anything past a learning project, use an auth provider. Build one once to understand the trade-offs, then hand the algorithm choices, the salt rounds, and the breach response to someone whose job that is.