How to Test Multi-User Access Control in Your App
Learn the two-browser test procedure for catching multi-user access control bugs before production. Includes manual scenarios, automation tools, and production verification.
- Multi-user access control bugs happen when one user can access another user's data or trigger actions they should not be allowed to perform, and they almost never show up in solo testing.
- Test by opening your app in two separate browser windows with two different user accounts logged in, then try to access resources across user boundaries to verify the app blocks unauthorized access.
- Common mistakes include hiding features in the UI but not enforcing permissions on the backend, testing only the success case, and ignoring API endpoints not exposed in the UI.
- Automate testing with Cypress or Playwright for UI testing, or Burp Suite for endpoint-level access control checks; add Keploy for automated test generation from live traffic.
- Verify access control in production by auditing logs for unauthorized access attempts, setting alerts for anomalies, and running spot checks on random resources weekly.
Testing multi-user access control means verifying that your app enforces permissions correctly when different users try to access the same resource. Most developers test this poorly: they log in, click around, and assume it works. But access control failures are among the most exploitable bugs in production, and they slip through because the test procedure is not obvious. This guide walks you through the exact steps to catch these bugs before your app goes live.
What is multi-user access control and why is it so hard to test?
Multi-user access control is the set of rules that decide which users can do what in your app. User A should see only their own data. Admin B should see all data but not modify user configs. Guest C should see nothing. Simple in theory. Hard in practice.
The problem is that access control bugs have two failure modes. Horizontal privilege escalation happens when User A tricks your app into showing them User B's data (same role, unauthorized resource access). Vertical privilege escalation happens when a guest somehow triggers an admin endpoint. Neither shows up in your local testing because you are always the same logged-in user. You click a button, the app responds, you move on.
In production, User A and User B log in at the same time. One of them tries to access the other's resource. Your API either blocks it (correct) or leaks it (catastrophic). This is why access control testing requires a fundamentally different workflow than normal testing. You cannot test it alone. You need to simulate at least two users hitting the app simultaneously.
According to security researchers at NHIMG, this is how security teams discover authorization bugs: capture HTTP traffic from two different users, swap their session IDs or tenant identifiers, and see if the server blocks the unauthorized request. If it does not, you have a breach. The issue is that most developer testing skips this step entirely. You test the happy path. You do not test the unhappy path where one user tries to access another user's data.
How do you set up a two-account test environment?
You need two browser instances with two different user accounts logged in at the same time. This is simpler than it sounds.
Step 1: Create two test users with different roles. If your app has an admin panel, create them there. If not, sign them up manually. Use distinct email addresses (test-user-1@example.com and test-user-2@example.com) and give them different roles (one basic user, one admin, or one guest and one editor--whatever roles your app supports). Do not use the same user twice. That defeats the point.
Step 2: Open two separate browser windows or use browser containers. Option A: Open your app in a regular browser window and log in as User 1. Open the same app in an incognito window and log in as User 2. Option B: Use browser plugins like Firefox Multi-Account Containers or Chrome profiles to separate sessions. Either way, you now have two isolated browser contexts.
Step 3: Position the windows side by side. Arrange them so you can see both at the same time. One on the left, one on the right. This is where you will spend the next hour clicking things.
What are the step-by-step test scenarios?
Now you test. But what do you test? Run through these scenarios:
Scenario 1: Horizontal escalation (same role, wrong resource). Log in as User 1. Navigate to a resource that belongs to User 1 only (a profile, a document, a project, anything with an owner field). Write down the URL or resource ID. Copy it. Switch to User 2's browser. Paste the URL or modify the ID in the address bar to point to User 1's resource. Hit Enter. What happens? If User 2 sees User 1's data, you have a bug. If the app shows an error or redirects to a 404 or access denied page, you are good.
Scenario 2: Vertical escalation (different role, privileged action). Log in as a guest or basic user in User 1's browser. Try to access an admin endpoint directly. If your app has an /admin page, type it into the address bar. If you see the admin panel, that is a bug. If the app blocks you or redirects to login, you pass. Do the same for any privileged action: changing user permissions, viewing billing, exporting all data, deleting accounts. Anything that only admins should do.
Scenario 3: API endpoint bypass. Open your browser's developer tools (F12). Go to the Network tab. In User 1's browser, perform an action (upload a file, post a comment, update a profile). Watch the network tab and identify the API request. Note the URL and the request body. Switch to User 2's browser. Open the developer console and make the same API request, but change the resource ID to belong to User 1. If the API returns success, you have a bug. If it returns 403 Forbidden or 401 Unauthorized, you pass.
Scenario 4: Concurrent access and session isolation. Have both users perform the same action at the same time (upload a file, post a comment). Do they interfere? Does one user's action affect the other's data? This catches bugs where shared state or global variables leak between sessions.
How do you test the same workflow with two users simultaneously?
The scenarios above are manual. They work, but they are slow and error-prone. For faster feedback, you can automate this using the same testing frameworks you already use.
If you use Cypress for UI testing, you can open two Cypress browser instances and run test steps in parallel. Create one test that logs in as User 1, performs an action, and checks the response. Create another that logs in as User 2 and tries to access User 1's resource. Run both at the same time and compare results.
If you test via API (using Postman, Jest, pytest, or a custom script), generate session tokens or auth headers for both users. Make a request to a protected endpoint as User 1, capture the response. Make the same request as User 2, verify it fails. This is faster than UI testing because you skip the browser rendering overhead.
PortSwigger's Burp Suite, a popular penetration testing tool, automates this by capturing traffic from one user, swapping the session cookie with another user's cookie, and replaying the request. If the response changes, you have an authorization issue. Burp also offers session handling rules that can swap cookies automatically across multiple requests, which speeds up testing of multi-endpoint workflows.
Keploy, another testing framework, generates test cases from live traffic: it watches real users interact with your app, captures their requests and responses, and produces test scenarios automatically. You can then modify these tests to inject multi-user access control checks (swap user IDs, swap session tokens) and run them in your CI/CD pipeline.
What common mistakes trip up developers?
These are the bugs that almost always ship to production.
Mistake 1: Hiding the button but not checking permissions on the backend. Your UI does not show an "Edit" button to guest users. So you assume guests cannot edit. Wrong. A guest can still open the browser console, find the edit endpoint in the Network tab, and make a direct API request. Your app should reject the request on the server side, not rely on the UI to hide it.
Mistake 2: Testing only the happy path. You test that an admin can do admin things. You do not test that a regular user cannot. Half the scenarios slip through because you never try the "deny" case.
Mistake 3: Assuming login equals full access. Being logged in does not mean you can access everything. Permissions are separate from authentication. A user can be authenticated (logged in) but unauthorized (not allowed to access a resource). Most developers test one, not both.
Mistake 4: Ignoring API endpoints not exposed in the UI. Your UI exposes ten endpoints. But your app has thirty. The twenty hidden ones might not check permissions because no user ever triggers them via the UI. A determined attacker will find them and test them anyway.
Mistake 5: Forgetting about concurrent access. You test User A and User B sequentially: A logs in, does something, logs out. B logs in, does something. In reality, A and B log in at the same time. Their sessions run in parallel. Shared-state bugs only show up under concurrent load.
Mistake 6: Copying the access control logic into multiple places. Your permission check lives in three controllers and two middleware. A bug in one place ships to production because the others diverged slightly. Centralize your permission logic into a single function or service that all endpoints call. This is also what Cerbos, an authorization policy engine, does: it consolidates all permission decisions in one place, so you audit once instead of thirty times.
What tools and frameworks help automate access control testing?
Manual testing catches most bugs, but automation catches the rest and prevents regressions.
Cypress and Playwright are browser automation frameworks. Both let you write tests that log in as different users and verify access control. Cypress is easier to learn; Playwright is faster for large test suites. Both integrate into your CI/CD pipeline so you run these tests on every deployment.
Burp Suite (commercial tool, free Community Edition available) is the industry standard for security testing. It intercepts HTTP traffic, lets you swap session cookies between users, and replays requests to check if access control holds. This is overkill for a small app but essential for complex ones with many endpoints. PortSwigger's documentation walks through the exact steps for testing horizontal access controls using Burp.
OWASP ZAP is a free, open-source alternative to Burp. It has similar session handling and replay features. If budget is tight, start with ZAP.
Keploy watches your live traffic and generates test cases automatically. You can configure it to inject access control checks (swap user IDs) into those tests. This is useful if you have dozens of endpoints and cannot manually write tests for all of them.
Jest, pytest, or any API testing framework can handle access control checks if you write them yourself. Create a test that logs in as User 1, hits an endpoint that returns their data. Create another test that logs in as User 2 and tries the same endpoint with User 1's resource ID. Assert that User 2 gets a 403 error. This is lower-level than Cypress or Burp but gives you total control.
Where to start: If you are testing a web app with a UI, use Cypress or Playwright. If you are testing an API, use Jest or pytest. If you are serious about security, add Burp or ZAP later. If you have a high-volume app, automate test generation with Keploy.
How do you verify access control works in production?
You tested locally. You tested in staging. Now your app is live. Access control still matters. Production verification is different because you have real users and real data. You cannot test by hand.
First, audit your logs. Every successful and failed access attempt should be logged with the user ID, the resource ID, the action (read, write, delete), and the result (granted, denied). Search your logs for repeated 403 errors from the same user. Are they trying the same resource over and over? Is this expected (they are testing) or a sign of a breach? Review denied requests at least weekly.
Second, implement role-based audit trails. When a user with elevated privileges accesses sensitive data, log it. Not because they are malicious, but because you need evidence if something goes wrong. This is especially important if your app handles GDPR-regulated data. Opsily's guide to GDPR-compliant app hosting covers this in detail: audit trails are a compliance requirement, not optional.
Third, set alerts for anomalies. If a user who normally has read-only access suddenly triggers a delete endpoint, that is worth investigating. If a user accesses data from a different country than usual, log it. These patterns catch compromised accounts.
Fourth, run spot checks. Pick a random resource from production data. Log in as a user who should not have access to it. Try to read it, edit it, delete it. If any of these succeed, you have a production bug that requires immediate fixing.
Once your access control is verified locally and working in production, the next step is ensuring your app is hosted on a platform that respects those permissions. A self-hosted PaaS like Ship provides production infrastructure with built-in authentication layers and audit logging, so you do not have to re-build these safeguards. Before shipping to any hosting platform, Opsily's checklist for the first paying customer covers all the pre-production steps, including access control validation.
Frequently Asked Questions
How do I check all app permissions? List every user role in your app (admin, editor, viewer, guest). List every action they can take (read, write, delete, export). Create a matrix: rows are roles, columns are actions. For each cell, run the test scenario above (try the action as that role, verify it succeeds or fails as expected). This is your permission audit.
What are some good test scenarios for a login page? Login testing is different from access control testing. For login, test: correct password (should succeed), wrong password (should fail), missing email (should fail), account lock after N failed attempts, password reset flow, session expiration, concurrent logins from different devices. Access control testing assumes login works and tests what happens after.
What is the difference between authentication and authorization? Authentication is proving who you are (login). Authorization is proving what you are allowed to do (permissions). You can be authenticated but unauthorized. A guest can log in (authenticated) but not access the admin panel (unauthorized). Test both.
Should I test access control in every framework or just once? Just once in your permission service. If you duplicate permission checks across multiple controllers or endpoints, you introduce inconsistency. Centralize the logic. Call it from everywhere. This is also why policy engines like Cerbos exist: they enforce permissions in a single place, reducing bugs and simplifying audits.
Can concurrent users cause access control bugs? Yes. If your app uses shared state (global variables, class-level fields) or relies on session affinity (assuming the same user always hits the same server), concurrent users expose bugs. Test this by logging in as multiple users and performing conflicting actions at the same time (both trying to edit the same resource). If the app crashes, deadlocks, or returns inconsistent data, you have a concurrency bug.
What does it mean if my tests pass locally but fail in production? Production has real data, real concurrency, and real networks. Local testing is single-user, single-process, on localhost. Differences: (1) your staging environment does not mirror production data (use real data for access control testing), (2) your local testing skips concurrency (test multiple users simultaneously), (3) your local API is not under load (production is). Reproduce the production scenario as closely as possible in staging before shipping.
How often should I re-test access control? Every time you add a new user role, every time you add a new endpoint, every time you change the permission logic. Ideally, make access control testing part of your standard QA process: test it before every release. Automate it in your CI/CD pipeline so you catch regressions on day one.
The Bottom Line
Multi-user access control testing is not optional: it is the difference between a secure app and a data leak. Most developers test it poorly because the procedure is not obvious. The key is setting up two browser windows with two different users, then systematically checking if each user can access resources they should not be able to reach.
Start with the manual two-browser test scenario outlined above. Build a matrix of roles and actions. Try them. Once you are confident, automate with Cypress, Playwright, or Burp Suite. Log everything in production and spot-check permissions weekly.
Once your access control is rock solid locally, deploy to a platform like Ship that provides built-in authentication and audit logging, so your permissions stay protected in production. Check out Opsily's checklist for going live with your first paying customer to make sure you have not missed anything.