Multi-Tenant Authentication with Amazon Cognito

Authentication is only part of the problem in a multi-tenant SaaS application. After identifying the user, every API request must also be tied to the correct tenant and authorized for the actions that user can perform.

Amazon Cognito can handle sign-in and token issuance, but tenant isolation still has to be designed explicitly. The pattern below keeps authentication in Cognito while enforcing tenant boundaries in the application and data layers.

Multi-tenant Cognito architecture

What Cognito should do

A Cognito User Pool manages users and authenticates them. After a successful sign-in, Cognito issues JSON Web Tokens (JWTs), which are signed tokens containing claims about the authenticated identity.

For a multi-tenant application, the important question is: which tenant does this identity belong to?

A practical model is to maintain a tenant identifier such as:

tenant_123

The user record can store that identifier as a Cognito custom attribute such as custom:tenant_id. If the API needs the tenant identifier in the access token, use a supported Cognito token-customization mechanism such as a Pre Token Generation Lambda trigger [SOURCE NEEDED].

The API should derive tenant context from the validated token, not from a tenant ID submitted by the browser.

Separate authentication from authorization

Cognito answers:

Who is this user?

Your application must still answer:

What is this user allowed to do inside this tenant?

Do not treat possession of a valid Cognito token as permission to access every tenant resource.

A useful authorization context might contain:

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "tenant_123",
  "roles": ["admin"]
}

sub is the Cognito user identifier. tenant_id establishes the tenant boundary. roles represents application permissions.

Cognito groups can be useful for coarse roles, but creating groups for every tenant-role combination becomes difficult to manage as tenant count grows. For larger systems, tenant membership and roles can instead be maintained in application data and projected into authorization claims when needed.

Tenant claim and API request flow

A practical implementation pattern

1. Authenticate with one Cognito User Pool

For many SaaS applications, one User Pool can serve multiple tenants. Each user is associated with a tenant identifier.

Separate User Pools can provide stronger administrative separation, but they also add operational overhead.

2. Put tenant context in the authenticated identity

Store or derive a tenant identifier for each user.

Do not rely on this request:

GET /bookings?tenantId=tenant_456

to decide which tenant the caller can access.

Instead, extract the tenant identifier from the validated JWT.

3. Validate the token at the API boundary

API Gateway can validate Cognito-issued JWTs before invoking application logic. The backend then uses the validated claims supplied by the authorizer.

4. Enforce tenant isolation in every data operation

Suppose DynamoDB stores appointments.

A useful key design is:

PK = TENANT#tenant_123
SK = APPOINTMENT#abc123

The backend constructs the partition key from the authenticated tenant claim.

A simplified Lambda handler might contain:

const claims = event.requestContext.authorizer.jwt.claims;
const tenantId = claims["tenant_id"];

if (!tenantId) {
  throw new Error("Missing tenant context");
}

const pk = `TENANT#${tenantId}`;

The client never decides the partition key.

5. Apply role checks separately

Tenant membership answers where the user can operate. Roles answer what the user can do.

For example:

  • member can view appointments.
  • manager can modify schedules.
  • admin can manage staff.

Keep these checks explicit in business logic or a shared authorization layer.

Example: appointment-booking SaaS

Consider a platform used by independent businesses.

Maria works for tenant_123. She signs in through Cognito and receives a token containing her identity, tenant context, and role.

She requests:

GET /appointments

API Gateway validates the token. The Lambda function reads tenant_123 from the authenticated claims and queries only the DynamoDB partition for that tenant.

If Maria changes the browser request and sends tenant_456, the backend ignores it. If the API exposes a route such as /tenants/{tenantId}/appointments, the backend compares the path tenant with the authenticated tenant before accessing data.

That check is the actual tenant boundary.

Common multi-tenant authentication mistakes

Trade-offs and common mistakes

Trusting tenant IDs supplied by the client

A tenant ID in a request parameter is input, not identity. Tenant context must come from a validated authentication source.

Using Cognito groups as the complete authorization model

Groups work well for simple roles, but SaaS permissions may involve tenant-specific membership, resource ownership, or delegated access. Those rules usually belong in an application authorization model.

Securing the API but not the data model

Design DynamoDB keys, SQL predicates, or repository methods around tenant context so isolation is enforced consistently.

Putting too much information in tokens

JWT claims are convenient, but tokens remain valid for a period of time. A role change may therefore not be reflected until a new token is issued. Frequently changing authorization state may need to be checked against application data instead.

Conclusion

Cognito can solve the identity portion of multi-tenant authentication, but tenant isolation remains an application architecture responsibility. Treat the tenant identifier as trusted security context, validate it at the API boundary, and carry it into every data-access decision.

As a next step, trace one API operation from Cognito token to database query and verify that no client-controlled tenant identifier can change the resources it accesses.