Jun 30th, 2021

Multi-Tenant Architectures with AWS Cognito

#AWSCognito

#MultiTenantApp

#AWSLambda

A New User Management Approach for Multi-Tenant Applications

Image shows Multi Tenancy Overview

With the growing popularity of cloud-based technologies, the usage of multi-tenant applications is getting popular. 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. Additionally, authentication and authorization mechanisms are important for all multi-tenant architectural designs. In this blog post, we will focus on authentication and authorization mechanisms for multi-tenant architectures with AWS Cognito and we will explore an example scenario for you.

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.

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.

Image shows AWS Cognito Userpool per tenant

b. AWS Cognito Groups for Every Tenant:

Image shows AWS Cognito Groups per tenant

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.

Image shows AWS Cognito Groups

c. AWS Cognito Custom Attribute for Every Tenant:

Image shows AWS Cognito custom attributes per tenant

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. You can handle all multi-tenancy logic in your application and back-end service with this ID and Lambda Authorizer. 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.

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.

Image shows AWS 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 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 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 of your functions. 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:

Image shows Example Architecture for AWS Cognito Multi Tenancy

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:

Image shows AWS Lambda Services URLs for Demo

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

Image shows AWS Cognito Access Token Details

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

Image shows AWS Customer API Example

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 and this is the expected behavior of our PurpleBox API with proper Multi-tenant and Lambda Authorizer logic.

Image shows AWS PurpleBox API Example scenarioj

If you liked this post, share it now!

Our Recent Posts

Introduction to Burp Suite’s Latest Extension DOM-Invader

Learn about the Burp Suite 2021.7 release and the DOM Invader extension features. Explore the n...

Read More

The Ultimate Guide to SQL Injection [AppSec Blog Series Part 4]

Learn about SQL Injection and explore the types of SQLi. Explore real-life SQL Injection attack...

Read More

What Awaits Us with the PCI DSS 4.0 Timeline Release?

Explore the most controversial changes proposed in the PCI DSS V4.0 Timeline Release. Ensure th...

Read More