Research & Analysis · Security operations · 18 min read

How Do You Test API Object Authorization Across Tenants?

Sources checked against the cited passages; awaiting my final read before this is indexed.

I test API object authorization by comparing requests against an explicit permission model, using controlled accounts and objects on both sides of each boundary. A passing denial must keep protected data out of the response and prevent forbidden effects, while a matching permitted request still works. The test also needs to cover changed permissions, alternate access paths and delayed jobs. A valid token or a hidden interface button cannot establish those properties.

The difficult question is what a passing test actually proves. An API can reject a foreign object ID in one handler while leaking the same object through an export, accepting a forbidden field update or authorizing a queued job with yesterday's membership. I propose a method for making those claims testable. It combines primary standards, assessment guidance and a published authorization-system design; the worked example is hypothetical, not a claim about a client deployment or a measured vulnerability rate.

Start with the business decision the server must enforce

Before selecting test cases, I would ask the product owner to resolve who may do what to which object. Consider a fictional document service with two tenants, project membership, document viewers and editors, and a separate export permission. A tenant administrator manages membership but cannot automatically read every document. An editor can revise an active document but cannot change its tenant or remove a retention hold. Those are deliberate example rules, not assumptions about how every application should work.

NIST's attribute-based access-control guidance evaluates subject, object, requested operation and sometimes environmental attributes against policy or relationships (Hu et al., 2014). That gives us a useful vocabulary, but the vocabulary does not supply the business rule. For this service, I would represent a decision as P(subject, action, object, context, policy_version). Context includes the tenant relationship and relevant object state. A role is one input, not the whole predicate. An editor role without the project relationship should not grant access.

The expected result must come from a specification that can disagree with the implementation. If your test calls the production policy function to calculate its own expected answer, a mistake shared by both sides can pass unnoticed. I would have the product owner and a second engineer review a small decision table, then encode expected outcomes independently. The table is an oracle in the testing sense: an explicit account of correct behavior. It can still be wrong, so ambiguous rules are specification defects to resolve before declaring coverage.

Attribute provenance belongs in that table. The server can receive an object ID or requested tenant from a client, but it should establish their meaning through trusted state. A client-supplied owner_id is a requested value, not evidence of ownership. Hu et al. (2014, §§3.2.2.4–3.2.2.10) discuss integrity, authoritative attribute sources and attribute management. My application of that guidance is to record the source and freshness requirement for each consequential input. Otherwise, a carefully written policy may evaluate facts the caller was allowed to invent.

Separate object, function and field permissions

Broken object-level authorization concerns access to a particular object through a function the caller can use. OWASP's API1 guidance requires checking permission for the requested action on the requested record and explains why comparing a session user ID with an arbitrary object ID is insufficient (OWASP Foundation, 2023a). In the hypothetical service, a viewer may use the document-read endpoint but only for documents covered by an explicit relationship. Unpredictable identifiers can make discovery harder; they do not establish that relationship.

Function permission asks whether the caller may invoke the operation at all. OWASP API5 distinguishes this problem and recommends explicit grants with denial by default (OWASP Foundation, 2023c). Reading a document should not imply permission to change membership or create an export. I would test each operation directly through an approved test client. The browser's decision to hide a button has no place in the expected server-side authorization result.

Field permission is another boundary. OWASP API3 addresses both excessive disclosure of object properties and unauthorized modification of sensitive properties (OWASP Foundation, 2023b). In our example, permission to edit a document title does not include changing tenant_id, owner_id or hold_state. Test the returned representation as well as the accepted input. A successful title update that also changes a protected field is an authorization failure even if the response omits that field.

I would record these as separate obligations rather than collapse them into “the endpoint requires authentication.” That makes failures actionable: the wrong function needs an operation guard; the wrong object needs a relationship check; the wrong property needs a narrower read or write contract. A single request can violate more than one obligation. Classification helps locate the missing decision, but it should not delay fixing a demonstrated path to unauthorized data or change.

Establish token validity without confusing it with entitlement

For APIs using the JWT access-token profile in RFC 9068, the resource server checks token type, issuer, audience, signature and expiry. The profile also says authorization claims should be evaluated with other available context; it does not define the application's object policy (Bertocci, 2021, §4). Those are separate tests. A token signed by a trusted issuer for another service should fail token validation, while a valid token for this service may still fail the document relationship check.

RFC 9700 recommends restricting access-token privileges and audiences and checking whether the token is intended for the requested resource and action (Lodderstedt et al., 2025, §2.3). A scope such as documents:read can bound an operation without identifying every document the subject may read. I would therefore include a valid, correctly scoped token in cross-tenant tests. Using an expired token for every denied case tests authentication failure and never reaches the authorization decision you intended to examine.

Some systems use opaque tokens and introspection instead of local JWT validation. RFC 7662 provides a way to query token activity and related context, while explicitly identifying the security trade-off created by cached responses (Richer, 2015, §§2.2, 4). Introspection does not automatically retrieve your application's latest project membership. Specify which authority owns each fact: token activity, tenant membership, object relationship and business state may live in different systems and change on different schedules.

This separation also improves diagnosis. Record whether rejection occurred at token validation, function selection, object policy or field policy. You need that distinction internally even when the client receives a deliberately restrained error. An authorization test should fail if it reaches the wrong layer, unless its purpose was to test that earlier layer. Otherwise, a broken login service can make an entire negative test suite look reassuringly green.

Build a matrix that changes one relevant condition at a time

OWASP's versioned testing guide recommends multiple users with different objects and privileges when assessing direct object references (OWASP Foundation, n.d.-b). I would create synthetic fixtures whose relationships are known: a permitted viewer, an unrelated user in the same tenant, a user in another tenant, an editor, and an administrator without document access. Use objects with controlled ownership and state, rather than guessing identifiers from live customer data. Every test should run within the agreed assessment environment and scope.

Start with a successful baseline and then alter a consequential input. Change the object while keeping the caller valid; change the caller while keeping the object and request valid; change the requested action without changing the relationship. This makes the difference interpretable. If you change identity, tenant, method, content type and object state at once, a denial cannot tell you which condition was enforced. More complex combinations matter later, but they should build on understood component tests.

The following is a proposed matrix for the fictional document service. These are expected outcomes under the stated example policy, not observed test results.

CaseControlled change from a permitted requestExpected resultObservation needed
ANone: current viewer reads an assigned documentAllowCorrect permitted representation
BSubstitute an unrelated document in the same tenantDenyNo protected content or metadata
CSubstitute a document in another tenantDenyNo foreign content and no cross-tenant effect
DViewer invokes the export operation without its grantDenyNo export job or downloadable artifact
EEditor adds a protected ownership field to a valid editReject or exclude it according to the contractProtected stored field remains unchanged
FRemove project membership, then repeat with the same valid tokenDeny within the defined propagation boundDecision version and timing recorded
GMix permitted and forbidden documents in a bulk operationFollow the specified atomic or partial-result contractEvery object's result and side effects checked
HLose access after queuing an export but before retrievalFollow the explicit job/retrieval policyNo download beyond the authorized scope

An expected denial needs a known existing object. Otherwise, the service might return “not found” because the fixture was never created. Verify object existence through the authorized control account, then test the unauthorized subject without changing the object. Keep fixture setup outside the operation being assessed, and clean up through a separate authorized path. A teardown that deletes evidence before assertions run can conceal an unintended write.

I would also retain a valid control after changing permissions. Removing one user's access should not disable the document for everyone. The denied request and the surviving permitted request together distinguish a working boundary from an unavailable service. For state-sensitive operations, record the exact transition: an archived object, a held object and a deleted object may require different outcomes. “Inactive” is too vague if the implementation treats those states differently.

Observe the effect, not just the response code

HTTP semantics allow an origin server to return 404 to conceal a forbidden resource's existence; 403 indicates refusal to fulfill the request (Fielding et al., 2022, §§15.5.4–15.5.5). Neither status alone proves that the application enforced the intended policy. The response body might contain a forbidden field, or a handler might reject only after enqueueing work. I would assert the externally visible result and the relevant durable effects separately.

For a write, inspect the protected state through a trusted read path after the request. For an export, check both job creation and artifact retrieval. For an invitation, examine whether a message or membership change occurred. In this proposed method, “denied” means no forbidden effect under the chosen operation contract. Legitimate security logging is an expected effect; an unauthorized email containing document content is not. The observation window must cover deferred effects rather than stop when the HTTP response arrives.

Collection interfaces need their own evidence. Search, pagination, counts, previews and bulk downloads can expose information that a detail endpoint correctly withholds. Seed distinct synthetic markers in forbidden records, then look for those markers in all relevant output forms. Also test empty and mixed result sets. A response that hides the record but includes its title in an error message still violates a policy that protects the title. This is an application-specific observation strategy, not a claim that every count or timing difference is equally sensitive.

Decide which existence disclosures matter before interpreting results. A public catalog may intentionally reveal that a document exists while protecting its contents; a private case system may forbid even that disclosure. Timing tests need repeated measurements and controls for noise. A single slower response is not evidence of a reliable inference channel. I would distinguish confirmed content exposure, a reproducible existence signal, and an inconclusive timing observation rather than label all three as the same defect.

Test the interval between permission change and enforcement

Authorization is a temporal claim. Suppose a project owner removes a viewer, receives a successful acknowledgement, and then adds a confidential attachment. Can the removed viewer read the new attachment using the old token? The answer depends on policy consistency, not simply token expiry. Pang et al. (2019, §2.2) describe the “new enemy” problem in Zanzibar: stale access-control state can expose new content to someone whose access was removed. Their design respects ordering between access-control and content changes. It is an existence proof of a design approach, not evidence that your application has those guarantees.

For our example, I would specify whether the removal acknowledgement means the denial is effective everywhere or merely that propagation has begun. Define the observable starting event, permitted delay, affected operations and treatment of already-running requests. “Revocation is immediate” is not a testable promise until those terms are fixed. A measurement beginning when an administrator clicks a button differs from one beginning when the authoritative policy store commits the change.

Use controlled scheduling to exercise the boundary. Establish an allowed request, remove membership, wait for the defined acknowledgement, then repeat with the same token against each relevant service instance or region. Test both existing content and content added after removal. Record the policy revision used for the decision where the architecture exposes it. If the service cannot report that revision, say what weaker evidence is available instead of inventing a precise consistency guarantee.

Caching requires a model of the actual chain. A local decision cache can renew an entry from an attribute cache that is already stale. Its nominal lifetime therefore does not necessarily bound end-to-end revocation delay. Conversely, several caches consulted concurrently do not automatically add their lifetimes. I would trace who refreshes from whom, which version travels with the data, and what invalidation acknowledges. Calculate a bound only after those assumptions are established, then test near the boundary. RFC 7662's warning about introspection caching supports the token-specific concern; the broader cache-chain analysis here is my proposed application method.

Couple authorization to the state change it permits

A current decision can still become stale before a write commits. Imagine an editor passes the permission check, a second transaction transfers the document to a different tenant, and the first request then writes using only the document ID. Whether that write is permitted depends on the transaction semantics you intended. A check performed earlier in the handler does not, by itself, establish that the same relevant state held at commit.

I would ask engineering to identify the linearization point: the moment the system treats the operation as having taken effect. Depending on the storage design, a transaction, an atomic conditional update or a policy/version precondition may bind the decision to the update. These are possible designs, not interchangeable recipes. A version condition is useful only if every policy-relevant change advances the checked version; a local database transaction cannot automatically make a remote membership service part of the same atomic decision.

Test that reasoning with a controlled interleaving in a non-production harness. Pause after policy evaluation, change the relevant ownership or membership state, then resume the operation. The expected result may be rejection, retry with fresh evaluation, or completion under a clearly documented earlier authorization snapshot. What matters is that the result matches the declared contract and does not silently cross a tenant boundary. A random load test may never exercise the critical interleaving, so its lack of failures is weak evidence for this property.

For a bulk operation, decide whether authorization failure on one item cancels the entire batch. If partial results are allowed, every processed item still needs its own justified decision and a bounded response. If all-or-nothing behavior is promised, checking objects one by one while committing each immediately contradicts the promise. I would test a forbidden item at the beginning, middle and end, and inspect the resulting state. The position changes what an early-exit implementation might have already done.

Replay behavior deserves a separate case when an API uses idempotency keys to avoid duplicate work. In the hypothetical service, a successful export request might be retried after the user's membership is removed. Decide whether the retry may return an acknowledgement, reveal the original result or initiate anything new. Those outcomes carry different authority. A mechanism that suppresses duplicate execution should not silently become permission to disclose an earlier result to a caller who can no longer retrieve it.

I would repeat a controlled request with the same key after the relevant permission change, then try that key from a different test principal. Inspect both the response and whether work was repeated. The expected result depends on the documented replay contract, but a cached response must remain within the requesting principal's permitted disclosure scope. Bind the test evidence to the caller and operation context as well as the key. This is a proposed application check; no claim is made that a particular queue or API framework implements that behavior automatically.

Carry the right authority through services and queues

A gateway often knows the external principal; a downstream service may see only a workload credential. NIST SP 800-204 distinguishes common access policies from fine-grained decisions closer to the microservice and discusses the consistency problem created by distributed enforcement (Chandramouli, 2019, §4.1). I would draw the path from entry point to data operation and identify where each component learns the principal, object and action. A private network location is not a substitute for that account of authority.

There is a material difference between a service acting under its own permission and acting for a user. RFC 8693 defines token exchange supporting delegation and impersonation; its actor claim can identify the current delegated actor (Jones et al., 2020, §§1.1, 4.1). Using that protocol does not decide which actions your worker should be allowed to perform. The receiving service still needs to validate the trusted context and apply its own policy. A user ID copied from an untrusted header is not equivalent to verified delegation.

Queued work needs an explicit lifetime. For the hypothetical export, I would choose revalidation before execution and before download, with cancellation if the initiating user loses the required relationship. Another service may deliberately execute a previously approved business instruction under separate organizational authority. That can be legitimate, but the authority, scope and cancellation rules should be named. It should not happen accidentally because a queue consumer has a broad service credential and never consults the initiating decision again.

The test sequence follows those decisions: enqueue while allowed, change membership before execution, inspect whether the worker runs, and attempt retrieval under the changed state. Repeat with removal after execution but before download. Include the artifact delivery path: blocking the job-status endpoint is insufficient if the resulting file remains retrievable elsewhere under a still-valid delivery grant. Record the lifetime and revocation behavior of that separate grant. A job identifier, artifact identifier and download capability may represent three different authorization surfaces.

Make policy failure and test coverage visible

A central policy service reduces duplicated rule definitions but adds a dependency. A local library reduces a network dependency but can drift between deployments. OWASP's Authorization Cheat Sheet recommends default denial, permission validation on every request and safe handling of authorization failures (OWASP Foundation, n.d.-a). I would preserve a distinction between “policy denies this action” and “the system could not obtain a trustworthy decision.” Both can prevent the protected action without pretending they are the same operational event.

Exercise an unavailable policy store, a timed-out attribute lookup, malformed policy data and a failed invalidation message. Observe whether retry, fallback or a circuit breaker creates permission that was never granted. If the business requires bounded use of a last-known decision, specify exactly which actions, data version and maximum age qualify. Treat that as an explicit risk decision with separate tests. An undocumented fail-open branch is not an availability strategy you can meaningfully review.

Coverage should be reported against authorization obligations, not only endpoint count. Ten routes may share one correctly enforced policy, while one route may expose reads, updates, exports and nested objects with different decisions. Maintain a map from each obligation to the relevant entry points, fixtures and assertions. Unit tests examine rule combinations; integration tests examine the actual identity and data paths. Neither substitutes for the other. A shared library's unit tests cannot show that every consumer invokes it before acting.

I would include deliberate fault checks in an isolated test harness: remove a required relationship condition or bypass one field restriction and confirm that the corresponding test fails. This probes whether the suite could detect the defect it claims to cover. It does not estimate production vulnerability prevalence. Report which fault classes were detected and which boundaries remain untested. A percentage without that denominator can conceal more than it explains.

Decide what the evidence can support

For a release review, I would assemble the approved permission table, deployed policy and application versions, fixture relationships, allowed and denied observations, state-transition results, deferred-effect observations and unresolved exclusions. The reviewer should be able to follow a consequential rule from its business meaning to an actual assertion. Logs can support that trace with a correlation ID, decision reason and policy version; raw tokens and customer payloads do not belong in the evidence bundle. Use synthetic identifiers and retain only what is necessary to reproduce the result.

My proposed release criterion is deliberately narrower than “authorization is secure.” For the named scope and configuration, all mandatory obligations have valid control cases, forbidden data and effects were absent in the tested cases, and transition behavior met the declared consistency contract. Missing observation of a queued effect is incomplete evidence, not a pass. A confirmed cross-tenant read or write contradicts the core boundary and requires correction before this method can support acceptance. Other exclusions need a named consequence and decision owner.

There are limits. The permission model may omit a business relationship; the test environment may differ from production; dynamic policies may create combinations beyond a finite matrix. Pairwise testing can reduce combinations but does not prove that interactions among three or more attributes are safe. Formal verification can strengthen reasoning about a model while leaving identity propagation, data freshness and deployment wiring outside its assumptions. I would use each technique for the claim it can actually establish.

The strongest practical objection is cost. Small teams cannot test every permutation on every commit. Start with tenant isolation, privileged actions, sensitive fields and revocation paths, then retain regression cases as the product changes. That prioritization should be explicit. “We tested the highest-consequence obligations” is an honest result; “every possible authorization path is verified” usually exceeds the evidence. The method presented here has not been empirically evaluated as a complete program, and the standards cited do not validate its effectiveness for every architecture.

If you need to know whether your API actually separates customers, request an API authorization briefing. Bring the permission model and a sanitized account of the request paths. I can help turn them into an assessment scope, a reproducible test matrix and a release decision tied to observable behavior. For the adjacent service-identity question, read who owns the identities your cloud services use; this paper tests what those identities may do to particular objects.

References