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 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.
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.
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.
b. AWS Cognito Groups for Every 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.
c. AWS Cognito Custom Attribute for Every 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.
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.
- Users login successfully & get a token from AWS Cognito.
- Users send requests to an API service.
- API GW is connected to Lambda Authorizer. Users’ token is sent to Lambda authorizer to verify.
- 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:
- 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;
}
- 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;
}
- 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;
}
- 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:
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:
We get customer’s access token from Hosted UI configuration for customer user. We have decoded access token on jwt.io below:
We send a request to the customer API with the customer’s access token. We should get a response from here:
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.
We hope you enjoyed our article. Check out our Cloud Security and Devops services to stay secure!