Identity vs access
API authentication does not authorize the object.
API Authentication Does Not Stop Broken Object Authorization
Authentication establishes the API caller, while API security applies that identity to object-level and function-level authorization before business logic runs.
“The endpoint requires a bearer token, so users cannot access each other’s records.”
Let me stop you before that becomes a production incident with an API reference attached.
API authentication does not authorize the object.
A valid credential establishes who is asking.
It may hand your API a user ID, roles, scopes.
The invoice, profile, document or tenant record named in your request is a different thing.
Your service still has to decide whether that established identity may access that object.
No one says “the receptionist recognized me, so I own every filing cabinet in the building.”
Put the receptionist behind a bearer token and suddenly every record identifier becomes community property.
It doesn’t.
Your API-security pipeline is explicit.
Authentication first. Object-level authorization.
Function-level authorization.
Schema validation.
Then the rate-limit gate, before backend resources are spent.
Authentication supplies claims to the later checks.
It does not issue a green light those checks can frame and hang on the wall.
1. “But the User ID Comes From a Signed JWT.”
Fine.
The user ID tells your service which authenticated caller is making this request.
It doesn’t tell your service whether that caller may read invoice INV-1042.
Or modify profile P-77.
Or retrieve a record belonging to another tenant.
Those are relationships between the caller and the object.
They have to be evaluated where both things are present.
Your API pipeline describes the object identifier arriving through a path parameter, a query string, a header or a body field.
That is the point people keep stepping around.
Your API does not authorize a theoretical invoice.
It authorizes the particular identifier that arrived in this request.
A token claim establishes a subject. Object authorization evaluates the subject’s relationship to a requested record.
The same endpoint can serve a different object on every call.
A successful request for one invoice is not a reusable authorization result for the next invoice.
Caching that distinction away isn’t a performance optimization.
It is a permission broadener wearing a lanyard.
The signature is useful evidence that a trusted issuer made a claim.
It is not a tiny legal clerk inside your token who has inspected your request and approved every identifier the caller can type.
2. “But the Route Is Behind the Gateway.”
Good.
A gateway is a sensible place to run checks before business logic.
Kong Gateway documents authentication, ACL or authorization, request validation, and rate limiting in a single access phase.
Those plugins are ordered by per-plugin priority, not by an assortment of reassuring product boxes.
A request can be authenticated and still need object authorization, function authorization and validation before your application code sees it.
That single phase doesn’t turn four controls into one control.
It gives them a common execution location.
There is a difference.
Object-level authorization asks whether this identity owns, shares, has delegated access to, or otherwise may reach the specific record.
Function-level authorization asks whether your identity may invoke this route, HTTP method, RPC operation or GraphQL operation at all.
Those questions overlap only in the imagination of somebody who has written “protected endpoint” on an architecture diagram and gone for lunch.
A low-privileged caller can own a record and still lack permission to invoke an admin-only mutation, or a bulk-export operation.
Conversely, a role permitted to call GET on a route can still be denied the object from another tenant. DELETE is the obvious version: a role may reach GET and not a privileged verb.
Some production pipelines run the object check before the function check, because an off-limits target makes the function decision pointless.
Others run the function check first, because it is cheaper and independent of data.
Neither ordering is universal.
Both decisions are required before business logic.
3. “But Our Frontend Never Sends Another Customer’s ID.”
Your frontend is a client.
It isn’t an enforcement point.
It can omit a button.
Hide an identifier.
Decline to construct a request.
None of that changes the identifier your API actually receives in the path, query string, header or body.
Your backend still has to compare the caller and tenant context against the requested object before it returns data.
This isn’t an accusation that ordinary users are about to become packet surgeons between lunch and stand-up.
It is a design statement.
Your API is the point that receives your request and can enforce policy.
A browser’s intended behaviour is evidence about user experience.
It is not authorization evidence about a record.
OWASP’s REST Security Cheat Sheet draws a related boundary for input validation.
Payloads should use strong types, fixed ranges and constrained strings rather than arbitrary content. JSON Schema’s additionalProperties: false rejects fields not named in the schema.
Good.
That can stop an undeclared field reaching your application code.
It can’t make an allowed invoice_id belong to the caller.
Schema validation asks whether a request has an allowed shape.
Object authorization asks whether the allowed-shaped request names an allowed object.
A well-typed cross-tenant identifier is still cross-tenant.
4. “But the Database Uses a Primary Key.”
Then your database can identify the row very efficiently.
Congratulations on having a database.
A primary key isn’t an ownership proof.
It doesn’t say whether a caller owns your record.
Or has delegated access.
Or belongs to its tenant.
Or has permission through a shared-record policy.
It says which record your key identifies.
Your query or policy check has to bind the authenticated caller and tenant context to the object it loads.
That is the important word.
A query that finds a record by its primary key and only later wonders who asked for it has already separated the two facts your control needs to evaluate together.
Which is why your pipeline requires rejection before the object is fetched or acted on.
A failed object check returns 403, or 404, where confirming the object’s existence would disclose information.
That choice is conditional.
The requirement to avoid exposing your record, or its existence, to an unauthorized caller is not.
The distinction is almost offensively ordinary.
A library card identifies a borrower.
A catalogue number identifies a book.
Neither proves the borrower may take every book in the building.
Nobody designs libraries that way.
APIs apparently require a special seminar.
5. “But a Role Check Protects the Endpoint.”
It protects a different boundary.
Function-level authorization evaluates whether your caller’s role or scope permits the operation.
An admin-only mutation.
A bulk export.
A privileged HTTP verb.
An RPC method.
A GraphQL operation.
That control prevents an inappropriate function call regardless of which object the caller names.
Object-level authorization evaluates your record.
Function-level authorization evaluates the operation.
BOLA is not cured by the absence of broken function authorization, and broken function authorization is not cured by the presence of an ownership check.
Put them together and the decision becomes useful.
May this principal perform this operation, on this object, under this tenant or sharing policy?
Omit either half and one of the two obvious bad cases remains.
An administrator role isn’t a diplomatic passport to every tenant.
A customer’s ownership of one profile isn’t permission to invoke an administrative delete.
A route-level role check that never examines the object is a bouncer checking for a wristband before letting somebody choose which apartment to enter.
6. “But Querying Ownership for Every Request Is Expensive.”
Your request already needs an answer.
Omitting the answer doesn’t make your system faster.
It makes the access decision implicit, broad, and very hard to review after your tenant boundary fails.
Different ordering is permitted precisely because a data-independent function check may be cheaper than an object lookup.
Run it first where that is appropriate.
Then run the object check before the object is fetched or acted on.
That is an engineering choice about evaluation order.
It isn’t permission to delete a checkpoint.
Kong’s consumer-keyed rate limiting illustrates the same discipline about what a control is keyed to.
With authentication on a route, its rate-limiting plugin defaults to the authenticated Consumer. Without authentication, it falls back to client IP from X-Real-IP or X-Forwarded-For.
A per-IP limit does not establish which authenticated caller sent a request. Especially behind a corporate NAT or a mobile carrier.
Nor does a per-consumer rate limit establish ownership of the requested invoice.
AWS API Gateway’s account-level default of 10,000 requests per second, with a 5,000-request burst bucket and lower 2,500 RPS defaults in several newer Regions, is another useful reminder.
Rate limiting manages request volume.
Not object rights.
A 429 says the gate rejected traffic.
It doesn’t say the next allowed request may retrieve any row it names.
Your controls are not substitutes for each other merely because they all happen before your database.
A turnstile, a route guard, a schema validator and a rate limiter are four different things.
Calling them all “the API gateway” does not turn them into an ownership policy.
7. “But We Tested the Main Endpoint.”
Which main endpoint?
OWASP API9:2023, Improper Inventory Management, documents a beta host, beta.api.socialnetwork.owasp.org, that lacked the rate limiting its production sibling enforced.
The point is not that rate limiting and BOLA are the same weakness.
They aren’t.
The point is that a correctly designed checkpoint on one exposed API does not teleport to another host, version or environment.
You can have a beautiful object policy in the current route, and an older route beside it that authenticates, fetches by identifier, and returns data.
Your production diagram will look terrific.
The unmaintained version will look like an incident report.
So test identifier substitution with controlled test identities and records from different tenants or access sets.
Cover path parameters, query strings, headers and bodies wherever your API accepts identifiers.
Cover every route, API version, host and environment that exposes the operation.
Verify that the denial happens before data or object existence is returned.
This is defensive testing, not a scavenger hunt.
It checks whether your stated tenant boundary survives the API inventory production actually exposes.
8. No, This Does Not Mean Every Object Needs the Same Owner Field.
It doesn’t.
Some objects are shared.
Some have delegated access.
Some are readable by a role within a tenant.
Some are intentionally available to every authenticated caller.
A universal owner_id comparison would be a bad replacement for a missing policy, because it would merely make the wrong policy uniform.
The requirement is less theatrical and more useful.
Express your policy explicitly.
Does this caller own the object?
Is the caller in the object’s tenant?
Has access been delegated?
Is this a shared record?
Does the caller’s role permit this operation on this class of object?
Evaluate the applicable answer for the identifier in your request.
You still need strong authentication, too.
WebAuthn, the W3C API within FIDO2, uses origin-bound public-key cryptography: the private key stays in the authenticator, and the relying party stores the public key.
NIST SP 800-63B treats verifier impersonation resistance as an AAL3 property.
That is a substantial improvement in proving your caller at login.
It does not decide who owns an invoice after login.
A session cookie represents a completed authentication event.
Not the strength of the factor that produced it.
Authentication’s job remains authentication.
Object policy’s job remains object policy.
Promote either one to the other and you have given a useful control a job it cannot perform.
9. What You Should Actually Do
Authenticate first, and carry the resulting identity claims forward. Then load or policy-check the requested object against the caller and tenant context, before returning data, confirming existence, or running business logic.
Enforce function permission separately for the HTTP method, route, RPC method, GraphQL operation, admin mutation or bulk export.
Make the rejection behaviour intentional. Return 403 where the authorization failure can be disclosed; 404 where existence itself is sensitive.
Either way, reject before the normal object-fetch path.
A response that helpfully proves an invoice exists for another tenant has already answered more than it was asked.
Keep schema validation strict. OWASP calls for a 413 when a request exceeds an appropriate size limit, and additionalProperties: false turns undeclared fields into rejections.
Those are useful controls for the operation’s full contract.
They do not replace authorization.
Run the complete sequence consistently. A cheap structural JSON or size check can precede authentication to shed malformed traffic. The fuller, operation-aware schema check still belongs after authorization.
Optimizing the order does not repeal the policy.
Test identifier substitution across the entire inventory. Log denied cross-tenant attempts with the caller, tenant, object, route and decision context you need for investigation.
Do not log bearer tokens.
A log full of credentials isn’t observability.
It is an overdue session-theft incident with excellent indexing.
10. The Actual Model Is Not Complicated.
Authentication answers: who is making this API request?
Object-level authorization answers: may that caller access this record?
Function-level authorization answers: may that caller perform this operation?
Schema validation answers: is your request the allowed shape?
Rate limiting answers: may this identity, key or source spend more backend capacity now?
Your gateway may host several of those in one access phase.
Your application may implement some of them.
The placement can vary.
The distinctions cannot.
Possession of a valid credential does not establish a right to an arbitrary object identifier.
Use bearer tokens.
Use gateways.
Use roles.
Use schemas.
Use rate limits.
Use WebAuthn where it fits.
Then check the invoice.
Objections
- Object-level authorization evaluates the particular identifier in the current request, while function-level authorization evaluates whether the caller may invoke the route or operation at all.
- A valid token carries claims forward for authorization but does not turn a user ID, role, or scope into proof of access to an arbitrary object.
- A denied object request returns 403 or, where existence disclosure is a concern, 404 before the object is fetched or acted on.
- A gateway can run authentication, authorization, validation, and rate limiting in one access phase, but its plugin order must still preserve both authorization decisions before business logic.
- Frontend behavior, primary keys, and an endpoint-level role check do not bind a requested object to a caller or tenant.
- Identifier-substitution tests must cover every route, API version, host, and environment because an incomplete pipeline can survive beside a protected production endpoint.
- Denied cross-tenant attempts should be logged without logging bearer tokens, while legitimate shared and delegated access is represented as explicit object policy.
What this does not say
This does not mean every object must use the same ownership model; shared records and delegated access are valid, but each must be expressed as an explicit policy rather than inferred from possession of a token.
Raised in review
- Does a valid bearer token let someone read any invoice ID?
- No. A bearer token establishes an authenticated caller and can carry claims such as a user ID, roles, or scopes. It does not establish that the caller owns or may access the invoice identifier in the current request. The service or gateway evaluates object-level authorization against that specific invoice and rejects the request before returning data or confirming that the invoice exists.
- What is the difference between broken object authorization and a missing role check?
- Broken object authorization concerns whether an authenticated caller may access the particular record named in a request. Broken function authorization concerns whether that caller may invoke the operation at all, such as an administrative mutation, bulk export, or restricted HTTP method. Both checks are necessary: ownership alone does not permit an admin function, and a role alone does not permit access to another tenant's record.
- Why does the object check have to use the ID in this request?
- The same API route can name a different object on every call through a path parameter, query string, header, or body field. A prior decision about one object therefore does not authorize the next identifier. Object-level authorization evaluates the caller and the exact requested object each time, rather than treating a previous successful request as a reusable permission for every record served by the endpoint.
- Should an API return 403 or 404 when access to an object is denied?
- An API can return 403 when the caller is known but not permitted to access the requested object. It can return 404 when revealing the object's existence would itself disclose information to an unauthorized caller. In either case, the object-level authorization decision occurs before the object is fetched or acted on, so the response does not leak object data through the normal application path.
- If a route is behind an API gateway, is object authorization already handled?
- No. A gateway can authenticate a request and run authorization plugins, but an object-level decision still needs the requested object identifier and an applicable ownership, tenant, sharing, or delegation policy. Kong Gateway documents authentication, ACL or authorization, request validation, and rate limiting in one access phase, ordered by plugin priority. A single phase is not a single decision, and route protection is not record protection.
- Why does the frontend not prevent broken object authorization?
- A frontend can choose which identifiers it normally sends, but it is not an authorization boundary. The API receives the actual path parameter, query string, header, or body field and makes the access decision there. If the backend accepts an arbitrary identifier after only authenticating the caller, an ordinary interface restriction leaves the object-level policy absent rather than enforced.
- Does a database primary key prove that a record belongs to the caller?
- No. A primary key identifies a record; it does not express whether the authenticated caller owns it, shares access to it, acts under delegated authority, or belongs to its tenant. The query or policy decision binds the caller and tenant context to the requested object before data is returned. Identifier uniqueness is a data-model property, not an access-control decision.
- Can an Admin role replace an object-level authorization check?
- No. A role or scope can answer whether a caller may invoke an operation, which is function-level authorization. It does not by itself answer whether the particular profile, invoice, tenant record, or other object is in scope for that caller. A correctly privileged role can still be denied access to an object outside its tenant, while a caller who owns an object can still be denied an admin-only operation.
- How can an API test for broken object authorization safely?
- A controlled test uses authorized test identities and test records from different tenants or access sets, then verifies that identifier substitution is denied on every route, host, version, and environment. The test covers identifiers in paths, query strings, headers, and bodies, as applicable. It also verifies that denied requests return 403 or 404 before data or object existence is exposed and that denials are logged without tokens.
- Why must API versions and beta hosts be tested separately?
- A protection implemented on one production endpoint does not automatically exist on another host, version, or environment. OWASP API9:2023 documents an inventory-management case in which beta.api.socialnetwork.owasp.org lacked the rate limiting enforced by its production sibling. Authorization has the same deployment risk: a complete pipeline is useful only where every exposed copy of the API actually runs it.
- Is checking ownership on every request always the right policy?
- No. Some objects are shared, delegated, public to an authenticated population, or available to a role within a tenant. The requirement is not a universal owner-column comparison. The requirement is an explicit object-level policy that evaluates the authenticated caller, the requested object, and relevant tenant or delegation context. Possession of a valid credential alone is not a policy for arbitrary identifiers.
- What should be logged when an API denies cross-tenant access?
- The service logs the denied authorization event with enough request, object, tenant, and caller context to investigate a cross-tenant attempt, while excluding bearer tokens from logs. The denial is valuable security telemetry because it shows that the object-level control ran and rejected the request. Logging a credential would create a separate session-theft risk and does not improve the authorization decision.