API Authorization Testing: Roles, Permissions and 401 vs 403
Authentication proves who you are; authorization decides what you can touch. Broken authorization is the number one API security risk: a delivery partner reading every customer’s address, a viewer deleting records, a user downloading someone else’s invoice just by changing a number in the URL. In this lesson you’ll learn how to design a permission matrix, test roles and ownership, and tell 401 from 403, with live admin and viewer tokens on the practice API.
403 Forbidden (or 404 to hide existence), and unauthenticated calls return 401. Always include object-level checks (BOLA/IDOR): user A must not read or change user B’s resources by changing an id.Three Levels of Authorization
| Level | Question | OWASP API Top 10 | Example bug |
|---|---|---|---|
| Function level | Can this role call this endpoint/method? | API5: Broken Function Level Authorization | Viewer calls DELETE /users/5 and it works |
| Object level | Can this user access this specific record? | API1: Broken Object Level Authorization (BOLA) | GET /orders/1002 shows another customer’s order |
| Property level | Can this user read/write this field? | API3: Broken Object Property Level Authorization | User sends "role":"admin" in a profile update and becomes admin |
401 vs 403 vs 404 in Authorization
| Situation | Correct code |
|---|---|
| No token / invalid token | 401 Unauthorized |
| Valid token, role not allowed (viewer creating data) | 403 Forbidden |
| Valid token, resource belongs to someone else | 403 or 404 (404 hides that it exists) |
| Valid token, allowed, resource missing | 404 Not Found |
Step 1: Build a Permission Matrix
Before testing, write down who may do what. Here is the matrix for the practice API’s token-protected objects (/token/objects):
| Action | No token | viewer | admin |
|---|---|---|---|
GET /token/objects | 401 | 200 | 200 |
GET /token/objects/{id} | 401 | 200 | 200 |
POST /token/objects | 401 | 403 | 201 |
PUT/PATCH /token/objects/{id} | 401 | 403 | 200 |
DELETE /token/objects/{id} | 401 | 403 | 204 |
GET /auth/me | 401 | 200, can_write: false | 200, can_write: true |
In real projects the matrix often has 5 to 10 roles (customer, seller, support agent, finance, admin, super-admin…). It becomes the single source of truth for both developers and testers, and every cell is a test case.
Step 2: Test Function-Level Authorization (Roles)
Log in as both users (practice credentials: admin/admin@123, viewer/viewer@123):
https://sqatools.in/wp-json/sqa-api/v1/auth/logincurl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"username":"viewer","password":"viewer@123"}' \
"https://sqatools.in/wp-json/sqa-api/v1/auth/login"BASE=https://sqatools.in/wp-json/sqa-api/v1
# Viewer can read
curl -i "$BASE/token/objects" -H "Authorization: Bearer $VIEWER" # 200
# Viewer cannot create
curl -i -X POST "$BASE/token/objects" -H "Authorization: Bearer $VIEWER" \
-H "Content-Type: application/json" -d '{"name":"x"}'
# 403 {"error":{"code":"forbidden","message":"The viewer role can only read objects. Log in as admin to change them."}}
# Admin can create
curl -i -X POST "$BASE/token/objects" -H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" -d '{"name":"admin object","ttl":120}' # 201
Function-level test ideas
- Every write method (POST, PUT, PATCH, DELETE) with every low-privilege role.
- Admin-only endpoints (
/admin/*,/reports,/users/export) with a normal user token. Look for them in Swagger docs, the web app’s JavaScript and mobile app traffic. - “Hidden” HTTP methods: the UI only uses GET, but does
DELETEwork? - Old API versions (
/v1/) that may lack the checks added in/v2/.
Step 3: Test Object-Level Authorization (BOLA / IDOR)
BOLA (also known as IDOR, Insecure Direct Object Reference) is the most common serious API bug. The API checks that you are logged in but not whether this object is yours.
/api/invoices/50231, changes it to 50230 and downloads another customer’s invoice with name, address and phone. Security researchers report this pattern constantly in bug bounty programmes because sequential numeric ids make it trivial to scan thousands of records.How to test BOLA
- Create two users of the same role: User A and User B.
- As B, create a resource (order, address, document). Note its id.
- As A, try to
GET,PUT,PATCHandDELETEB’s resource. - Expected: 403 or 404 for every call, and B’s data unchanged (verify with B’s token).
- Repeat for nested routes (
/users/{B}/orders), list endpoints with filters (/orders?user_id=B), and ids inside the body ({"order_id": B's}).
Also test predictable ids: are they sequential (1001, 1002…) or random UUIDs? UUIDs don’t fix BOLA, but sequential ids make it much easier to exploit.
Step 4: Test Property-Level Authorization (Mass Assignment)
Send fields you shouldn’t control and check the server ignores or rejects them:
PATCH /users/me
{
"name": "Asha",
"role": "admin", // privilege escalation attempt
"wallet_balance": 99999, // money!
"is_verified": true
}
Expected: role, wallet_balance and is_verified are rejected (422) or silently ignored; a follow-up GET proves nothing changed. On the read side, check responses don’t include fields a role shouldn’t see (salary, internal notes, other users’ emails).
The practice API rejects unknown fields: POST /validate/user with an extra field returns 422, and POST /objects with a client-chosen id returns 422.
Automating a Permission Matrix with Playwright
Turn the matrix into data, and Playwright generates one test per cell:
import { test, expect, APIRequestContext } from '@playwright/test';
const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
const USERS = { admin: 'admin@123', viewer: 'viewer@123' } as const;
type Role = keyof typeof USERS | 'anonymous';
async function tokenFor(request: APIRequestContext, role: Role) {
if (role === 'anonymous') return {};
const res = await request.post(`${BASE}/auth/login`, { data: { username: role, password: USERS[role] } });
return { Authorization: `Bearer ${(await res.json()).access_token}` };
}
const matrix: { action: string; method: 'get' | 'post'; path: string; data?: object; expect: Record<Role, number> }[] = [
{ action: 'list objects', method: 'get', path: '/token/objects',
expect: { anonymous: 401, viewer: 200, admin: 200 } },
{ action: 'create object', method: 'post', path: '/token/objects', data: { name: 'rbac test', ttl: 60 },
expect: { anonymous: 401, viewer: 403, admin: 201 } },
];
for (const row of matrix) {
for (const role of Object.keys(row.expect) as Role[]) {
test(`${role} -> ${row.action} = ${row.expect[role]}`, async ({ request }) => {
const headers = await tokenFor(request, role);
const res = await request[row.method](`${BASE}${row.path}`, { headers, data: row.data });
expect(res.status()).toBe(row.expect[role]);
});
}
}
test('viewer cannot delete an admin object, admin can', async ({ request }) => {
const admin = await tokenFor(request, 'admin');
const viewer = await tokenFor(request, 'viewer');
const created = await (await request.post(`${BASE}/token/objects`, { headers: admin, data: { name: 'owned', ttl: 60 } })).json();
expect((await request.delete(`${BASE}/token/objects/${created.id}`, { headers: viewer })).status()).toBe(403);
expect((await request.get(`${BASE}/token/objects/${created.id}`, { headers: viewer })).status()).toBe(200); // still there
expect((await request.delete(`${BASE}/token/objects/${created.id}`, { headers: admin })).status()).toBe(204);
});
Adding a new role or endpoint now means adding one line of data, not writing new tests.
Where Authorization Bugs Hide
- New endpoints added in a hurry, especially exports, bulk operations and reports.
- Mobile-only APIs nobody tests through the web UI.
- GraphQL fields and nested resolvers (each field needs its own check).
- Background jobs and webhooks that act “as the system”.
- Role changes: a user demoted from admin still has an old token with
role: admin. - Multi-tenant SaaS: company A’s admin seeing company B’s data (tenant isolation).
- File downloads: signed URLs that never expire or are guessable.
Authorization Testing Checklist
- Permission matrix documented and agreed.
- Every endpoint and method tested with no token (401) and each role.
- Two users of the same role: no cross-access (BOLA) for read, update and delete.
- Ids in path, query, body and headers all checked.
- Privileged fields can’t be set by the client (mass assignment).
- Responses don’t leak fields beyond the role’s permissions.
- Role change and account deactivation take effect immediately or at next token refresh.
- Tenant isolation for multi-company systems.
Interview Questions: Authorization Testing
What is the difference between 401 and 403 in authorization testing?
401 means the request isn’t authenticated (no, invalid or expired credentials). 403 means the user is authenticated but not allowed to perform that action or access that resource.
What is BOLA or IDOR?
Broken Object Level Authorization (Insecure Direct Object Reference) is when an API returns or modifies a resource based on an id without checking that the logged-in user owns or may access it, for example changing /orders/1001 to /orders/1002 shows another customer’s order.
How do you test role-based access control in APIs?
Create a permission matrix of roles, endpoints and methods, obtain a token for each role, call every endpoint and method with each token and with no token, and assert the expected status for each cell. Automate it as data-driven tests.
How do you test BOLA?
Use two users with the same role. Create a resource as user B, then try to read, update and delete it as user A through every route that accepts its id (path, query, body). Expect 403 or 404 and verify B’s data is unchanged.
What is mass assignment?
When an API binds all client-sent fields to an internal object, letting a user set fields they shouldn’t, such as role, is_admin or balance. Test by sending such fields in create and update requests and verifying they’re rejected or ignored.
Should an API return 403 or 404 for another user’s resource?
Both are acceptable. 404 is often preferred because it doesn’t reveal that the resource exists. What matters is that no data is returned and nothing is changed.
What is a permission matrix?
A table listing each role against each endpoint and action with the expected outcome. It’s the specification for authorization and the basis for systematic test cases.
What is tenant isolation testing?
In multi-tenant SaaS systems, verifying that users of one organisation can’t access another organisation’s data, even with an admin role in their own tenant.
What happens if a user’s role changes while their token is still valid?
If the role is read only from the token, the old privileges remain until it expires. Good systems re-check roles on the server or keep tokens short and refresh them. Testers should verify how quickly role changes take effect.
Why is authorization hard to test only through the UI?
The UI hides buttons for actions a user isn’t allowed to do, but the API may still accept the request. Authorization must be tested directly at the API level with each role’s token.