An access control lab works best with two user accounts, an API that returns objects by id, and one missing check. Learners prove that user A can read user B's data, add the check, then add a deny-by-default rule so that the next endpoint is safe even if someone forgets.
Lab setup
Provide an orders API with two users and a handful of records each. Learners log in as user A and request an order that belongs to user B by changing the id. When it works, they have found the flaw, and the room usually goes quiet for a moment.
Give them the code for one endpoint and ask them to fix it before moving on.
What the guidance says
The OWASP Authorization Cheat Sheet lists the principles worth teaching:
- Deny by default. The application should make an explicit decision to permit or deny, and prefer explicit configuration over framework defaults.
- Check on every request. Include AJAX and server-side calls, and ideally apply checks globally instead of method by method. The page notes that an attacker only needs to find one way in.
- Least privilege. Grant the minimum access a role needs, and review permissions periodically for creep.
- Object-level checks. Hiding or randomizing identifiers is not sufficient. Verify access to the specific object on every request, since access to one object of a type does not imply access to others.
- Server-side enforcement. Client-side checks may help the experience but should never decide access.
Exercise steps
- Exploit. Read another user's order by changing an id.
- Try the lazy fix. Switch ids to random strings. Let learners see that it only slows the attacker if the id ever leaks. This demonstrates the point about hidden identifiers.
- Fix properly. Add an ownership check in the data access path, so that a query for an order always includes the caller's identity.
- Add a global rule. Introduce middleware that denies any route without an explicit policy. Add a new endpoint with no policy and show that it returns forbidden.
- Test. Write a test that logs in as user A and asserts that user B's order returns forbidden, for every endpoint that takes an id.
Extending it
Add a role check for an admin-only report, and ask learners to find the client-side button hiding that is the only protection. They then move the check to the server.
For API specifics, see broken object level authorization explained.
Debrief questions
- Which of our endpoints take an id, and which have a test for another user's id?
- Do we have a default deny, or do new routes start open?
- Who reviews permission changes?
Quick check
Are random identifiers an access control?
No. They make guessing harder but do not replace a server-side check of the caller's rights on each object.
Sources
Last reviewed: 19 October 2026.