API security: keeping each customer's data separate
A valid login does not decide which order a user may open. Object checks, verified tenant context and tests for APIs, exports and background jobs.

Changing one address can bypass the entire interface
In an illustrative customer portal, a buyer opens /orders/4821. They change the number in the address and the server returns another company's order. Login works, the user has a customer role and the page hides administrator controls. The failure happened when the server decided whether this particular record was accessible. Changing the page layout cannot repair that decision.
OWASP calls this problem Broken Object Level Authorization. Random UUIDs make addresses harder to guess, but they do not replace permission checks. Identifiers can also appear in links, exports or logs. The server must verify the permitted action whenever it uses an object. The same rule applies to viewing, updating, deleting and downloading an attachment.
Identity, membership and permission are separate decisions
Authentication establishes identity. Membership establishes the organisations in which that identity may work. Authorisation establishes the permitted action inside the selected organisation. One accountant can manage two companies with different rights in each. A role attached only to the user, without an organisation scope, would not describe that arrangement correctly.
The client may send the selected organisation's identifier, but the server verifies the user's relationship to it. An X-Tenant-ID header selects context; it does not prove membership. In this illustrative design, the server creates a verified request context and subsequent layers use that context. Missing rules deny access. A fallback that reads every record would expand access whenever developers introduce a new feature.
Include tenant scope in the database query
Rather than loading any order and filtering it in the browser afterwards, the server can search directly within the verified scope. The following parameterised SQL query illustrates part of a read operation. :tenant_id comes from verified server context. Before executing the query, the server must check permission to read orders and any additional record restrictions required by the business rules.
The same restriction must appear in UPDATE and DELETE operations. Otherwise the system has a protected detail page but an unprotected change operation. Database relationships can use a tenant_id and id pair so an attachment cannot reference another tenant's order. That constraint protects relationship integrity; the application still decides which operations the user may perform.
SELECT id, status, created_at
FROM orders
WHERE tenant_id = :tenant_id
AND id = :order_id;Exports and caches need the same boundaries
Protecting order details is insufficient if the CSV export reads every order. Check record counts, search, aggregate reports and batch operations separately. Even a result count can reveal that another company's data exists. The response to an inaccessible object must reflect whether the server may acknowledge the object's existence at all.
Cache boundaries must distinguish every value that affects permission or response shape. An order:4821 key may be insufficient when identifiers are unique only within a tenant. Adding tenant_id does not fix a cache that shares different user-specific representations of the same order. The team should decide which responses may be shared and how permission changes invalidate them.
Background jobs need verifiable context
A worker generating an export may have no active user session. Its job record therefore needs the tenant, requester and requested operation. The worker verifies the job's integrity and the permission required by the system's rules. If membership is revoked after the job enters the queue, the design must state whether the export is cancelled or continues under a separately approved service operation.
The resulting file also needs a protected delivery path. A random filename and a private bucket reduce exposure, but do not replace the check when a download link is issued. Support staff working across tenants should have a separate role, a reason for access and an attributable record. An ordinary customer session must not acquire service privileges through a freely supplied header.
Test two companies and several user permissions
Prepare two separate customer accounts, a user allowed to read and a user without that permission. Exercise the same operation through a detail page, a direct HTTP request, an export and a background worker. Check the actual data and side effects, not just the status code. A rejected request must not change an order before returning an error.
Keep these tests around the shared authorisation layer and important business operations. A new endpoint, export or cache change then receives a specific regression check. A reproducible test involving two customers gives the team better evidence than a general claim that the system requires users to log in.
- Changing an order ID cannot read or update another tenant's record.
- Revoked membership blocks new exports and delivery of existing files according to the agreed rule.
- The cache cannot return a broader representation to a user with fewer permissions.
- A batch containing another tenant's ID returns the documented result without a partial unauthorised write.
Sources and documentation
For implementation, consult the documentation for the version you use.
