Authentication & authorization
Who you are vs what you're allowed to do — the two questions every real backend must answer, and the ownership check AI forgets.
Two questions sit in front of every protected action: who are you? and are you allowed to do this? They’re different jobs — authentication and authorization — and the second one, the ownership check, is the single thing AI-generated handlers forget most often. This chapter is the builder’s model of auth, enough to specify it and catch the holes. (The interview-depth version is 2.3.)
Authentication vs authorization
Authentication answers who are you — the login step that establishes identity. Authorization answers what are you allowed to do — and it runs on every sensitive action afterward. The bouncer checks your ID at the door (authN); the rule about which rooms your ticket opens is authZ. You can absolutely be authenticated and still not authorized: logged in, but not allowed to delete a post that isn’t yours. Conflating the two is how privilege bugs are born.
How login is kept: sessions vs tokens
Both answer “how does the server know it’s still you on the next request.” A session keeps the state server-side and hands the browser an opaque session ID (usually in a cookie); the server looks it up each request — easy to revoke (delete the session), but stateful. A JWT is a signed token that carries the claims (user id, expiry); the server just verifies the signature, no lookup — stateless and scalable, but you can’t easily “log someone out” before it expires. One trap worth knowing: a JWT is signed, not encrypted — its payload is readable by anyone holding it, so never put secrets in it.
The check AI forgets: ownership
Here’s the hole that ships constantly. The AI writes DELETE /api/todos/:id, checks that
someone is logged in, and deletes the todo — without checking that the todo belongs to
this user. Now anyone can delete anyone’s data by guessing an id. The fix is one line of
authorization: confirm todo.userId === currentUser.id before acting, and return 403
if not. This — “scope every action to the logged-in user” — is the most important sentence
you can add to a backend prompt.
app.delete("/api/todos/:id", async (req, res) => {
const user = await requireAuth(req); // authN — 401 if missing
const todo = await db.todo.findUnique({ where: { id: req.params.id } });
if (!todo) return res.status(404).json({ error: "not found" });
if (todo.userId !== user.id) { // authZ — the OWNERSHIP check
return res.status(403).json({ error: "forbidden" });
}
await db.todo.delete({ where: { id: todo.id } });
res.status(204).end();
});Take out the 403 block and the endpoint “works” in every demo — and lets any logged-in
user delete anyone’s todo. That missing block is exactly what to look for in AI output.
| Session | JWT (token) | |
|---|---|---|
| State | server-side (a session store) | none — the token carries it |
| Revoke | easy — delete the session | hard before expiry |
| Scales to many servers | needs a shared store | naturally (just verify the signature) |
| Reach for it | classic web apps | APIs, mobile, multiple services |
01 Learning objectives
0 / 6 done02 Curated reading
03 Knowledge check
- 01easy
Authentication vs authorization:
- 02medium
Which check do AI-generated handlers most often forget?
- 03medium
A JWT's payload is encrypted, so it's safe to store secrets in it.
04 Interview questions
browse all ↗What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.
-
An endpoint lets any logged-in user delete a record by id, including records they don't own. What's the bug, and how do you fix it?
The endpoint authenticates but doesn't authorize — it confirms you're logged in but not that the record is *yours*. That's a broken object-level authorization flaw (an IDOR): change the id in the URL and you act on someone else's data. The fix is an ownership check before the action — load the record, confirm
record.userId === currentUser.id, and return403otherwise (or404to avoid leaking existence). It's OWASP's #1 risk and the single most common thing AI-generated handlers omit.What a strong answer coversAuthenticated ≠ authorized: logged in, but the record isn't theirs.
This is an IDOR / broken object-level authorization (OWASP #1).
Fix: ownership check before acting — 403/404 if not the owner.
Derive the user from the verified session/token, never the request body.
Quick self-checkDELETE /api/posts/:id checks the user is logged in, then deletes the post. What's missing?
-
Wrong — that lets any user delete any post by guessing an id.
-
Correct — authorization on the specific object is the missing, critical check.
-
Wrong — performance isn't the issue; access control is.
-
Wrong — transport security doesn't authorize the action.
Follow-ups they push on- Why is returning 404 instead of 403 sometimes preferable here?
- Why isn't checking ownership in the frontend enough?
Red flag Trusting a `userId` sent in the request body to decide ownership — the client can set it to anything; derive identity server-side.
source: OWASP Top 10 — A01: Broken Access Control ↗