Multi-Tenant AWS Amplify: Method 2: Cognito Groups
This is Method 2: Cognito Groups for creating multi-tenant AWS Amplify mobile apps in React Native.
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\amplifymultitenant
XXXX
PostConfirmation\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 APIamplify 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