Sessions and authorization
Authorize resource ownership
Enforce ownership in server-side queries so one authenticated user cannot read or modify another user’s books by changing an ID.
8 minute lesson
Authentication does not make every resource available to every signed-in user. Object identifiers are not authorization tokens.
Query or mutate by both resource ID and trusted owner ID. Return a deliberately chosen not-found or forbidden response without leaking unnecessary existence detail. Apply the rule to reads, writes, exports, and bulk routes.
UPDATE books
SET title = ?
WHERE id = ? AND owner_id = ?
Check the affected-row count. Zero means the book did not exist for this owner. The handler should not run a second unrestricted query merely to produce a more detailed error unless the product truly needs that disclosure.
Reads need the same predicate:
SELECT id, title
FROM books
WHERE id = ? AND owner_id = ?
Filtering a returned object in JavaScript is too late if the query already loaded another user’s private data. Put the boundary as close to the data operation as possible.
Indirect paths are easy to miss. Search, exports, attachments, counts, batch updates, background jobs, and WebSocket subscriptions all need ownership or role checks. A UI that hides another user’s button is not authorization; an attacker can call the route directly.
For administrative access, make the elevated rule explicit and auditable. Do not scatter isAdmin || ownerId === userId throughout handlers. Centralize the policy and test both sides.
Create two users and test every resource route with the other user’s book ID. Include list, export, update, and delete paths, and verify both the response and database state.
Lesson completed