API Authorization Testing
API authorization testing checks that an API lets each authenticated caller do only what the access policy allows: the right functions, on the right objects, with the right fields. Authentication answers who sent the request; authorization answers whether that caller may do this. Customer B is properly signed in, so GET /v1/orders/9021 with B's token passes authentication; if order 9021 belongs to customer A, the API must still refuse it.
The tester's main instrument is a test matrix: users down one side, requests across the top, and the policy's expected answer in each cell.
Why it matters
Authorization defects expose data and actions to people who already hold a valid account. The OWASP API Security Top 10 (2023) ranks Broken Object Level Authorization first (API1:2023): one missing ownership check lets any customer read other customers' addresses by changing a number in the URL.
Normal use never shows these defects: the app displays only the user's own data and hides admin buttons. Only a test that deliberately calls the API as the wrong user finds them.
Roles, ownership and the access policy
An authorization decision combines who the caller is, what they want to do and which object it touches. Most products combine role-based access control (RBAC), where a role such as customer, support agent or admin grants functions, with ownership rules: customers act only on their own objects. Multi-tenant products add one more: users never see another company's data.
Written down, these rules are the access policy, your test basis. The OWASP Authorization Cheat Sheet recommends denying by default, granting least privilege and validating permissions on every request. For testing this means:
- The API is the boundary, not the screen: hiding the "Refund" button protects nothing if
POST /v1/orders/{id}/refundaccepts a customer's token. - The role comes from the server's own records or a verified token claim, never from client input such as an
X-Role: adminheader or"isAdmin": truein a body. - Scopes, from the OAuth lesson, limit what the client application may do: a token without the required scope gets 403 with
insufficient_scope. They add to the role and ownership checks and never replace them: a token withorders.writestill may not change another customer's order. - As in the API authentication lesson, 401 means no valid credential and 403 means a known caller is refused. RFC 9110 also allows 404 to hide that a resource exists; the contract says which one applies.
Three levels: function, object and property
Authorization breaks at three levels, each a risk in the OWASP API Security Top 10. Gaining a higher role's functions is vertical privilege escalation; reaching another user's data in the same role is horizontal.
- Function level (API5:2023, Broken Function Level Authorization): may this role call this operation at all? A customer calls
GET /v1/admin/users, or switchesGETtoDELETEon a path they may only read. - Object level (API1:2023, BOLA, also known by the older name IDOR, insecure direct object reference): may this user act on this object? Customer B sends the id of A's order. Ids hide in paths, query strings, bodies, headers and nested routes such as
/orders/9021/invoice. - Property level (API3:2023, Broken Object Property Level Authorization): may this user see or set this field? A response returns fields the caller must not see, or the API accepts a field the caller must not set, often called mass assignment.
# Function level: customer A's token on an admin function
GET /v1/admin/users
Authorization: Bearer <token of customer A>
-> expected 403
# Object level: customer B's token on customer A's order
GET /v1/orders/9021
Authorization: Bearer <token of customer B>
-> expected 404 (the contract hides other customers' orders)
# Property level: a customer sends a field only admins may set
PATCH /v1/users/me
Authorization: Bearer <token of customer A>
Content-Type: application/json
{"displayName": "Sara A.", "role": "admin"}
-> expected 400, or the role field ignored; A stays a customerBuild the test matrix: users × requests
The matrix turns the policy into test cases you can count. Build it before sending anything:
- Rows: an anonymous caller, every role, and two users of the same role (A and B), because horizontal tests need objects that are not yours; in multi-tenant products, a user of another tenant too.
- Columns: every endpoint and method in the OpenAPI document, including admin, search and export endpoints and older versions still deployed.
- Cells: the expected result from the policy, on test objects with a known owner, written before execution. Most cells are refusals, and defects hide there.
- Execution: change only the token, then check the status, the body (no other user's data, no forbidden fields) and the state: a 403 that still saved the change is a defect.
- Lists:
GET /v1/ordersfor B must not include A's orders, even with?customerId=added.
Try it: OpenAPI → Test Cases Try it: API Response Comparator
Worked example: an orders API
A shop's orders API: customers read and change only their own orders; support agents read any order and issue refunds but change none; admins may do everything and alone manage users. Another customer's order returns 404. Order 9021 belongs to customer A. Of the matrix's 20 cells, only 8 allow the request.
| Actor | R1 · read the orderGET /v1/orders/9021 | R2 · change the addressPATCH /v1/orders/9021 | R3 · refund the orderPOST /v1/orders/9021/refund | R4 · list all usersGET /v1/admin/users |
|---|---|---|---|---|
| Anonymousno token | 401authentication | 401authentication | 401authentication | 401authentication |
| Customer Aowner of order 9021 | 200allowed | 200allowed | 403function level | 403function level |
| Customer Banother customer | 404object level | 404object level | 404object level | 403function level |
| Support agent | 200allowed | 403function level | 201allowed | 403function level |
| Admin | 200allowed | 200allowed | 201allowed | 200allowed |
- Allowed by the policy, with the expected success code
- Refused by authorization:
403for a function the role may not use,404for another customer’s order - Object-level (BOLA) cells: the same role, someone else’s order
- Refused by authentication: no valid credential,
401
Expected results written from the policy before execution. Customers read and change only their own orders; support agents read any order and issue refunds but change none; admins may do everything and alone manage users.
Running it in the test environment, one actor's token at a time, found three defects:
- Customer B, R1:
200with A's address instead of 404. Object level, critical: any customer can read any order. - Customer A, R3:
201, a refund for A's own order. Function level, critical: owning an object does not grant every function on it, and here money leaves the shop. - Support agent, R2:
403as expected, but a freshGETshowed the new address. Function level, major: the status said refused, but the change was saved. Only the state check revealed it.
Text version
Orders API, test environment. Order 9021 belongs to customer A.
R1 GET /v1/orders/9021 read the order
R2 PATCH /v1/orders/9021 change the delivery address
R3 POST /v1/orders/9021/refund refund the order
R4 GET /v1/admin/users list all user accounts
Actor R1 R2 R3 R4
anonymous (no token) 401 401 401 401
customer A (owner) 200 200 403 403
customer B 404 404 404 403
support agent 200 403 201 403
admin 200 200 201 200Common mistakes
- Testing with one account per role, which makes object-level tests impossible.
- Assuming that a hidden button means the endpoint behind it refuses the wrong role.
- Checking only the status code, so a refusal that still changed data, or a list with other users' records, passes.
- Swapping ids only in the URL path, not in query parameters, bodies, nested routes or list and export endpoints.
- Skipping object-level tests because the ids are random UUIDs.
- Testing production data, real customer accounts or any system without written permission.
The QA perspective
Ask for the access policy during refinement, as a role × operation table, before the endpoints are built. Written as a matrix, it exposes gaps such as "may a support agent see payment details?" while they are cheap to answer; the product owner decides, and you turn the answers into test cases.
Agree which test accounts QA owns in each environment: at least two per role, and two tenants where relevant. Record the actor, its role, the object's owner, the request, the expected and actual status and the state check, with tokens masked. A BOLA or function-level defect that exposes personal or financial data is a security defect, usually critical. Automate the matrix as data-driven API tests in the pipeline, and test only systems you own or have written permission to test.
Practice exercise
Review an authorization test log for a clinic API
A clinic API's policy: patients read and cancel only their own appointments and may change only reason; doctors read the appointments assigned to them and add clinical notes; receptionists read and reschedule all appointments but never see clinical notes; only admins create staff accounts. Appointments the caller may not see return 404.
Accounts: patients P1 and P2, doctor D1, receptionist RC1. Appointment 5531 belongs to P1 and is assigned to D1. Review this log from the test environment.
Clinic API, test environment. Appointment 5531: patient P1, doctor D1.
# Actor Request Status Observed
1 P1 GET /v1/appointments/5531 200 P1's appointment
2 P2 GET /v1/appointments/5531 200 P1's appointment
3 P2 POST /v1/appointments/5531/cancel 404 appointment unchanged
4 RC1 GET /v1/appointments/5531 200 body has "clinicalNotes"
5 P1 POST /v1/appointments/5531/notes 403 no note added
6 D1 POST /v1/staff {"role": "doctor"} 201 staff account created
7 P1 PATCH /v1/appointments/5531 200 doctor changed to D7
{"reason": "follow-up", "doctorId": "D7"}- Write each row's expected result from the policy before reading the observed one.
- Mark each row correct or defect, and name each defect's level: function, object or property.
- Rank the defects by severity and write a bug title for the worst.
- Name two actor and request combinations the log misses.
Show the model answer
Expected: 1 → 200; 2 → 404; 3 → 404, unchanged; 4 → 200 without clinicalNotes; 5 → 403, not 404, because P1 may see the appointment; 6 → 403, no account; 7 → as the contract decides, either 400 with nothing changed or 200 with only reason changed and doctorId ignored; in both cases the doctor stays D1. Rows 1, 3 and 5 are correct.
Critical: row 2, object level (BOLA, API1:2023), a patient reads another patient's appointment; row 6, function level (API5:2023), a doctor creates staff accounts.
Major: row 7, property level (mass assignment, API3:2023), a patient changes the doctor; row 4, property level (excessive data exposure, API3:2023), a receptionist sees clinical notes, or critical where those notes are the most sensitive data.
Bug title for row 2: "GET /v1/appointments/{id} returns another patient's appointment with 200 instead of 404 (P2 reads P1's 5531)".
Not covered, for example: an anonymous call (401); GET /v1/appointments as P2, which must not list 5531.
Frequently asked questions
Should a refused request return 403 or 404?
Both can be correct. RFC 9110 defines 403 for a request the server understood but refuses, and allows 404 when the server does not want to reveal that the resource exists. Many APIs return 404 for other users' objects so ids cannot be probed. What matters is that the contract names one answer and the API is consistent: 403 for existing ids and 404 for missing ones tells an attacker which ids exist.
Our ids are random UUIDs. Do we still need object-level tests?
Yes. Unpredictable ids are harder to guess, but ids leak through shared links, logs, screenshots and other API responses. OWASP recommends unpredictable ids in addition to, not instead of, an authorization check: for every id that comes from the client, the server must check that the caller may access that object.
The matrix grows fast. Do I have to test every cell?
Prioritise by risk: start with every write operation, every admin function, every endpoint that returns personal or financial data, and list, search and export endpoints. Then automate the matrix as data-driven tests, one data row per actor and request, so a new endpoint or role only adds rows and the whole matrix runs on every build.
Is authorization testing security testing or functional testing?
Both. The access policy is a functional requirement, so checking it belongs in everyday API testing. Specialist penetration testers go further, but they should not be the first to find out that customer B can read customer A's order.
References
- OWASP API Security Top 10 (2023)The ten most common API weaknesses: broken object-level authorization, broken authentication, unrestricted resource consumption and more.
- OWASP Cheat Sheet Series: AuthorizationAccess control done right: least privilege, deny by default, check permissions on every request, prefer attribute- or relationship-based models, and test the authorization logic.
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- OWASP Web Security Testing Guide (WSTG)The reference methodology for testing web applications and APIs for security weaknesses, test by test.
Practise with Mutqan tools
Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.
Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.
Compare two API responses (e.g. staging vs production) and list every structural and value difference.
Generate a structured test-case matrix (positive, negative, auth, boundary) from an OpenAPI / Swagger specification.
Decode JWT header and payload, inspect claims and check expiration — entirely in your browser.
Test Bearer, Basic, API-key and custom-header authentication against an endpoint and compare authorised vs unauthorised responses.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.