Production Backend API Capstone
Roles Alone Cannot Protect a Resource
One-line summary
Authentication verifies who is making the request, and authorization decides whether that principal may perform this action on this resource in this organization. If you check only the writer role, users with the same role can read and modify each other's orders, which is horizontal privilege escalation.
Why a role check is insufficient
Role-based access control narrows the scope of functionality, but it cannot express relationships to resources. Even if Alice and Bob are both writers, Bob has no reason to cancel Alice's order. An organization admin should also hold admin rights only within their own organization. So compare the order's owner_id and org_id together with the identity's sub, org_id, role, and the requested action. If the organization differs, reject first even for an admin; if it is the same organization, apply the owner or permitted-admin policy.
A missing authentication is 401; an authenticated identity whose relationship does not match is 403. Whether to make the response for a nonexistent resource the same as for an unauthorized one, to reduce enumeration attacks, is also a policy decision. In this assignment, the read boundary handles unauthorized IDs consistently with 403. The tests must include three cases: another owner in the same organization, an admin from another organization, and the legitimate owner.
Leaks that are easy to miss in practice
If you print the whole auth header to the log or include the raw text in a token parsing error, the observability system becomes a secret store. A debug print that runs when a module is imported can also leak sensitive environment values into the grader or worker logs. Importing a library must be silent, and error responses should give only a category such as unauthorized or forbidden. In audit logs, record a non-sensitive principal ID and the policy result instead of the token.
Where to put the authorization decision
Even the same rule creates different leak points depending on which layer of code it lives in. There are three places, and each blocks something different.
Middleware before the route. This is the right place for things that do not need to look at the resource, such as authentication and role checks. It can be applied to every route at once, so there is no place to forget it. However, it cannot decide resource relationships here, because you have to read an order to know its owner.
Inside the handler. This is where you read the resource and then compare the owner and organization. It is the most expressive, but a person has to write it on every route, so it is easy to miss. If someone adding a new route forgets that line, only that route is open. So when you use this approach, you also need a mechanism, such as a test, that confirms there is no route that skips the authorization check.
Inside the storage query. This puts the owner and organization into the lookup condition. Its biggest advantage is that the application check and the storage scope cannot drift apart, and even if you miss the first two places, other people's data does not come out. In exchange, you must accept that "not found" and "not authorized" become the same; that helps reduce enumeration attacks, but it becomes harder to explain to users why something did not work.
In practice you layer all three. Use middleware to guarantee authentication, the storage query to cut the scope, and the handler to decide the per-action policy. With only one layer, any route that skips that layer is a hole, and with layers, the others catch it even if you forget one.
Also count what you deny. A small number of requests are always rejected in normal operation, but if the number suddenly jumps, either someone is sweeping through other people's data or our policy has just been changed incorrectly. Both are things you need to know, and if you do not count, you know neither.
Practical judgment criteria
An authorization test must not end at "the writer succeeds." Pin the most dangerous neighbor relationships and tenant boundaries as counterexamples. Include resource ownership in the database lookup condition as well, so the application check and the storage scope do not drift apart. The next module covers how to observe these denials and successes in the same trace context without leaving secrets behind.