SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Authorization / 8 MIN READ

Authorization testing strategies: verify decisions and enforcement

Build an authorization test suite that covers permission decisions, API enforcement, tenant isolation, inherited access, revocation, and dependency failures.

Test two separate properties: whether the permission model returns the expected decision, and whether the application enforces that decision before disclosing data or performing protected work. Build the suite around an explicit access matrix, pair allowed requests with denied requests, and exercise permission changes and dependency failures.

Authentication is not a substitute for these tests. As the OWASP Authorization Cheat Sheet explains, establishing a caller’s identity does not authorize that caller to access every resource or perform every action.

Define an access matrix before writing tests

Describe each expected decision as a combination of subject, action, resource, and context. Derive expectations from application requirements, not from the authorization implementation under test. The implementation should not also be the authority that determines whether its own output is correct.

OWASP recommends enumerating user types, resources, and operations during design, then creating tests that verify those permissions are enforced.

For an illustrative private-document application, suppose the requirements say:

  • Owners may read and edit their documents.
  • Readers may read but not edit.
  • Reader access may be direct or inherited through a group.
  • Every permitted operation requires current membership in the document’s tenant.
  • No other access is permitted.

That produces a starting matrix like this:

Subject Action Resource and context Expected result
Owner Edit Own document, current tenant member Allow
Direct reader Read Shared document, current tenant member Allow
Direct reader Edit Shared document, current tenant member Deny
Group-derived reader Read Group-shared document, current tenant member Allow
Tenant member without a document grant Read Same-tenant document Deny
User belonging only to another tenant Read Foreign-tenant document with a direct reader grant Deny
Anonymous caller Read Private document Deny
Former tenant member Read Old document grant still present Deny

The cross-tenant case deliberately includes a document grant: test the tenant requirement independently of whether a grant exists.

These are example requirements, not universal rules. Add explicit cases for public resources, service identities, support access, and tenant administrators if your application has them. Do not assume that a role named admin has authority across every tenant.

Give matrix cases stable identifiers so failures can point back to the requirement they violate.

Separate policy tests from application enforcement tests

OWASP recommends unit and integration tests for authorization logic, while cautioning that they do not replace dedicated security testing or penetration testing.

Organize the suite into three complementary layers:

  1. Decision tests: Exercise policy rules against small, deterministic fixtures. Cover both sides of each rule, including missing relationships and unsupported actions.
  2. Integration tests: Verify how authenticated identities become authorization subjects, how resources and tenants are resolved, and how permission-service results are interpreted.
  3. End-to-end enforcement tests: Send requests through actual application entry points and inspect responses, stored state, and side effects.

Keep component responsibilities explicit. For Ory Network, Ory documents identity and user management built on Kratos, OAuth 2.0 and OpenID Connect built on Hydra, and fine-grained permissions based on Keto. Test the application’s connections between those responsibilities rather than stopping at successful login or token validation.

Pair a denied request with an authorized control using the same route and equivalent input. Use the control to rule out a broken route, invalid payload, or missing fixture as the reason the prohibited operation did not succeed.

Use mocks to exercise specific error paths, but keep real integration coverage for subject identifiers, tenant resolution, and permission-request construction.

Test object, function, and field boundaries

Use separate test groups for object, function, and field boundaries. OWASP’s authorization guidance illustrates identifier tampering with an account identifier changed to request another account’s transactions. It also distinguishes authenticated ordinary users from users authorized to perform administrative actions.

Object access: Seed two users in one tenant and another user in a second tenant, with distinct private resources. Start with a permitted request, then replace resource identifiers in paths, query parameters, request bodies, and nested references. Include list, search, download, export, and bulk routes where they exist.

For collection responses, inspect every returned item and any metadata your policy protects, such as counts or summaries. Define the intended behavior for mixed-authority bulk requests: reject the whole operation or process only authorized items with explicit per-item results. For partial processing, assert that denied items remain unchanged and undisclosed. For whole-operation rejection, assert that none of the requested business changes occurred.

Function access: Call privileged operations directly with a valid lower-privilege session or token. Exercise supported methods and alternate routes. A hidden button should not be the mechanism your test relies on to prevent an operation.

Field access: Define readable and writable fields separately. Through ordinary update endpoints, attempt to change protected properties such as ownership, tenant assignment, or administrative status. Include nested and bulk payloads if supported. For reads, check that callers cannot retrieve fields outside their permitted view.

For a denied update, use this assertion sequence:

  1. Capture the protected record’s relevant state.
  2. Submit an otherwise valid request as an unauthorized subject.
  3. Assert the application’s documented rejection behavior.
  4. Inspect the response for protected data.
  5. Confirm that protected state remains unchanged and prohibited business work was not enqueued.

Use the application’s chosen disclosure policy for responses such as 403 or 404. Do not make the status code the only assertion.

Cover direct, inherited, and overlapping grants

Build relationship-shaped fixtures for relationship-based policies. Ory’s Keto documentation describes fine-grained relationships and inheritance through groups, roles, and hierarchies. Its introductory document model grants write to editors and read to editors or viewers.

For a comparable application policy, test:

  • A direct grant with no inherited grant.
  • An inherited grant with no direct grant.
  • Two independent paths granting the same permission.
  • Removal of one path while another remains valid.
  • Removal of the subject from a granting group.
  • Removal of the final permitted path.
  • An unrelated relationship that must not confer access.

The important expectation is not “removing a grant always denies access.” Specify whether another valid path should continue to authorize the subject.

Also test the boundaries of inheritance. For example, if your policy lets a folder grant access to its children, include a sibling folder and an unrelated tenant to check where that inheritance must stop. These are application test fixtures, not assumptions about a built-in product testing framework.

Exercise permission changes with existing sessions

Do not test every policy state only with freshly authenticated users. Reuse a session established before a permission change to exercise membership removal, role changes, ownership transfers, and resource moves.

For each transition, specify:

  • The permission before the change.
  • The relationship or attribute being changed.
  • The expected permission afterward.
  • The acceptable propagation or revocation window.
  • The application paths and caches involved.

Take the timing requirement from the deployed system’s documented contract. Do not assume immediate revocation merely because a relationship update returned successfully. If the application has no defined revocation requirement, establish one before treating the test as a release gate.

Test requests before the change, during any permitted transition window, and after its deadline. Use bounded polling with a clear failure deadline rather than an arbitrary sleep.

For background work, define when authorization must hold: at submission, execution, or both. Then test a queued operation whose requester loses access before a worker executes it. Apply the same discipline to concurrent operations: define what a permission change is allowed to affect before writing the race test.

Inject failures without turning them into grants

OWASP’s testing guidance asks whether an application terminates safely when an access-control check fails, including under abnormal conditions.

For operations requiring authorization, inject relevant failures at the permission boundary:

  • Timeouts and connection failures.
  • Invalid or incomplete responses.
  • Unresolved identity-to-subject mappings.
  • Missing tenant context.
  • Permission-cache failures, if the application uses such a cache.

Assert that a failure does not itself become a permission grant. If the application has a fallback, test it against the same authorization and freshness requirements. Without a valid authorization decision, protected reads and writes must not proceed.

The application may return an operational error rather than a policy-denial response; test that distinction deliberately.

Inspect effects as well as responses. A rejected write should not modify the protected record, enqueue the prohibited operation, or trigger that operation’s external notification. For a rejected read, inspect the response body for protected values.

Keep genuine policy denials distinguishable from dependency failures in telemetry. Assert useful diagnostic fields without logging credentials, tokens, or protected payloads. An expected denial audit event should not be mistaken for a prohibited business side effect.

Make authorization regressions reproducible

Run deterministic decision tests and critical enforcement cases on every change. Schedule broader generated, concurrency, and failure-injection suites according to their runtime and operational cost.

Use property-based tests for requirements that truly are invariant in your policy. For the private-document example, a read-only subject must not change document content, and a subject outside the resource’s tenant must not gain access through a document grant alone. Generate relationship combinations around those requirements and preserve failing seeds.

In an isolated test environment, deliberately remove an enforcement check or weaken a tenant predicate. Confirm that a relevant test fails. Treat this mutation exercise as a check on the suite—not as proof that every authorization defect is covered.

Track coverage by protected operation, decision branch, denied path, and grant transition, rather than relying only on line coverage. Every authorization defect should become a regression case with isolated fixtures and explicit expectations.

The release question is concrete: for each protected operation, can the suite demonstrate both permitted behavior and rejection of prohibited behavior, including what happens when permissions change or dependencies fail?

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗