“Checking that someone has a ticket, but never checking it's a ticket for this seat.”
Authentication is not authorization
Two different questions hide behind "auth." Authentication asks who are you, proven by a login. Authorization asks what are you allowed to touch, decided per request, per resource. They're independent: being logged in (authenticated) says nothing about whether you may read order #124 (authorization). AI tools nail the first because it's visible and prompted-for. They miss the second because, with one test user who owns everything, it never comes up.
The result is an app where logging in is solid and, once you're in, you can reach everyone's data. The front door has a good lock; none of the interior doors do.
The bug you'll actually have: IDOR
Insecure Direct Object Reference is the unglamorous name for the most likely flaw in your app. The pattern: a resource is addressed by an id, and the server returns it to anyone authenticated, without checking ownership. Your UI only ever links you to your own records, so it looks fine, but the id in the URL or the API call is just a number, and changing it returns someone else's.
you own order 123
GET /api/orders/123 → 200 { your order } ✓ correct
GET /api/orders/124 → 200 { someone else's } ✗ IDOR: no ownership check
^ the only thing you changed was a numberIt's trivial to find and trivial to exploit: increment an id, read a stranger's invoice. And it's everywhere in AI-built apps, because the generated endpoint fetches the record by id and returns it; the "does this user own it" line is the invisible safeguard nobody asked for.
Auth drifts as you prompt
A subtler failure: a guard you added in one prompt disappears in a later, unrelated one. You ask the tool to "add filtering to this endpoint," it rewrites the handler, and the ownership check that was there is silently gone, because the model regenerated the function around the new request and didn't carry the old constraint forward. The endpoint still returns data, the demo still works, and the hole reopened without a trace.
This is why security in AI-built apps can't be a one-time pass. The checks decay every time you regenerate code around them. You re-verify after changes, which is exactly what chapter 6's checklist is for.
The rule: check on the server, every time
Every defence here reduces to one discipline: the server decides, on every request, using values the client can't forge:
- Authenticate every protected request server-side; never infer identity from something the client sent.
- Authorize per resource: does THIS authenticated user own (or have a role granting) THIS specific row? Not just "are they logged in."
- Never trust a client-supplied user_id, role, or is_admin flag; derive identity and permissions from the verified session.
- Default deny: if no rule explicitly grants access, refuse. Prefer returning 404 over 403 so you don't even confirm the resource exists.
This is the same principle as Row-Level Security from chapter 2, applied at the application layer instead of the database. In fact, the two reinforce each other: RLS catches what a missed application check lets through, and vice versa. Defence in depth means you want both.
In one line each
- Authentication (who you are) and authorization (what you may touch) are different; AI builds the first, skips the second.
- IDOR, returning a resource by id without checking ownership, is the most likely serious flaw in an AI-built app.
- Authorization checks drift: regenerating code around a guard silently drops it, so security must be re-verified after changes.
- The rule: the server decides per resource, every request, using a verified identity: default deny, never trust client-sent ids.
Where to go next