Multi-Tenant AWS Amplify: Method 3: Virtual Cognito Groups

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

--

Return to Multi-Tenant AWS Amplify: Overview

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

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

We alter the access token using a Pre Token Generation Lambda Trigger, adding the tenant information as a virtual AWS Cognito group to each access token. Recall that a Pre Token Generation Lambda trigger is invoked before token generation. No actual AWS Cognito groups are created — the tenant information is prepended before token generation to any existing, real AWS Cognito groups in the access token.

Upsides of this method include 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, because the Cognito groups are virtual, not real, it avoids the hard limit of 500 groups, which might have limited the future growth of your app.

Since this post was written, AWS raised the Cognito group limit to 10k. So Method 3 is probably no longer necessary for most multi-tenant use cases.

Downsides include that it is more expensive: the Lambda function is invoked whenever the tokens are generated, increasing AWS Lambda cost (as opposed to using the Post Confirmation Lambda Trigger in Method 2: Cognito Groups)

The virtual 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 Override ID Token Claims

> amplify add auth
...
Do you want to configure advanced settings?
No, I am done.
> Yes, I want to make some additional changes.
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
Successfully added the Lambda function locally

Edit amplify\backend\function\amplifymultitenantXXXXPreTokenGeneration\src\alter-claims.js

// alter-claims.js
exports.handler = async (event, context, callback) => {
// get old groups
const groups = event.request.groupConfiguration.groupsToOverride;
const tenant = ''; // ADD YOUR TENANT SELECTION LOGIC HERE
// add tenant to groups
event.response = {
claimsOverrideDetails: {
groupOverrideDetails: {
groupsToOverride: [tenant, ...groups],
},
},
};
// Return to Amazon Cognito
callback(null, event);
};

This function will be run before access token generation. Your tenant selection logic can read and use any Pre Token Generation Lambda Trigger parameters, including AWS Cognito user attributes, which are passed to the alter-claims.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 Method 2: Cognito Groups and this 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