Today, we will explore authentication and authorization mechanisms for multi-tenant architectures with AWS Cognito and an example scenario. As cloud-based technologies become increasingly popular, so too does the usage of multi-tenant applications.

However, ensuring that each tenant’s data remains isolated and invisible to other tenants is critical, as is implementing proper authentication and authorization mechanisms.

We delve into several approaches for building multi-tenant applications with AWS Cognito, including user pools, groups, and custom attributes. Additionally, we discuss the Lambda Authorizer, a powerful tool for enforcing multi-tenant security controls and enabling centralized authentication and authorization logic.

A New User Management Approach for Multi-Tenant Applications

With the growing popularity of cloud-based technologies, the usage of multi-tenant applications is getting popular. Multi-tenancy means a single resource such as a DynamoDB table or an S3 bucket object serves multiple customers.

Each customer should access only their own data layers. Each tenant’s data should be isolated and remain invisible to other tenants. Additionally, authentication and authorization mechanisms are important for all multi-tenant architectural designs.

AWS Cognito

What is Multi-Tenancy?

Multi-tenancy means that a single resource such as a DynamoDB table or an S3 bucket object serves multiple customers. Each customer should access only their own data layers. Each tenant’s data should be isolated and remain invisible to other tenants.

Also see: Introduction to AWS Serverless Application Model

Building Multi-Tenant Applications with AWS Cognito

There are different approaches for building multi-tenant applications with AWS Cognito.

a. AWS Cognito User Pool for Every Tenant:

In this approach, we create a user pool for each tenant. This architecture provides maximum isolation for each tenant. Also, it allows us to implement different user pool-based configurations for each tenant such as password policy. In addition, the development of this approach is costly and the operation requires a lot of effort. We need to add logic to our application that allows users to sign up and sign in to their corresponding tenant’s user pool.

Decoded access token

b. AWS Cognito Groups for Every Tenant:

AWS Cognito Groups

With group-based multi-tenancy, you can associate an Amazon Cognito user pool group with a tenant. You can handle multi-tenancy logic with Lambda Authorizer in your application and back-end services. You can give AWS Cognito Group control to the Lambda Authorizer function.

We will explain what a Lambda Authorizer is in the next chapters.

AWS Cognito Groups

c. AWS Cognito Custom Attribute for Every Tenant:

AWS Cognito Custom Attribute

With a custom attribute-based multi-tenancy approach, you can generate and add an ID for every user profile as a custom attribute. Custom attributes are useful when you want to add additional user data to AWS Cognito User Pool. You can use lambda triggers for adding custom attributes in the registration/login process. With this ID and Lambda Authorizer, you can handle all multi-tenancy logic in your application and back-end service. This approach allows you to use a unified sign-up and sign-in experience for all users. You can also identify the user’s tenant in your application by checking this custom attribute.

This article may interest you: What is Cloud Security?

Multi-Tenancy Solution: Lambda Authorizer

A Lambda Authorizer or custom authorizer is an API Gateway feature that provides an access control mechanism for your API services. It’s basically a Lambda function that you can implement a custom authorization scheme that uses a token authentication strategy. When we think of multi-tenant applications, we can easily say that such a structure will meet the tenant conditions and we can create logic for accessing resources.

Lambda Authorizer

What is Lambda Authorizer?

A Lambda Authorizer or custom authorizer is an API Gateway feature that provides an access control mechanism for your API services.

  1. Users login successfully & get a token from AWS Cognito.
  2. Users send requests to an API service.
  3. API GW is connected to Lambda Authorizer. Users’ token is sent to the Lambda authorizer to verify.
  4. Lambda Authorizer has AWS Cognito User Pool checks. Lambda Authorizer verifies the users’ tokens. Users send requests with a token and get responses successfully from the API.

One of the biggest advantages of using a custom authorizer is centralizing your authentication/authorization logic in a single function rather than implementing that logic in each function. If your authorization/authentication mechanism changes or different cases add to your logic, you can simply redeploy Lambda Authorizer rather than redeploying each function.

While deploying a Lambda Authorizer, we need to verify and check some points before implementing our multi-tenancy logic:

  1. Confirm if the token is JWT or not:
//Fail if the token is not jwt
var decodedJwt = jwt.decode(token, { complete: true });
if (!decodedJwt) {
  console.log(Not a valid JWT token);
  context.fail(Unauthorized);
  return;
}
  1. Confirm JWT token is from your User Pool or not:
//Fail if token is not from your UserPool
if (decodedJwt.payload.iss != iss) {
  console.log(invalid issuer);
  context.fail(Unauthorized);
  return;
}
  1. A Confirm JWT token is an access token/id token:
//Reject the jwt if it's not an 'Access Token'
if (decodedJwt.payload.token_use != access) {
  console.log(Not an access token);
  context.fail(Unauthorized);
  return;
}
  1. Lambda Authorizer has AWS Cognito User Pool checks. Lambda Authorizer verifies the users’ tokens. Verify the signature of the JWT token to ensure it’s coming from your User Pool. This verification provides JWT confidentiality and integrity:
jwt.verify(token, pem, { issuer: iss }, function(err, payload) {
      if(err) {
        context.fail(Unauthorized);
      } else {
        //Valid token. Generate the API Gateway policy for the user
        //Always generate the policy on value of 'sub' claim and not for 'username' because username is reassignable
        //sub is UUID for a user which is never reassigned to another user.
        var principalId = payload.sub;
      }

After these checkpoints, we can implement our multi-tenancy logic and additional controls for our desired architecture. The rest of all controls are related to our business logic and our coding ability.

Example Scenario: AWS Cognito Groups for Every Tenant

Let’s assume we have an application that keeps records of contracts made between PurpleBox and customers. While each customer can only access their contracts with us, we, as PurpleBox, want to access the contracts we have made with all our customers. We should implement a multi-tenant application for this application. For this, we have designed the architecture below:

Example Scenario: AWS Cognito Groups

We have two types of API services for this scenario: PurpleBox Admin APIs and Customer APIs. As we have explained earlier, customers should not be able to authorize PurpleBox APIs. We have listed two API URLs below:

PurpleBox APIs

We get the customer’s access token from Hosted UI configuration for the customer user. We have decoded the access token on jwt.io below:

Access token

We send a request to the customer API with the customer’s access token. We should get a response from here:

customer API

After that, we send a request to the PurpleBox API with a customer’s access token. We should not get any responses from this API. We get “User is not authorized to access this resource.” error message, which is the expected behavior of our PurpleBox API with proper Multi-tenant and Lambda Authorizer logic.

customer API

Conclusion

Multi-tenant architectures require careful consideration of authentication and authorization mechanisms. AWS Cognito provides several options to handle multi-tenancy, including:

  • User pools,
  • Groups,
  • Custom attributes.

Each approach has its advantages and disadvantages, depending on the specific needs of your application. A Lambda Authorizer is a powerful tool that can help enforce multi-tenant security controls and enable centralized authentication and authorization logic.

At PurpleBox, we specialize in Cloud Security and DevOps, making sure our clients’ applications are secure and scalable. Visit our Services page to learn how we can help your organization stay secure in the cloud.