ByeBuy.ai
BUILD YOUR ESCAPE ROUTE · ✦ CURSOR · HOST IT · ◫ SUPABASE · CONNECT IT · ↯ RELAY · BUILD YOUR ESCAPE ROUTE · ✦ CURSOR · HOST IT · ◫ SUPABASE · CONNECT IT · ↯ RELAY ·
CURRICULUM
← BYEBUY NOTES

September 12, 2026

IDENTITY IS NOT PERMISSION

Identity Is Not Permission

Class 46 gave you an architecture you can explain on one page: browser, application, database, providers, auth. Now we put the first real lock on the door. Every app that remembers people has to answer two different questions, and mixing them up causes some of the most expensive bugs in software.

Two questions, not one

Authentication (authN) asks: *who is this?* The person proves identity — a password, a magic link, a passkey, "Continue with Google" — and the system concludes "this is Maya, user usr_41."

Authorization (authZ) asks: *what may they do?* Given that identity, plus the thing they want and the context around it, is this action allowed?

Memorize this sentence, because your AI will not volunteer it:

A signed-in user is simply a *known* user. Whether they may read, edit, delete, or trigger something is a separate decision, made fresh on every request. This distinction sits at the heart of OWASP Authentication Failures, which documents what happens when applications confuse a successful login with blanket permission.

Maya and the event she does not own

Use the simpler running example for this class: Neighborhood Events. Anyone can browse public events. Signed-in organizers can add listings and edit their own.

Maya signs in successfully. Authentication worked. She opens her event, /events/evt_101, and clicks Edit. Allowed — she owns it.

Then Maya (or her curious browser, or a script) requests /events/evt_207/edit — an event owned by another organizer, Daniel. She is still signed in. Authentication still passes. The only correct answer is deny, because the authorization check fails:

event.owner_id === "usr_41"?  // Maya's id
evt_207.owner_id === "usr_88"  // Daniel's id → deny

Nothing about Maya's login changed. What changed was the *resource* and her *relationship* to it. An AI that was told only "add login" will frequently get this wrong: it checks if (user) instead of if (userMayEditThisRecord). Your job is to never leave that second check unspecified. This is the Part X contract habit applied to people: the task card for any auth work must name the actor, the resource, the allowed action, and the enforcement point — the same precision you used for interface contracts — or the AI will invent its own.

Six words you must use precisely

When you direct AI on auth work, loose vocabulary produces loose enforcement. Use these terms:

  • ID: the stable identifier for a person, e.g. usr_41. Names change; emails change; the ID is what ownership points at.
  • Account: the identity record itself — sign-in methods, recovery contact, status (active, suspended), and links to roles or teams.
  • Role: a coarse grouping like admin, organizer, member, or reader. Roles answer "what kind of actor is this, broadly?"
  • Ownership: a record-level fact, usually an owner_id column. It answers "whose thing is this?"
  • Permission: a single allowed action, e.g. events:update-own.
  • Policy: the rule that combines identity, resource, and context into a decision: "an organizer may update an event where event.owner_id === user.id."

Roles are a blunt tool. Ownership and context usually decide the real action. Maya's role (organizer) lets her create and edit *in general*; ownership decides *which* listings. An admin role might let someone moderate reports of abuse — it should not automatically mean "can read everyone's private watchlist in the browser," a mistake we will dissect in Lesson 47.3.

The safe decision path

Every protected request — page load, API route, background action — should follow the same order:

request → establish identity → validate requested action
  → load target record → check policy / ownership
  → allow or deny → log relevant outcome

Walk it with Maya's forbidden edit:

1. Request: POST /api/events/evt_207 with new title text. 2. Establish identity: verify the session. Who is calling — Maya (usr_41), anonymous, or an expired session? If identity cannot be established, stop here with "unauthenticated." 3. Validate requested action: is the input well-formed and is this a real action? (Validation gets its full treatment in Class 48; here, just note it happens *before* the policy check so malformed input never reaches the data layer.) 4. Load target record: fetch evt_207 from the database, including its owner_id. 5. Check policy / ownership: does usr_41 satisfy the update policy for this record? No — owner_id is usr_88, and Maya is not an admin moderator for this action. 6. Allow or deny: deny with a 403 Forbidden, not a 404 trick, not an empty success. The customer sees "You cannot edit this event." Nothing about Daniel's data leaks in the error. 7. Log relevant outcome: record time, route, actor reference (usr_41), resource reference (evt_207), decision (deny: not-owner), without logging passwords, tokens, or private content.

Notice what the path never does: it never trusts a userId or isOwner: true sent by the browser. The browser's claims are *inputs*; the server and data layer are the *authority*. Identity comes from the verified session; ownership comes from the stored record.

The hidden-button failure

Here is the classic AI-generated vulnerability, and you should ask for it by name during review:

The app hides the Edit button when Maya views Daniel's event. In the builder's own browser, everything looks correct — no button, no problem. But the API route POST /api/events/:id checks only if (session) and updates whatever ID it receives.

Anyone who opens browser developer tools, replays the request with evt_207, or writes a five-line script bypasses the "protection" entirely. Hiding UI is a courtesy, not a control.

The fix is structural: the backend route — and ideally the database policy behind it — must deny unauthorized calls even when the request is perfectly forged. Frontend visibility and backend enforcement must agree, but only the backend decides.

When you brief an AI, say it explicitly: "Hiding the button is not the fix. Add the server-side ownership check and prove a forged request is denied." Lesson 47.3 shows the four tests that prove it.

Exercise: write AUTHORIZATION-MATRIX.md

Do not describe permissions in prose and hope the AI infers them. Write a table with five columns: actor, resource, allowed action, enforcement point, test.

For Neighborhood Events, plus one Research Desk row to carry forward:

| Actor | Resource | Allowed action | Enforcement point | Test |

|---|---|---|---|---|

| Anonymous visitor | Public event listing | Read | Public route, no session required | Anonymous GET returns 200 |

| Signed-in organizer (owner) | Own event (owner_id = self) | Create, update, delete own | API route checks session + owner_id; DB policy matches | Maya updates evt_101 → 200 |

| Signed-in organizer (non-owner) | Another's event | Read only; update/delete denied | Same route denies; DB policy denies | Maya updates evt_207 → 403 |

| Anonymous / signed-out | Any event mutation | Denied | Route requires session first | Signed-out POST → 401 |

| Admin moderator | Flagged event | Hide/unhide via ops tooling, audited | Separate admin route + audit log | Admin action writes audit entry; non-admin → 403 |

| Research Desk reader | Public demo brief | Read | Public route | Anonymous read → 200 |

| Research Desk owner | Own watchlist item | Create, read, delete own | Route + owner_id policy | Owner delete → 200; other user → 403 |

Your finish line for this lesson: an AUTHORIZATION-MATRIX.md with those five columns filled for every actor and resource in your slice. Each row must name *where* enforcement lives (route? database policy? both?) and *which test* proves it. A row without an enforcement point is a wish. A row without a test is a rumor.

Quick verification: pick any row, forge the request as the wrong actor (different user ID, or no session), and confirm the backend denies it. If only the button disappeared, you are not done.

Check your understanding

1. Maya is signed in and requests another organizer's event. Which step fails — authentication or authorization — and why? 2. Why must the policy check load the stored record instead of trusting an owner_id sent in the request body? 3. Your AI says "I hid the admin panel from non-admins, so it's secure." What do you ask it to do next?

Next, Lesson 47.2 turns the first half of the path — proving identity — into a concrete sign-in and session design you can specify without building cryptography yourself.

ARTICLE DISCUSSION

JOIN THE
CONVERSATION.

0 COMMENTS

BYEBUY ACCOUNT ACCESS

Sign in

Use your account to save routes and make the catalogue yours.

Enter your email and we’ll send a secure sign-in link and code.

NEW ROUTES ADDED WEEKLY · 9,235 CATALOGUE ENTRIES · BUILD · DEPLOY · QUERY · STACK · SAY BYE TO BUY · NEW ROUTES ADDED WEEKLY · 9,235 CATALOGUE ENTRIES · BUILD · DEPLOY · QUERY · STACK · SAY BYE TO BUY ·