Build tenant authorization by resolving a trusted tenant context, checking permissions server-side, and scoping every data operation to that tenant. A successful login establishes identity, but it doesn’t grant access to every customer workspace.
For a small SaaS team, the safest starting point is one consistent authorization path built around verified tenant context. Start by separating identity, organization membership, and permission decisions.
Define What Your Tenant Boundary Protects
A tenant is the workspace or organization whose resources must remain isolated. It may differ from the company paying your invoices because one buyer can operate several workspaces.
Authentication answers, “Who is calling?” Authorization answers, “May this caller perform this action on this resource in this tenant?”
Keep global user accounts separate from tenant memberships. A person can belong to several organizations with different permissions in each. Therefore, store roles against memberships rather than assigning one global “admin” flag.
Your access decision should require all applicable conditions:
- The caller has a valid authenticated identity.
- Their membership or service delegation permits access to the selected tenant.
- Their permissions allow the requested operation on that resource.
AWS describes tenant isolation as using tenant context to restrict resource access. This boundary applies even when customers share application servers and database tables.
Establish Tenant Context From Trusted Sources
Treat tenant selection as a request
A subdomain, workspace slug, URL parameter, or organization picker can identify the requested tenant. None proves that the caller belongs there.
Authenticate the caller first. Then resolve the selected workspace to your internal tenant ID and validate an active membership.
For SSO, maintain a server-side mapping between tenants and approved identity-provider connections. An email domain can support discovery, but it shouldn’t make the final access decision. Contractors, shared domains, and multi-organization users make that unreliable.
Also reject requests when the selected tenant conflicts with a tenant restriction attached to the authenticated credential.
Pass one verified context downstream
After validation, create a request context containing the principal ID, tenant ID, membership ID, and applicable permissions. Treat it as immutable within that request.
Downstream handlers should consume this context rather than rereading arbitrary headers or request-body fields. Missing context must stop execution before tenant-owned data access.
For tenant-scoped API or provisioning credentials, resolve the tenant from the validated credential’s server-side record. A SCIM credential must never select another organization through its payload.
A tenant identifier tells the server which boundary the caller wants to enter. Membership or delegated authority determines whether they may enter it.
Choose RBAC and ABAC Around Real Permissions
Use RBAC for stable job responsibilities
Role-based access control (RBAC) groups permissions into roles such as owner, administrator, editor, and viewer. It’s a practical starting point when responsibilities are predictable.
Define permissions as actions, such as document:read or member:invite. Then map tenant-scoped roles to those actions. This keeps endpoint code independent of role names.
An administrator in one workspace gains no authority in another. Likewise, provisioning a user shouldn’t automatically grant administrator access.
If directory groups assign roles, map each group to permissions within its own tenant.
Add attributes for resource-specific rules
Attribute-based access control (ABAC) evaluates facts about the caller, resource, or environment. Use it when roles alone can’t express the rule.
For example, an editor might update only documents they own, while an approved reviewer can update documents assigned to them.
Combine these approaches: RBAC grants a general capability, while attributes restrict the resources or conditions where it applies. OWASP’s authorization guidance explains both models and recommends contextual controls for finer-grained decisions.
Tenant membership remains mandatory regardless of the role or attribute rule. Also keep commercial entitlements separate: a workspace’s paid export feature doesn’t authorize every member to export.
Enforce Boundaries at Every Data Access
Scope reads and writes before execution
Every endpoint needs server-side authorization, including search, exports, bulk operations, file downloads, and administrative routes. Hiding a button only changes the interface.
For shared tables, query resources using both tenant and resource identifiers. A lookup shaped like WHERE tenant_id = :tenant_id AND id = :id makes ownership part of retrieval.
Apply the same restriction to updates and deletes. For creation, assign tenant_id from verified context rather than accepting it from the request body.
Validate referenced resources too. A document created in one tenant must not reference another tenant’s folder. Scope joins, counts, search indexes, and object-storage access with the same discipline.
Add a database boundary where appropriate
OWASP’s multi-tenant security guidance describes separate databases, separate schemas, and shared-table isolation. Each has different operational and security tradeoffs.
With PostgreSQL row-level security (RLS), database policies can add protection when application code misses a tenant filter. However, the application’s database role must not be a superuser or have BYPASSRLS.
Set tenant context with transaction-safe handling, especially with connection pools. Test the actual application role and pooling path, including reuse across tenants. RLS supplements action-level authorization; it doesn’t decide whether a viewer may edit.
Implement One Consistent Server-Side Path
For a document update, authenticate the caller, validate membership, load the tenant-scoped resource, and authorize the action before mutation.
This Python-style pseudocode uses application-defined helpers:
def update_document(request, identity, memberships, documents, policy):
principal = identity.authenticate(request)
membership = memberships.require_active(
principal.id, request.route["tenant_id"]
)
changes = parse_allowed_fields(request.body, {"title", "body"})
with documents.transaction():
document = documents.get_for_update(
tenant_id=membership.tenant_id,
document_id=request.route["document_id"],
)
if document is None:
raise NotFound()
policy.require(membership, "document:update", document)
return documents.update(
tenant_id=membership.tenant_id,
document_id=document.id,
fields=changes,
)
Here, authenticate rejects invalid credentials, while require_active rejects missing or inactive memberships. policy.require stops execution when the action isn’t allowed.
The field allowlist excludes ownership fields, including tenant_id. Both retrieval and mutation receive the validated tenant ID. The transaction groups the resource lookup and update; it doesn’t replace authorization.
For an authenticated caller, returning 404 when the scoped resource isn’t found avoids disclosing whether another tenant owns that ID.
Use shared enforcement helpers, but avoid a repository API that silently defaults to unrestricted access when tenant context is absent.
Carry Authorization Into Jobs and Service Calls
Background jobs need explicit tenant scope
A worker doesn’t inherit the browser request’s trusted context. Capture tenant and actor information from the validated server-side operation, then validate that context when processing the job.
Decide whether the job acts on behalf of a user or under a tenant-scoped system identity. That choice determines which permissions it needs.
For user-delegated work, reevaluate access at execution time because the user may have lost membership after submission. System-owned maintenance needs its own narrow authorization rules.
Scope worker reads and writes just like interactive requests. Also include tenant scope in deduplication keys so retries can’t collide across customers.
Internal services still need authorization
Authenticate the calling service, then authorize its requested tenant and operation. Being inside your network doesn’t grant access to every customer’s records.
A downstream service shouldn’t trust an unsigned tenant header simply because another application sent it. It needs authenticated caller identity and a validated basis for the delegated tenant scope.
Avoid turning one broadly privileged service credential into permission for any caller-selected organization.
Apply the same principle to scheduled exports and webhook-triggered work: establish which tenant the authenticated integration may affect before accessing resources.
Handle Revocation and Common Failure Modes
Offboarding must remove effective access, not merely change a directory record. Deactivate the relevant membership and make existing sessions and API access respect that state. Preserve unrelated memberships when the person belongs to other organizations.
If cleanup happens asynchronously, commit the access denial before starting follow-up work. A later successful SSO login must not reactivate a directory-disabled membership automatically.
Caching creates another common leak. Include tenant scope in resource cache keys. Authorization caches also need the principal, action, relevant resource context, and a revocation strategy.
Similarly, avoid reusing mutable global tenant state across requests or workers. Concurrent work can otherwise inherit another customer’s context.
Policy failures need explicit handling. If membership lookup or required policy evaluation fails, deny the protected operation. Don’t fall back to a permissive role.
For diagnostics, log the tenant, actor, action, resource identifier, decision, and correlation ID. Exclude credentials and sensitive payloads.
Test Cross-Tenant Access and Fail-Closed Behavior
Treat cloud tenant isolation as a separate security test concern. Correct role checks don’t prove that storage, caches, and asynchronous work preserve tenant boundaries.
Use two isolated test tenants with distinct records. Include a user belonging to both, with different permissions in each. For each denied request, assert both the response and the absence of data changes or side effects.
These cases form a useful regression baseline:
| Test | Required result |
|---|---|
| Read another tenant’s known resource ID | No resource data or metadata is returned. |
| Update or delete that resource | No mutation occurs. |
| Search, export, count, or bulk-read | Results contain only authorized tenant data. |
| Change a tenant header or body field | Validated context cannot be overridden. |
| Remove membership before job execution | User-delegated work loses access. |
| Omit tenant context or interrupt policy evaluation | The protected operation fails closed. |
| Reuse a pooled database connection across tenants | The previous tenant’s context doesn’t affect access. |
| Use a service credential outside its delegation | The downstream operation is denied. |
Also test mixed-tenant IDs in bulk requests and foreign-tenant references during creation.
Run positive tests alongside denials. An editor should complete permitted work without gaining administrator capabilities. Repeat these tests whenever routes, policies, database roles, or worker paths change.
Build Around One Enforced Tenant Boundary
Reliable tenant authorization starts with verified context and follows it through every resource access. Roles, database policies, and service credentials must respect the same tenant boundary.
Build one complete request path first, including its denial tests. Then apply that pattern to exports, provisioning, background jobs, and internal calls so no alternate path bypasses the boundary.