A token answers only the first question

An API can correctly identify the caller and still expose another customer’s data. Changing an order, invoice, or workspace identifier is enough when the handler loads the record but never verifies its relationship to the authenticated subject. This is the familiar shape of broken object-level authorization.

Authentication establishes identity. Authorization decides whether that identity may perform this action on this resource in the current context. The server has to make both decisions independently, no matter which controls the front end hides.

Where the gap appears

The dangerous pattern is deceptively small: middleware validates a JWT, the handler reads an id from the URL, and the database returns a matching row. The missing condition is ownership or tenant scope. The same gap often returns in exports, background jobs, batch endpoints, GraphQL nodes, and file links.

Put tenant or owner constraints into the storage query itself. Avoid loading a broad result and filtering it afterward, where a future return path or side effect can bypass the check.

  • Test read, update, delete, and state transitions separately.
  • Use a neighboring user, another tenant, and a lower-privileged role in negative tests.
  • Apply policy to nested resources and bulk operations, not only top-level routes.
  • Log authorization denials without copying sensitive object data into the log.

Make the safe path the easy path

A useful authorization contract reads like a sentence: subject may perform action on resource when policy holds. Centralize that decision in a policy layer and require every business operation to call it. The policy receives verified identity context, but it still decides object access itself.

Review one resource with a compact matrix of actors and operations. If a new endpoint can be added without an explicit authorization decision, the architecture still depends too heavily on developer memory.

← Back to the archive