Home · A blog about testing ·

Authentication vs authorization: how QA should test access rights

Many serious security bugs start with a simple confusion: the system knows who you are, but forgets to check whether this object belongs to you.

Access testing model
  1. No token: should be 401
  2. Wrong token: should be 401
  3. Right user, wrong role: 403 or hidden action
  4. Right role, wrong object: 403 or 404

Authentication answers “who are you?”

Authentication is login, password, token, session and expiry. It proves identity, but it does not decide which data the user may read or change.

Basic auth tests include correct credentials, wrong password, email case, missing token, expired token and logout.

Authorization answers “what may you do?”

Authorization checks role and ownership. A user may be logged in and still have no right to open another user order, account, document or customer card.

This is where many API bugs hide because IDs are easy to guess and frontend buttons are not real protection.

The object ownership check

Create or find two users. Open an object as the owner, note its id, then request the same id with the second user token. The expected result is usually 403 or 404.

Why 404 is sometimes better

For private objects, 404 hides whether the object exists. 403 says it exists but you cannot access it. Which one is correct depends on product requirements.

Role checks

Roles should be tested both in UI and API. If an admin-only button is hidden, send the request anyway with a regular user token.

A hidden button is usability. A rejected request is security.

Common access bugs

The most common bugs are missing owner filters, role checks only in frontend, tokens accepted after logout, and endpoints that return different errors for existing and non-existing private objects.

When you report an access bug, include both accounts, both tokens context and the object id. Without that, developers cannot reproduce the permission boundary.

The actor × action × object matrix

A role list is not enough. Build rows such as manager A reads client A, manager A edits client B, a normal user calls an admin endpoint, and a former employee reuses an old token. Every row needs an expected allow, deny or hide decision.

Test more than GET. BOLA may be fixed for reads but remain in PATCH, DELETE, bulk endpoints, exports or nested ids. UUIDs make guessing harder but never replace a server-side permission check.

Minimum access matrix
Actor       Action  Object        Expected\nmanager A   GET     client A      200\nmanager A   GET     client B      403/404 by contract\nmanager A   PATCH   client B      403/404, no data change\nviewer      DELETE  client A      403\nno token    GET     client A      401 + WWW-Authenticate

Session and token lifecycle

Test issue, rotation after login or privilege change, expiry, refresh, logout, revocation, password change and parallel devices. Some architectures allow an old access token until a short expiry; then logout must revoke refresh and the UI must not promise more.

  • The session id changes after login to prevent session fixation.
  • Cookies use Secure, HttpOnly and suitable SameSite; tokens do not leak into URLs or logs.
  • Expired, corrupted and incorrectly signed tokens are rejected.
  • Role changes reach permission checks within the agreed time despite caches.
  • Refresh reuse, logout and password changes follow the documented policy.

BOLA, BFLA and property-level access

BOLA means the user may call the function but the backend does not check the object id. BFLA means the role should not call the function at all, such as a manager calling admin export. Property-level failure exposes or accepts fields the role must not use.

In a CRM, test a foreign client id, an admin-only bulk export and a PATCH containing owner_id or is_vip that the form never shows. Hiding a button fixes none of those server risks.

Worked example: three different access failures in one CRM

One foreign-client check cannot prove the system is safe. Use two managers in one tenant, an admin and a read-only viewer. Create synthetic clients with known owners. Then test three independent boundaries: object, function and field.

Actor × action × object
  1. Define roles and object owners.
  2. Request another user’s object id.
  3. Call an admin-only function directly.
  4. Attempt to write a protected property.

BOLA: allowed function, foreign object

Manager B may call GET /clients/{id} for their own clients. Supply A’s client id with B’s token and expect 403 or 404 per contract, with no A data. Repeat for PATCH and DELETE in the test environment and for nested ids in the body. An unpredictable UUID is no permission check: ids can leak through exports or links.

BFLA: the function itself is forbidden

Bulk export and owner assignment may be admin-only. A hidden button proves nothing; send the request directly with B’s token. Verify both rejection and no side effect: no export file, owner change or queued event. Compare with the admin’s successful call so a broken endpoint is not mistaken for working authorization.

Property level: allowed function, forbidden field

A manager may edit their client’s phone but not `owner_id` or `is_vip`. Add these fields to PATCH in Postman even when the UI omits them. Read the object back as admin to ensure protected fields did not change. A 200 that silently ignores them is acceptable only if specified. Also check whether read responses expose sensitive fields to a viewer.

Include tenants and permission changes

In a multi-tenant CRM, same-role users in different organizations need a separate cross-tenant case. After role revocation or owner reassignment, retry an old token and measure the documented cache delay. Logout behavior, 401 versus 403 and optional 404 masking follow product policy, not tester preference.

Minimal denial matrix
Actor       Request                         Expected\nmanager B   GET /clients/A                  deny; no A data\nmanager B   PATCH /clients/A                deny; no write\nmanager B   POST /admin/clients/export       deny; no file\nmanager A   PATCH /clients/A {is_vip:true}  deny/ignore per contract\nviewer      GET /clients/A                  only safe fields\nno token    GET /clients/A                  401
What counts as evidence?

For each denial keep role, id, method, status, redacted response and proof of no side effect. Report the three failures separately because their causes and fixes differ. Escalate actual access to another person’s data through a private security channel.

Second case: access after owner reassignment

A CRM reassigns a client from manager A to B. Both were signed in already and permissions are cached. Access must move to B within the agreed time. A fresh login is not enough to test this: A’s old token and links may still work after permission revocation.

Define the timing policy

Ask whether revocation is immediate or permits a short replication/cache delay. “Until next login” may be unacceptable for sensitive data, but QA should not invent a TTL. Record the maximum window and source of truth: owner_id, permission service or token claims. Prepare two active sessions and controllable time.

Test rights before and after

Before reassignment, A reads and edits while B is denied; afterward the reverse. Check GET, PATCH, client list, export and linked tasks because endpoints may filter differently. Direct API requests with A’s old token matter more than a hidden card in UI. Denial must not leave a partial write.

Probe the transition race

A’s PATCH concurrent with reassignment needs an ordering rule: it could start before owner_id changes and commit afterward. If the contract lacks a linearization point, raise the question. Retain timestamp, object version and both correlation ids. A different order in one run is not automatically a defect.

Include fields and history

A may lose current-card access but still find data in activity history, search or export. B may get the card but not its linked deal or task because owner_id remains old there. List related resources and expected rights; sometimes the former manager may legitimately retain audit access, sometimes not. Clarify rather than assume.

Measure without leaking data

Use synthetic accounts and clients. Record role, token age, reassignment time, response and absence of write for each step. Never attach real client cards to a public issue. If stale access is confirmed, report exact duration, methods and objects instead of “permission cache is sometimes stale”.

Owner transition matrix
Time                  A GET/PATCH   B GET/PATCH\nbefore reassignment   allow         deny\nafter agreed TTL      deny          allow\nold token A           deny          —\nsearch/export A       no client A   —\nlinked tasks          per ownership policy
What is sufficient proof?

Not merely owner_id changing in storage, but consistent behavior across read and write paths after the agreed time boundary. An old token working too long is a separate security finding with measured duration on test data.

Common access-control questions

Should a foreign private object return 403 or 404?

Either can be valid by contract. 404 hides existence; 403 states refusal. Consistency, no data leakage and no action are the requirements.

Does a UUID prevent BOLA?

No. UUIDs reduce guessing, but ids leak through links, logs, exports and responses. The server must check authorization for every object.

Is checking roles through the UI enough?

No. Hidden controls improve UX, but a request can be sent directly. The server must authorize every method and object.

Access testing is small in steps and large in impact: one changed id can reveal whether the backend really protects user data.

Practise on live services Next write-up

← All write-ups