You need a minimum of two ordinary accounts, one account per privilege level, and one unauthenticated session. Two ordinary accounts let one user try to reach the other's objects. Role accounts show whether lower roles can call higher-role functions. Without them, a scan only proves the API works.
Why two is the floor
OWASP's test guidance for broken object level authorization uses two accounts. Account A creates resources it owns, such as a purchase order. Account B then changes the object ID in a request to try to reach account A's objects. A 200 response for someone else's object signals a problem. A 401 or 403 suggests enforcement.
With a single account you can only test your own data, which will always succeed.
The account matrix
| Account | Purpose | What it tests |
|---|---|---|
| User A | Owns seeded objects | Baseline behavior, source of valid IDs |
| User B | Same role as A, owns different objects | Object level access across users |
| Admin or manager | Highest role you use | Which functions exist at the top |
| Read-only or guest role | Lowest authenticated role | Function level access, field exposure |
| Unauthenticated | No token | Endpoints that should require login |
| Second tenant user (if multi-tenant) | Different organization | Tenant isolation |
For function level checks, the WSTG advice is to log in as a lower-privilege user and send requests meant for a higher role, for example deleting another user through an admin endpoint, then vary the HTTP method.
Setting them up properly
- Create accounts through the normal sign-up or admin path, so permissions match real users.
- Give each one seeded objects of every type: orders, files, messages, whatever the API holds.
- Record the IDs in a private file. A scanner that knows A's IDs can try them as B.
- Use short-lived tokens and note how a scanner will refresh them. An expired token mid-scan silently turns every later request into a 401, which looks like good security.
- Store credentials in your secrets manager, never in the scan config committed to a repository.
- Mark all test accounts so they can be found and removed later.
Common mistakes
- Using the admin account for the whole scan. Everything succeeds and nothing is learned.
- Testing the first tenant only.
- Forgetting to confirm the scan is actually logged in. Check the logs or a protected endpoint at the start of every run.
- Sharing test accounts with real staff, so test data mixes with real work.
What the results mean
Compare responses across accounts for the same request. Inconsistent enforcement across endpoints is itself a finding: if one endpoint checks ownership and a sibling does not, someone forgot a check in the second.
Worked example: a clinic booking API with three roles
Imagine an appointment booking API with patients, receptionists and doctors. Patients book and view their own appointments. Receptionists view and move any appointment at their clinic. Doctors view appointments assigned to them and add notes. Admins manage clinics. There are two clinics. Here is the account matrix a tester would build.
| Account | Role | Clinic | Seeded objects |
|---|---|---|---|
| patient_a | Patient | Clinic 1 | Three appointments |
| patient_b | Patient | Clinic 1 | Three different appointments |
| patient_c | Patient | Clinic 2 | Two appointments |
| reception_1 | Receptionist | Clinic 1 | None of its own, sees clinic 1 |
| doctor_1 | Doctor | Clinic 1 | Assigned to some of the appointments |
| admin_1 | Admin | Both | None |
| no login | Anonymous | None | None |
That is seven identities for a small system. Each one answers a specific question.
patient_a and patient_b test object access between peers. Using the OWASP method, patient_a creates appointments and patient_b tries to read, change and cancel them by editing IDs.
patient_c tests the tenant boundary. If patient_c can see Clinic 1 data, isolation is broken in a way that matters more than a peer leak.
reception_1 tests the line between a legitimate wide role and an over-wide one. Can reception_1 see Clinic 2 appointments? Can they read doctor notes, which the role should not see?
doctor_1 tests function level access. Can a doctor cancel an appointment they are not assigned to? Can a doctor call a receptionist-only reschedule route?
admin_1 shows what the top role can do, so the tester knows which functions exist. It is the source of the list of admin routes to try as everyone else.
Anonymous confirms that every route needing login says so.
Running the matrix
The tester picks a set of requests, perhaps twenty, covering read, create, update and delete on each object type. Each request is replayed under each identity, with IDs belonging to a different owner where relevant. That gives about 140 attempts, which a scanner or a short script can run in minutes.
The results go in a grid: requests down the side, identities across the top, status code in each cell. The tester then compares it with the expected grid from the role descriptions. Differences are findings. Cells that match are evidence of working controls, which is useful to keep.
Why the grid beats a single run
A single logged-in scan produces a list of findings with no sense of what was never checked. The grid makes gaps visible: an empty cell is an untested combination. It also becomes the starting point for a regression suite, since each cell is already a test with an expected result.
Keeping the accounts healthy over time
Test accounts decay. Passwords expire, roles change in the product, seeded objects get deleted by earlier runs. Treat the account set like any other test fixture.
- Script the setup. A script that creates accounts, assigns roles and seeds objects means any engineer can rebuild the matrix in minutes.
- Version the expectations. Keep the expected access table in the repository next to the scan config. When a role gains a permission, the table changes in the same pull request.
- Rotate credentials. Test accounts still hold tokens. Give them expiry dates and rotate them like any other secret.
- Watch for drift. If a new role appears in the product and not in your matrix, you are no longer testing everything. Add a check to your release checklist: any new role or tenant type needs a matching test account.
- Clean up. Delete test accounts and their data when a test ends if the environment is shared, so stale privileged accounts do not linger.
The matrix is a small asset, but a valuable one, because it turns a vague wish to test authorization into something you can run on any day.
Try it yourself
Open the demo and look at how the example scan reports findings per endpoint. Imagine each finding repeated for each account in the matrix above.
Click through it at your own pace, or open it full screen.
Quick check
Can one account with two roles replace two accounts?
No. Object level checks need two different owners. One account with two roles still owns all the objects.
Do test accounts need real-looking data?
They need realistic structure, not real people. Use synthetic records.
What if the system has dozens of roles?
Test one account per distinct permission set, not per job title. Group roles that share the same permissions, and put extra effort on roles that cross a boundary such as tenant, clinic or customer.
Sources
- OWASP WSTG: API broken object level authorization
- OWASP WSTG: API broken function level authorization
Last reviewed: 19 October 2026.