API Authorization Testing

API authorization testing, proven across every role

Authorization flaws, not injection, are what break modern APIs. This page is the methodology hub: what an authorization matrix is, how to build and test it across every role, tenant, and property with real accounts rather than a single logged-in session, and how Operator turns each violation of that matrix into a reproducible proof rather than a flagged alert someone has to chase down.

Written by Berk Dusunur, Founder & CEO, Planck Proof · Updated September 2026

Definition

What is API authorization testing?

API authorization testing is the practice of proving, with real accounts, that an API enforces access control correctly: that each authenticated caller can reach only the objects, functions, and properties their role is entitled to, and nothing that belongs to another user, role, or tenant.

It is distinct from authentication testing, which asks whether a caller is who they claim to be, and from input testing, which probes what a payload can corrupt. Authorization asks a comparative question instead: given two valid, authenticated identities, can one reach something that should only ever answer to the other? That question cannot be answered from a single session, which is why authorization testing always requires more than one credential and a systematic replay across every operation in the API, not a sample of it.

The stakes are why this is the discipline to get right first. Broken object level authorization has topped the OWASP API Security Top 10 since the list existed, and broken function level authorization sits in the top five, ahead of most injection categories. Both fail silently: the API returns a normal-looking 200, the client renders it, and nothing in a request log distinguishes a legitimate fetch from an attacker walking sequential ticket or account identifiers.

BOLA vs BFLA

Two flaws, tested across every role

Authorization testing covers two failure modes that break independently. BOLA, broken object level authorization, is when a caller reaches another account's records by tampering with an identifier. BFLA, broken function level authorization, is when a caller invokes an operation reserved for a higher privilege level. An API can bind objects to owners correctly yet still expose an admin-only function, or lock down functions yet leak objects across accounts, so a complete test exercises both dimensions for every role rather than assuming one implies the other. A team that only tests BOLA on its own top ten endpoints, for instance, still ships an unguarded admin function that BFLA testing would have caught in the same pass.

The Matrix

The authorization matrix: roles x operations x objects

An authorization matrix is the model that makes authorization testing systematic instead of spot-checked. Roles run down one axis, every operation in the OpenAPI spec runs across the other, and each cell holds the outcome that role should get when it calls that operation against an object it does not own: a 403 if the role is not entitled, a 200 scoped to the caller's own data if it is. Built from the spec rather than guessed at, the matrix turns "did we forget a check" into a fixed set of cells to fill in and verify.

The example below models a support ticketing API with four roles. For the list and create rows, the cell shows whether the role may call the operation at all. For the other four rows, it shows the expected result when the caller is not the owner of the ticket, which is the case a fixed-script scanner can never construct because it holds only one identity.

OperationGuestUserSupportAdmin
GET /tickets (list own tickets)403200200200
POST /tickets (create a ticket)403200200200
GET /tickets/{id} (view another account's ticket)403403200200
PATCH /tickets/{id}/assign (assign another account's ticket)403403200200
POST /tickets/{id}/notes (note on another account's ticket)403403200200
DELETE /tickets/{id} (delete another account's ticket)403403403200

A real matrix runs to every operation in the spec, not six, and every tenant boundary the system has, not just role. It also runs deeper than the table above shows: object identifiers turn up in path segments, request bodies, query strings, and nested references inside a response, so each operation is really several cells once every place an identifier can be swapped is counted. The value holds at any size: a single deviation from an expected cell is a finding, not a judgment call, and the matrix is the artifact that makes that deviation visible instead of buried in a log.

Method

Multi-user authorization testing, step by step

Filling in the matrix and finding where it breaks takes seven steps, whether a person runs them by hand across a weekend or an agent runs them at the scale of the full spec in one pass. Each step builds on the one before it, so skipping role or object seeding leaves later steps with nothing real to replay against.

Step 1

Role seeding

Provision one bearer token for each role and tenant the system defines, from unauthenticated guest through admin. One credential per identity is enough; no crawling or UI walkthrough is required to obtain it.

Step 2

Object seeding

Create or select at least two objects per object-owning role, ideally two peer accounts in the same role rather than only one per level, so cross-account tests have a real foreign record to target, not only a privilege boundary to bump against.

Step 3

Cross-role replay per method

For every operation in the spec, replay the identical request under each role's token, on every HTTP method the operation supports, and compare the actual status and body to the matrix cell it should produce.

Step 4

Tenant boundary

Repeat the replay across tenant boundaries, not only roles. Two admins in different tenants must not reach each other's objects, which a role-only test never checks because it never leaves one tenant.

Step 5

Function-level checks

Call operations a role's interface never renders a button for, directly by method and path, including internal and management routes. A hidden control in the UI is not a server-side authorization check.

Step 6

Property-level checks

On objects a role may legitimately read or write, check individual fields for ones it should not: an internal flag, a price, a status field, or another user's role field accepted through mass assignment.

Step 7

Evidence capture

Every cell that deviates from the matrix is captured with the full request and response for both identities, the baseline that shows the intended behavior, and a replayable reproduction an engineer can run without re-deriving the steps.

Proof

What a proven authorization finding looks like

Illustrative example, not a specific customer result. A User-role token calls an operation the matrix reserves for Support and Admin against a ticket it does not own.

PATCH https://api.example.com/v1/tickets/4821/assign HTTP/1.1
Host: api.example.com
Authorization: Bearer <user-role-token>
Content-Type: application/json

{ "assignee_id": 118 }

HTTP/1.1 200 OK
Content-Type: application/json

{ "ticket_id": 4821, "assignee_id": 118, "status": "assigned" }

Per the matrix, a User token should receive a 403 on this row; it received a 200 and reassigned a ticket it does not own. Nothing about the response looks abnormal in isolation, which is exactly why this class of flaw survives a manual click-through and a single-session scan: only the comparison against the matrix cell reveals it. The finding ships with this request and response, the Guest-token baseline that confirms the endpoint is meant to be gated, reproduction steps, a CVSS v3.1 vector, and a curl command so an engineer reproduces it in one step without re-deriving the request:

curl -X PATCH https://api.example.com/v1/tickets/4821/assign \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"assignee_id":118}'

The blind spot

Where scanners stop and Operator continues

A scanner runs as a single authenticated session and treats a 200 response as healthy, so it cannot tell that the object it fetched belongs to another account or that the function it called should have been forbidden. Filling in even a small matrix needs at least two identities and a comparison of what each may reach, a step fixed scripts are not built to take, however many payload variants they throw at a single parameter. A DAST crawler adds page discovery to that same single-session model, which finds more URLs but still no second identity to compare against.

Operator holds one token per role instead of one session, builds the matrix from the OpenAPI spec rather than a crawl, and replays every operation across every identity to find where the matrix is violated. That gap, reasoning about who is calling versus what got called, is also what separates agentic testing from DAST more broadly; see agentic pentesting versus DAST for the full comparison.

Multi-Tenant

Tenant isolation and BOPLA

Two variants of the matrix deserve their own attention because teams tend to test roles and stop there. Tenant isolation is the multi-tenant case of BOLA: two customers on shared infrastructure, tested the same way as two roles, by replaying one tenant's requests under another tenant's token, at every operation the schema exposes, not only the ones a UI happens to render for both. It is covered in depth on tenant isolation testing. BOPLA, broken object property level authorization, is the row of the matrix that operates below the object: a caller allowed to read or write a record at all, but able to see or set an individual field it should not, such as a role, balance, or price attribute accepted through mass assignment on an otherwise correctly-scoped update. Both are cells in the same matrix, not separate problems, which is why the method above tests them at steps 4 and 6 rather than as an afterthought bolted on at the end.

Remediation

How to fix and prevent authorization flaws

Enforce authorization on the server at the point of access, for every object, function, and property, and never trust the client to hide what it should not reach. Every operation should answer two questions before it runs: is this caller authenticated, and is this specific object, action, or field theirs to touch. The matrix above is a useful acceptance test for that rule: run it again after a fix ships, and the cell that used to return 200 should return 403.

  • Check ownership server-side on every object read, write, and delete, not only on the ones the UI links to.
  • Gate functions by role at the endpoint itself, not only by hiding the button in the UI.
  • Allowlist writable properties so mass assignment cannot set a field, such as role or balance, a caller's role should not touch.
  • Scope queries to the caller and tenant at the data layer so a query cannot return an object it should never have fetched.
  • Centralize the checks in shared middleware or a policy layer so every new endpoint inherits them by default instead of by memory.
FAQ

Common questions about API authorization testing

What is API authorization testing?

API authorization testing checks whether an API enforces access control correctly, that an authenticated caller can reach only the objects and invoke only the functions they are permitted to. It covers broken object level authorization (BOLA), broken function level authorization (BFLA), and broken authentication. It requires multiple identities and a comparison of what each is allowed to do, because a single session cannot reveal that it reached data belonging to someone else.

What is an authorization matrix in API testing?

An authorization matrix lists every role or tenant down one axis and every API operation across the other, with each cell holding the access the caller should get: a 403 if the role should be forbidden, a 200 scoped to the caller's own objects if it should be allowed. Building the matrix from the OpenAPI spec, rather than guessing, is what makes authorization testing systematic instead of spot-checked, and it doubles as the acceptance test a fix is checked against on retest.

How do you test multi-user authorization in an API?

Provision one token per role or tenant, then replay each operation under every identity against objects it does not own, comparing the actual response to the matrix. A full pass also checks tenant boundaries, calls functions a role's interface never exposes, and inspects individual request and response properties for fields a lower role should not read or write, capturing full request and response evidence for anything that deviates so an engineer can confirm the flaw without re-deriving the request from scratch.

Is API authorization testing the same as BOLA testing?

No. BOLA testing is one part of API authorization testing, the part that checks whether a caller can reach another account's objects. Authorization testing is the broader discipline: it also covers BFLA, function-level access, broken authentication, tenant isolation, and property-level exposure such as BOPLA. A BOLA test alone will not catch a privilege escalation to an admin-only function, so treating the two names as interchangeable leaves a whole category of risk unscoped.

Why do vulnerability scanners miss authorization flaws?

A scanner runs as a single authenticated session and treats a 200 response as healthy, so it has no way to know the object it reached belongs to another account or that the function it called should have been forbidden. Detecting an authorization flaw needs at least two identities and a systematic comparison of what each may reach, which fixed scripts do not perform no matter how many payloads they send through that single session.

Get Started

Prove whether your API enforces authorization

Give us one token per role and Operator will build the matrix, replay every operation across every identity for BOLA and BFLA, and hand you reproducible evidence for every cell that fails. See also BOLA testing and BFLA testing.