Beyond “200 OK”: Building Secure APIs Through Strong Authorization Checks

A successful login does not grant access to every record. Learn how to enforce and test authorization for each resource and action.

By Pradunya Suryawanshi

• 9 min read
Share:
Cartoon developer comparing an authorized invoice request with an authenticated request that is denied.

A successful login does not grant access to every resource.

Introduction: A successful response is only one part of the story

A customer signs in to an application and opens an invoice. The API receives the request, the server returns “200 OK,” and the experience appears seamless. Behind that simple interaction, several security checks should work together to ensure the customer can access exactly the information they are permitted to see.

One important check is authorization: confirming that the signed-in user has permission to access the specific record or perform the requested action. A secure application does not rely on a successful login alone; it evaluates access at the resource level before returning data or making a change.

This article uses fictional customers and test records to explain Broken Object Level Authorization (BOLA), a common API security risk, and to outline practical ways teams can prevent it through design, implementation, and testing. The examples are illustrative and do not describe an incident at any organization.

1. The security principle behind a successful response

Consider a fictional invoicing platform used by businesses in different countries. Customers can sign in, view invoices, download receipts, and manage billing information. The backend exposes an endpoint such as:

GET /api/invoices/1042

The server authenticates the user, identifies the requested invoice, and returns the permitted information. Now imagine Alice owns invoice 1042 while Bob owns invoice 1043. Alice’s ability to use the invoice endpoint does not automatically grant her access to every invoice in the system.

Two distinct questions need clear answers:

Authentication: Who is making this request?

Authorization: Is this person allowed to perform this action on this specific resource?

Authentication establishes identity. Authorization evaluates permission in context, including the resource, the requested action, and the application’s business rules. A successful login answers only the first question.

2. Understanding BOLA through a controlled example

During a planned security review, a team can use two fictional test accounts and test invoices to confirm that resource-level permissions behave as intended.

For example, Alice requests her own invoice:

GET /api/invoices/1042
200 OK

The expected result is that Alice receives only the information she is authorized to access. The reviewer can then test a second case in the approved test environment: Alice requests invoice 1043, which belongs to Bob. The expected result is an access denial, with no protected invoice data returned and no unauthorized action performed.

If an API were to return Bob’s invoice in that second case, it would indicate a missing or incomplete object-level authorization check. This type of weakness is known as Broken Object Level Authorization (BOLA), and it is closely related to Insecure Direct Object Reference (IDOR).

OWASP lists BOLA as API1:2023 in the API Security Top 10. It can affect operations that read, update, or delete resources—not only endpoints that return information. Similar checks are relevant to document IDs, order numbers, employee profiles, and records belonging to another organization.

The identifier itself is not the problem. Identifiers are a normal part of API design. The key requirement is that the server evaluates permission for the requested resource and action.

Cartoon developer inspects Alice’s invoice 1042 while Bob’s invoice 1043 remains locked.
Resource ownership matters even after authentication.

3. Why authorization deserves its own tests

A development team may already test that customers can sign in, unauthenticated requests are rejected, invalid IDs are handled correctly, responses contain expected fields, and automated checks pass. Those are valuable tests, but they do not necessarily prove that every customer can access only their own permitted records.

A test confirming that Alice can read Alice’s invoice does not establish what should happen when Alice requests Bob’s invoice. Resource ownership, role permissions, organization boundaries, and delegated access should be tested explicitly.

This is especially important in multi-tenant applications, where multiple customers use the same application and may share database infrastructure. Authorization tests help verify that each customer boundary is respected.

Examples of where resource-level authorization matters include:

E-commerce: order details, delivery information, and returns.

Healthcare: appointments and records assigned to the correct patient or care team.

Financial services: account details and transaction history.

Enterprise software: documents and workflows available to the right organization or team.

These are illustrative examples of the kinds of access rules teams should validate; they are not claims that any particular application is vulnerable.

4. Implementing authorization on the server

Using UUIDs or long, random identifiers can make references less predictable, but it does not establish permission. Even a hard-to-guess identifier must never be treated as an access credential. The authorization decision belongs on the server and should be based on the application’s actual access rules.

Pattern A: Scope retrieval to resources the user may access

For a straightforward system in which each invoice belongs to one customer, the query can include the customer’s authorized scope:

principal = authenticate(request)

invoice = find invoice where:
    invoice.id == requested_invoice_id
    AND invoice.customer_id == principal.customer_id

if invoice does not exist in the permitted scope:
    return NOT_FOUND

return permitted_invoice_fields(invoice)

Including the ownership condition in retrieval can reduce the chance that a record is loaded outside the user’s permitted scope. More complex applications may support shared accounts, delegated access, multiple organizations, or team membership; their queries must reflect those rules rather than assuming a single owner.

Pattern B: Evaluate an explicit authorization policy

When permissions depend on roles, team membership, delegated access, or other context, the application can load the resource and evaluate a clear policy before returning data or performing an action:

principal = authenticate(request)
resource = load_resource(requested_resource_id)

if not authorize(principal, "read", resource):
    return FORBIDDEN

return permitted_resource_fields(resource)

The authorization decision must happen before protected data is returned or a protected action is performed. A consistent policy should also define how missing resources and access denials are handled. Depending on the application’s disclosure requirements, a 403 Forbidden or 404 Not Found response may be appropriate.

Both patterns can be valid. The important point is to check the actual resource and requested action—not merely whether the caller is allowed to reach the endpoint.

5. Verify the expected behaviour

A security improvement should be validated with tests, not only reviewed in code. For the fictional invoicing platform, a test matrix could include:

Test caseExpected result
Alice requests Alice’s invoiceAccess allowed
Alice requests Bob’s invoiceAccess denied; protected data is not returned
Bob requests Bob’s invoiceAccess allowed
An unauthenticated visitor requests an invoiceAuthentication required
Alice attempts to modify Bob’s invoiceAccess denied; invoice remains unchanged
Alice attempts to delete Bob’s invoiceAccess denied; invoice remains available

The precise HTTP status code for an unauthorized resource may vary. The essential requirement is that protected data is not disclosed and unauthorized changes do not occur.

Apply the same principle to GET, PATCH, PUT, DELETE, file downloads, bulk exports, and alternate API routes. For modification and deletion tests, verify the resulting state of the system as well as the response status. Testing a status code alone is not enough.

6. Keep authorization consistent as the application evolves

Applications evolve: teams add mobile endpoints, bulk exports, administrative dashboards, document-generation workflows, and background workers. Each new path that accesses protected records should follow the same access rules. Authorization that exists only in one controller or user-interface flow can be difficult to maintain consistently.

Centralize and reuse authorization policies where practical, while ensuring every operation that accesses a protected resource applies the appropriate policy.

Apply least privilege: grant users only the access their role, relationship, and business purpose require. Define administrative access explicitly as well.

Deny by default: if a request does not satisfy an explicit access rule, do not assume permission.

Test allowed and denied access for reads, updates, deletes, exports, downloads, and other relevant workflows. Add regression tests when a security issue is fixed.

Log security events safely. Capture enough context to investigate authorization failures, while avoiding passwords, access tokens, full invoice contents, and unnecessary personal information in logs.

Use defense in depth. Scoped queries, appropriate database constraints, tenant isolation, and other safeguards can add protection, but they complement rather than replace correct authorization decisions.

These practices align with OWASP’s Authorization Cheat Sheet, which recommends least privilege, deny-by-default behaviour, and permission validation on every request.

Cartoon developer beside a checklist covering identity, permission, resource validation, consistent checks and edge cases.
Apply consistent checks and test the boundaries of access.

7. A practical checklist for an API review

Before releasing or changing an endpoint that accepts a resource identifier, ask:

Who is the authenticated principal?

What action is the caller requesting?

Which specific resource will be accessed?

Which rule grants this principal permission to access that resource?

Is the authorization rule enforced on the server for every relevant operation?

Have tests covered different users, roles, and tenants?

Do tests verify that unauthorized data is not returned and unauthorized changes do not occur?

Are nested resources, bulk operations, file downloads, and alternate API routes covered?

Can security events be investigated without recording sensitive information in logs?

If any answer is unclear, treat it as an opportunity to strengthen the design or add a targeted test before release.

Conclusion: Make secure access part of the design

A successful API response confirms that a request was processed; it does not, by itself, prove that every security rule was satisfied. Authentication establishes identity, while authorization determines what that identity may do with a particular resource.

By checking permissions at the resource level, applying the same rules across every access path, and testing both permitted and denied scenarios, teams can build more dependable APIs and help keep customer information within its intended boundaries.

Secure access is not a single check at the edge of an application. It is a consistent engineering practice that should be built into design, implementation, review, and testing.

References and further reading

OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization

OWASP Authorization Cheat Sheet

OWASP REST Security Cheat Sheet — Access Control Assessment

Editorial note: All customers, invoices, endpoints, and requests in this article are fictional and are used solely to explain secure design and testing practices. Security testing should be performed only on systems you own or have explicit permission to test.

Ready to Transform Your Business with AI?

Get expert guidance on implementing AI solutions that actually work. Our team will help you design, build, and deploy custom automation tailored to your business needs.

  • Free 30-minute strategy session
  • Custom implementation roadmap
  • No commitment required