Tenant and object authorization
Carry trusted tenant context through reads, writes, jobs and events, then authorize the specific object at every boundary.
On this page
Before you start
- Tenant membership rules
- An action vocabulary
- Resource ownership relationships
An authorized object request
A caller supplied tenant or object identifier is a lookup input, never the authority for access.
Scroll across the diagram to see all four systems.
Request object
Present a credential and a target resource identifier.
Trusted identity context
Resolve issuer, subject, tenant membership and requested action.
Constrained lookup
Read the object inside the authorized tenant boundary.
Object attributes
Evaluate ownership, role, state and any field restrictions.
Allow or deny
Return a decision that applies to this action and object.
Permitted representation
Filter fields and return only the authorized view.
Resolve tenant context from authority
Treat a tenant identifier in a route, header or request body as untrusted input. Resolve the caller’s allowed tenants from the authenticated identity and membership system. Compare the requested tenant to that authority before querying protected data.
A service account may act for several tenants, but the grant must define that delegation explicitly. Do not let a downstream worker select an arbitrary tenant merely because it knows the connection identifier.
Authorize the object as well as the endpoint
OWASP identifies missing object authorization as a major API risk. Every endpoint that takes an object identifier and performs an action needs to check that the principal may perform that action on that object. A valid session and a role check at login are insufficient.
Constrain database queries to the trusted tenant and then apply resource policy. Validate parent and child relationships together: a contact route nested under an account must prove that the contact actually belongs to the allowed account.
def update_contact(principal, tenant_id, contact_id, change):
membership = authorize_tenant(principal, tenant_id)
contact = contacts.get(
tenant_id=membership.tenant_id,
contact_id=contact_id,
)
authorize_action(principal, "contacts.write", contact)
permitted = validate_writable_fields(change, principal)
return contacts.update(contact, permitted)Define which fields can cross the boundary
A user who can update a contact’s phone number may not be allowed to set its owner, tenant identifier or compliance state. Allowlist writable fields and validate transitions rather than binding an entire request body directly to a persistence model.
Read permissions also need field policy. A reporting role may need aggregated revenue while a support role needs contact context. Avoid returning a complete object and relying on the frontend to hide sensitive fields.
| Boundary | Required context | Failure to prevent |
|---|---|---|
| API request | Principal, tenant, action and object | Changing the URL to access a different tenant. |
| Background job | Delegated authority and target tenant | Executing a stored job under a broader worker identity. |
| Webhook handling | Verified connection and mapped organization | Trusting tenant_id inside an unsigned payload. |
| Cache lookup | Tenant and relevant authorization dimensions | Serving one tenant’s response to another. |
| File export | Export owner and fresh download permission | Reusing a long lived link after access is removed. |
Preserve authority through delayed work
An asynchronous job should carry the minimum delegation context and a reference to the initiating principal. Decide which permissions are rechecked at execution time, especially for destructive actions or exports. Authorization at submission does not automatically permit every later action.
When membership is revoked, stop future work according to the grant policy and make the job state observable. A worker’s technical permission to call an API does not establish the business permission of the original request.
Test neighboring records and indirect references
Use at least two tenants and multiple roles in contract tests. Try cross tenant object IDs, batch requests containing mixed tenants, parent and child mismatches, old export links and changed membership during an active job.
Record the policy decision with a safe principal reference and resource class. Do not log the full protected record merely to prove that an authorization denial occurred.
Evaluate an object permission
Change tenant membership, action scope or the writable field to inspect the authorization boundary.
/api/v1/tenants/tenant_a/contacts/contact_240Expected outcome
Action permitted
The tenant, action and field are within the principal’s authority.
Decision path
- Resolve tenant membership
- Constrain object lookup
- Check contacts.write
- Allow the phone field
Input schema 4 fields
| Field | Type | Meaning |
|---|---|---|
requested_tenantRequired | string | Tenant selected in the request. |
authorized_tenantsRequired | array | Tenants established by trusted membership. |
scopesRequired | array | Granted action scopes. |
fieldRequired | string | Field the caller intends to update. |
Standards and documentation
Explore the protocol specifications and platform guidance for this topic.
Bring the contract into your operating environment.
Discuss an implementation