Multi-Tenant AWS Amplify: Method 2: Cognito Groups

Daniel Dantas (@dantasfiles)
4 min readDec 31, 2019

--

Return to Multi-Tenant AWS Amplify: Overview

This is method 2 of 3 for creating multi-tenant AWS Amplify mobile apps in React Native. In this method, each tenant has a Cognito group associated with it.

The example code for this post uses React Native 61.5 and AWS Amplify 2.2.1, and is at https://github.com/dantasfiles/AmplifyMultiTenant2

We use an AWS Cognito Post Confirmation Lambda Trigger to add the user to an AWS Cognito group associated with the tenant. If the AWS Cognito group does not yet exist, it is created. Recall that a Post Confirmation Lambda Trigger is invoked “after a new user is confirmed, allowing you to… add custom logic.”

Upsides of this method include that it’s simple to implement, that it continues to use the access token, instead of requiring the ID token to be passed to the API (as in Method 1: Cognito Custom Attributes), and that it is less expensive: the lambda function is only invoked once upon confirmation of a user, limiting AWS Lambda costs (as opposed using the Pre Token Generation Lambda Trigger in Method 3: Virtual Cognito Groups).

Downsides of this method include the hard limit of 500 groups, which may limit the future growth of your app.

Brandon Plasters points out that since this post was written, AWS raised the Cognito group limit to 10k. So this downside may no longer exist for most multi-tenant use cases.

The AWS Cognito group associated with the tenant will be included in the user’s access token, and can be used by Dynamic Group Authorization in the API for access control checks.

Instructions

Perform the initial steps as described in Multi-Tenant AWS Amplify: Overview

Add AWS Amplify authentication. Make sure to enable Add User to Group which creates a Post Confirmation Lambda Trigger, and enter any value for the name of the group — the code we write will ignore this value.

> amplify add auth  
...
Do you want to enable any of the following capabilities?
( ) Add Google reCaptcha Challenge
( ) Email Verification Link with Redirect
>(*) Add User to Group
( ) Email Domain Filtering (blacklist)
( ) Email Domain Filtering (whitelist)
( ) Custom Auth Challenge Flow (basic scaffolding - not for production)
( ) Override ID Token Claims
Do you want to enable any of the following capabilities? Add User to Group
? Enter the name of the group to which users will be added. ENTER ANYTHING HERE
Succesfully added the Lambda function locally

Modify amplify\backend\function\amplifymultitenantXXXXPostConfirmation\src\add-to-group.js

// add-to-group.js
/* eslint-disable-line */ const aws = require('aws-sdk');
exports.handler = async (event, context, callback) => {
const cisp = new aws.CognitoIdentityServiceProvider({
apiVersion: '2016-04-18',
});
const tenant = ADD TENANT LOGIC HERE;
const groupParams = {
GroupName: tenant,
UserPoolId: event.userPoolId,
};
const addUserParams = {
GroupName: tenant,
UserPoolId: event.userPoolId,
Username: event.userName,
};
try {
await cisp.getGroup(groupParams).promise();
} catch (e) {
await cisp.createGroup(groupParams).promise();
}
try {
await cisp.adminAddUserToGroup(addUserParams).promise();
callback(null, event);
} catch (e) {
callback(e);
}
};

This function is run after the user is confirmed, and it adds the user to a tenant group, creating the group if it doesn’t yet exist.
Your tenant selection logic can read and use any Post Confirmation Lambda Trigger parameters, including AWS Cognito user attributes, which are passed to the add-to-group.js Lambda function in the event.request.userAttributes parameter.

The access token of a user now contains the required tenant information in its cognito:groups field.

Note: the remaining sections of this post are identical for this Method 2: Cognito Groups and Method 3: Virtual Cognito Groups.

Viewing the tenant

We can extract the tenant information from the cognito:groups field of the access token with the following example code in App.js

// App.js
async function fetchTenant(setTenant) {
// get the access token of the signed in user
const {accessToken} = await Auth.currentSession();
// get the tenant from the top of the cognito groups list
const cognitogroups = accessToken.payload['cognito:groups'];
const tenant = cognitogroups[0];
setTenant(tenant);
}
const App = withAuthenticator(() => {
const [tenant, setTenant] = useState('');
useEffect(() => {
fetchTenant(setTenant);
}, []);
return (
...
<Text style={styles.sectionDescription}>
Your tenant is {tenant}
</Text>
...
);
});

Securing an API

We can now use that tenant information to secure our API.

Add an AWS Amplify API
amplify add api

Edit amplify\backend\api\amplifymultitenant\schema.graphql

type Todo
@model(subscriptions: null)
@auth(rules: [{allow: groups, groupsField: "tenant"}]) {
id: ID!
tenant: ID!
name: String!
description: String
}

First,tenant: ID! stores the tenant associated with the Todo item.

Second, the Dynamic Group Authorization @auth(rules: [{allow: groups, groupsField: "tenant"}]) looks in the tenant field (specified by the groupsField) of the Todo and matches it against the groups in the access token. Recall that in the previous step, we added the tenant information to the groups in the access token.
The @auth rule will only allow a user to add Todos that have the tenant field set to the user’s tenant, and will only allow a user to view Todos that are associated with their tenant.

To see how the above process works, here is a simplified version of the access control code in the resolver that AWS Amplify generates for the getTodo GraphQL operation. It makes sure that a Todo associated with a tenant (stored in $ctx.result.tenant) can only be read by a user associated with that tenant (stored in $ctx.identity.claims.get(“cognito:groups”)).

#foreach( $userGroup in $ctx.identity.claims.get("cognito:groups") )
#if( $ctx.result.tenant == $userGroup )
#set( $isDynamicGroupAuthorized = true )
#end
#end

--

--

Daniel Dantas (@dantasfiles)

I create guides to help me fully understand the issues that I’m encountering and fixing. Web: dantasfiles.com Email: daniel@dantasfiles.com