~/learn / authorization

Understanding Authorization

The difference between proving who you are and deciding what you may do, the confused-deputy problem at the heart of it, the models systems use to decide, and why authorization breaks far more often than authentication.

beginnerupdated

Two questions sit behind every request a system receives:

  1. Who is this? That is authentication. Passwords, passkeys, certificates, and tokens all answer it.
  2. Are they allowed to do this? That is authorization. It is asked again on every request, for every object, and it is where most serious application bugs live.

A system can authenticate perfectly and still be wide open. If you log in as yourself and then read someone else’s invoice by changing a number in the URL, authentication worked flawlessly. Authorization failed completely. Keeping these two ideas distinct is the single most useful habit in application security.

Why authorization breaks so often

Authentication is centralized. It usually lives in one login endpoint, one identity provider, one piece of middleware, and once it works it tends to keep working. Authorization is the opposite: it is scattered across every endpoint, every query, every button, every background job. Each one must independently ask the right question about the right object, and the rules often depend on data that only exists at request time. Miss a single check among hundreds and you have a vulnerability that no amount of login hardening will catch.

That structural asymmetry is why broken access control sits at number one in the OWASP Top 10, and why the first item in the OWASP API Security Top 10 is Broken Object Level Authorization. It is not that developers are careless. It is that authorization is a check you have to remember to write correctly in a thousand places, and “deny by default” is rarely the default.

The confused deputy

The deepest way to understand authorization failures is the confused deputy problem. A deputy is a program that holds more authority than the user calling it (your server can read any row in the database; the compiler can write to any file). The bug is tricking the deputy into misusing its authority on the attacker’s behalf. SSRF is a confused deputy (the server reaches internal hosts for you). CSRF is a confused deputy (the browser attaches your cookies for the attacker’s request). An IDOR is a confused deputy (the server fetches any record by id and forgets to ask whose it is). Once you see authorization bugs as “a trusted component used its power on behalf of someone who should not have had it,” the whole class coheres.

Three questions, three bug classes

Question the server must ask When it does not Example
Can this user access this specific object? Object-level failure (IDOR, BOLA) GET /api/orders/1002 returns another customer’s order
Can this user perform this function at all? Function-level failure (BFLA) A normal user calls the admin-only DELETE /api/users/7 and it works
Can this user change this property? Property-level failure (mass assignment) Sending "role": "admin" in a profile update is accepted

Test every endpoint against all three. Most real engagements find at least one of the three on an application that “passed” authentication review.

How systems decide

Role-based access control (RBAC). Users get roles, roles get permissions. Simple and common. It breaks down when permission depends on the data, for example “managers can see their own team’s reviews,” because a role alone cannot express “their own.”

Attribute-based access control (ABAC). Decisions use attributes of the user, the resource, and the context: department, owner, clearance, time of day, network. Flexible, and harder to reason about or audit because the policy space is large.

Relationship-based access control (ReBAC). Decisions follow relationships in a graph: you may edit this document because you own the folder that contains it, and someone you shared it with may comment. Google’s Zanzibar paper popularized this at scale; OpenFGA and SpiceDB implement it. A ReBAC check answers “is there a path from this user to this object with this permission,” which naturally expresses the ownership rules RBAC struggles with.

Most real applications mix all three, usually with no single place where the rules are written down. That scattering is exactly what a tester exploits, because an inconsistency between two code paths is a bug.

Where the decision should happen

  • On the server, every time. Hiding a button in the UI is not access control. The request can be sent directly, and it will be.
  • As close to the data as possible. WHERE id = ? AND owner_id = ? is far safer than fetching by id and checking ownership in application code afterward, because the first form makes forgetting the check impossible: a row the user does not own simply is not returned.
  • Deny by default. A new endpoint should be inaccessible until someone explicitly grants access, so the failure mode of forgetting is “locked out” rather than “wide open.”

Tokens carry identity, not permission

Modern systems pass identity around as tokens: session cookies, OAuth access tokens, JWTs. A token says who you are, and sometimes which scopes you hold. It does not decide whether this user may touch this object. A valid token is the beginning of the authorization question, never the answer. A system that stops at “the token is valid” has authenticated without authorizing, which is the number one bug all over again.

The pages in this guide go deeper on each piece: how sessions work, how OAuth delegates access, how single sign-on spans applications, what a JWT really is, and how to test access control methodically.

Go deeper