API Authorization Testing: Roles, Permissions and 401 vs 403

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.

Quick answerAPI authorization testing verifies that each authenticated user can perform only the actions and access only the data their role and ownership allow. Build a permission matrix (roles x endpoints x methods), then test every cell: allowed actions return 2xx, forbidden ones return 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.
Advertisement

Three Levels of Authorization

Real-life example: a hospital system A receptionist can book appointments but can’t read diagnoses (function level). A doctor can read medical records, but only of their own patients (object level). A nurse can see a patient’s medication list but not the billing field on the same record (property level). Each level needs its own tests.
LevelQuestionOWASP API Top 10Example bug
Function levelCan this role call this endpoint/method?API5: Broken Function Level AuthorizationViewer calls DELETE /users/5 and it works
Object levelCan this user access this specific record?API1: Broken Object Level Authorization (BOLA)GET /orders/1002 shows another customer’s order
Property levelCan this user read/write this field?API3: Broken Object Property Level AuthorizationUser sends "role":"admin" in a profile update and becomes admin

401 vs 403 vs 404 in Authorization

SituationCorrect code
No token / invalid token401 Unauthorized
Valid token, role not allowed (viewer creating data)403 Forbidden
Valid token, resource belongs to someone else403 or 404 (404 hides that it exists)
Valid token, allowed, resource missing404 Not Found
Remember it this way 401 = “I don’t know who you are.” 403 = “I know who you are, and the answer is no.”
Advertisement

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):

ActionNo tokenvieweradmin
GET /token/objects401200200
GET /token/objects/{id}401200200
POST /token/objects401403201
PUT/PATCH /token/objects/{id}401403200
DELETE /token/objects/{id}401403204
GET /auth/me401200, can_write: false200, 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.

Real-life example: food delivery roles Customer: own orders only. Restaurant: orders for its own outlet, can change status to “preparing”. Delivery partner: assigned orders only, sees address only after pickup. Support agent: any order, but can’t see full payment details. Admin: everything. Each line is a set of authorization tests.

Step 2: Test Function-Level Authorization (Roles)

Log in as both users (practice credentials: admin/admin@123, viewer/viewer@123):

POSThttps://sqatools.in/wp-json/sqa-api/v1/auth/login
curl -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 DELETE work?
  • Old API versions (/v1/) that may lack the checks added in /v2/.
Advertisement

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.

Real-world pattern A customer opens /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

  1. Create two users of the same role: User A and User B.
  2. As B, create a resource (order, address, document). Note its id.
  3. As A, try to GET, PUT, PATCH and DELETE B’s resource.
  4. Expected: 403 or 404 for every call, and B’s data unchanged (verify with B’s token).
  5. 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.

Advertisement

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.

Advertisement

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.

Advertisement

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

  1. Permission matrix documented and agreed.
  2. Every endpoint and method tested with no token (401) and each role.
  3. Two users of the same role: no cross-access (BOLA) for read, update and delete.
  4. Ids in path, query, body and headers all checked.
  5. Privileged fields can’t be set by the client (mass assignment).
  6. Responses don’t leak fields beyond the role’s permissions.
  7. Role change and account deactivation take effect immediately or at next token refresh.
  8. Tenant isolation for multi-company systems.
Advertisement

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.

Advertisement
View the full API testing syllabus