Handling Cognito-UserPool-Groups in our AWS Amplify CLI project
AWS Amplify Transform provides an @auth
directive. With it we have the possibility to grant access to certain parts of our API based on either static or dynamic groups. See the documentation for details.
Say we have task api and we want:
- Each user to log in to use our app and grant read access to our API
- To have managers, that can additionally create and update a task
- To have admins, that can do everything
We will not cover the API nor the @auth
directive, but tackle the problem, that upon Sign-Up, we expect all users to be in a group named 'Users'. Managers and Admins will be added to the groups by hand in the AWS Management Console, but each user will at least belong to the 'Users' group.
We want our code to handle the infrastructure as much as possible without leaving the Amplify CLI.
Let's see how to automate things in each o our environment stacks:
- Create all user groups ('Users', 'Managers', 'Admins')
- Add a trigger function to automatically add a user to a group
- Allow the function to add a user to a group
- Configure a trigger in the Cognito UserPool to run the function after Sign-Up
Custom CloudFormation templates
When adding or modifying categories with the Amplify CLI, some templates are not safe and will be overwritten. We can safely add custom resources to the amplify/backend/api/${api-name}/stacks/CustomResources.json
file or add other json file to this directory. These stacks are part of the api category, but could be used to create anything that CloudFormation supports. The emphasis is on create! We can reference other parts of our backend templates, but we cannot change them (at least to our knowledge).
We need to change amplify/backend/auth/cognitoxxxx/cognitoxxxx-cloudformation-template.yml
, which will get overwritten by operations like
amplify update auth
amplify add analytics
So, we need to be prepared to redo our changes multiple times. Checking in our code in source control will help us with that though.
Generate Groups
We want three groups to be added to our UserPools.
- Admins
- Managers
- Users
If we don't care to redo the changes, when Amplify CLI updates this file, we can edit the 'auth' CloudFormation template.
In amplify/backend/auth/cognitoxxxx/cognitoxxxx-cloudformation-template.yml
add at the end just before Outputs :
Resources:
## Other resources ...
## ...
UserPoolGroupAdmins:
Type: 'AWS::Cognito::UserPoolGroup'
Properties:
GroupName: Admins
UserPoolId: !Ref UserPool
UserPoolGroupManagers:
Type: 'AWS::Cognito::UserPoolGroup'
Properties:
GroupName: Managers
UserPoolId: !Ref UserPool
UserPoolGroupUsers:
Type: 'AWS::Cognito::UserPoolGroup'
Properties:
GroupName: Users
UserPoolId: !Ref UserPool
Outputs:
Otherwise we can go the safe route and use the amplify/backend/api/${api-name}/stacks/CustomResources.json
file to create these groups.
Add the following to the "Resources" section, right after "EmptyResource":
"Resources": {
"EmptyResource": {
"Type": "Custom::EmptyResource",
"Condition": "AlwaysFalse"
},
"UserPoolGroupAdmins": {
"Type": "AWS::Cognito::UserPoolGroup",
"Properties": {
"GroupName": "Admins",
"UserPoolId": {
"Ref": "AuthCognitoUserPoolId"
}
}
},
"UserPoolGroupManagers": {
"Type": "AWS::Cognito::UserPoolGroup",
"Properties": {
"GroupName": "Managers",
"UserPoolId": {
"Ref": "AuthCognitoUserPoolId"
}
}
},
"UserPoolGroupUsers": {
"Type": "AWS::Cognito::UserPoolGroup",
"Properties": {
"GroupName": "Users",
"UserPoolId": {
"Ref": "AuthCognitoUserPoolId"
}
}
}
}
The downside to this is, that it will show up as if the api category changed and not the auth category.
Automatically add a User to a Cognito User Pool Group with a Lambda-Trigger on Sign-Up
Now that we automatically create Cognito-UserPool-Groups, we want to assign users to one of those groups upon Sign-Up.
Cognito offers triggers during certain life-cycle events. One of these triggers is 'PostConfirmation', which may run a Lambda function after a user was successfully confirmed.
Add a new Lambda Function
The AWS Amplify CLI offers the ability to add a function to our project:
amplify add function
Choose the name 'addUserToGroup', when asked for the friendly name and Lambda function name and as a template we want Hello world function.
Change the file index.js
of our newly created function.
exports.handler = function afterConfirmationTrigger(event, context, callback) {
const AWS = require('aws-sdk');
const cognitoISP = new AWS.CognitoIdentityServiceProvider({
apiVersion: '2016-04-18'
});
const params = {
GroupName: 'Users',
UserPoolId: event.userPoolId,
Username: event.userName
};
cognitoISP
.adminAddUserToGroup(params)
.promise()
.then(() => callback(null, event))
.catch(err => callback(err, event));
};
In event.json
we can change the payload content to our liking to test the function.
{
"userPoolId": "the-user-pool-id",
"userName": "user1",
"userPoolGroupName": "Users"
}
To run a test use:
amplify invoke function addUserToGroup
This will not work yet. Since we need to give the function the permission to do what we want it to do.
Add IAM Policy to allow adding user to group
To allow our new function to actually add a user to a group, it needs a policy attached. This time we need to edit the CloudFormation template of the Lambda function. This should be pretty safe, since other categories usually don't have side effects on this template.
We need to specify the resource ARN of our UserPool target. We can resort to using a wildcard character or use parameters to get the exact ARN of our UserPool.
Without parameters
We edit the file amplify/backend/function/addUserToGroup/addUserToGroup-cloudformation-template.json
and add to the array lambdaexecutionpolicy.Properties.PolicyDocument.Statement
{
"Effect": "Allow",
"Action": ["cognito-idp:AdminAddUserToGroup"],
"Resource": {
"Fn::Sub": [
"arn:aws:cognito-idp:${region}:${account}:userpool/${region}*",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
}
}
]
}
}
This is simple and in one place but will give the function the permission to access all UserPools in our region.
With parameters and least privileges
To target a single UserPool istead, we need to know its ARN. To get access, we create a parameter and pass it to our CloudFormation template.
We create a new file amplify/backend/function/addUserToGroup/parameters.json
and add the ARN of the UserPool to it. We can take a look at the authcognitoname in amplify/backend/api/${api-name}/parameters.json
.
{
"AuthCognitoUserPoolArn": {
"Fn::Sub": [
"arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${id}",
{
"id": {
"Fn::GetAtt": ["--authcognitoname--", "Outputs.UserPoolId"]
}
}
]
}
}
To pass the parameters to the CloudFormation template, we need to edit amplify/backend/function/addUserToGroup/addUserToGroup-cloudformation-template.json
.
First, we have to define the parameter. We do this by adding the following to the "Parameters" array:
{
"Parameters": {
// ... other parameters
"AuthCognitoUserPoolArn": {
"Type": "String"
}
}
}
Then we add a statement tolambdaexecutionpolicy.Properties.PolicyDocument.Statement
which references the "AuthCognitoUserPoolArn" parameter and allows the action "cognito-idp:AdminAddUserToGroup":
{
"Effect": "Allow",
"Action": ["cognito-idp:AdminAddUserToGroup"],
"Resource": { "Ref": "AuthCognitoUserPoolArn" }
}
We can run amplify push
to deploy the function and check if the policy was added successfully by going to the Lambda Management Console and find our function. If we click on the 'Amazon Cognito User Pools' box, we can inspect the policy below it.
Add Lambda PostConfirmation-Trigger
To assign the Lambda function to a Cognito-UserPool-Trigger, we need to edit the 'auth' CloudFormation template.
This might get overwritten on certain Amplify CLI operations, but we don't see another choice.
In amplify/backend/auth/cognitoxxxx/cognitoxxxx-cloudformation-template.yml
we add our trigger somewhere in that section:
# BEGIN USER POOL RESOURCES
UserPool:
# Created upon user selection
# Depends on SNS Role for Arn if MFA is enabled
Type: AWS::Cognito::UserPool
UpdateReplacePolicy: Retain
Properties:
# Other properties ...
# Add this:
LambdaConfig:
PostConfirmation: !Sub
- arn:aws:lambda:${region}:${account}:function:addUserToGroup-${env}
- {
region: !Ref 'AWS::Region',
account: !Ref 'AWS::AccountId',
env: !Ref env,
}
To deploy the changes to our UserPool we run
amplify push
Checking our work
When everything worked, we can sign in to our AWS Management Console and check:
- Cognito UserPool should have created our three groups
- There is a PostConfirmation trigger pointing to our addUserToGroup Lambda function
- The Lambda function was created in our environment
- The function has the right policy attached, which allows it to add a user to a group in our UserPool
To see it in action, we sign up a new user in our Amplify CLI app of our choice and watch how this new user is automatically added to the 'Users' Cognito-UserPool-Group.
Recap
We created UserPool-Groups in CustomResources.json of the api category, added a Lambda function, attached a policy to it to access the UserPool and edited our Cognito CloudFormation template to define a PostConfirmation Trigger that executes our new Lambda function. Now each time a user signs up it will be assigned to a group.
As the development of Amplify CLI continues we should see help in this area. So this procedure might get irrelivant in a hopefully short while.
Top comments (8)
Hi Michael, I tried implementing your instructions but I keep getting this when I confirm the user...
PostConfirmation invocation failed due to error AccessDeniedException.
Should I be seeing anything under "Resource-based policy" in the Lambda function?
Thank you!
Thanks for taking the time to write this!
Your article got me 90% of the way.
After following your steps, my cognito stack didn't have permissions to invoke the lambda function.
I had to add permissions to my cloudformation template using this example from stackoverflow ~ stackoverflow.com/a/42460847/4364074
Hi Michael, thanks for the useful article!
But what if I added something like
userPoolGroupName
to cognito custom attributes and I want to read it from theevent
object in lambda function? How can I dynamically assign the group property?Thanks!
Using Cognito Lambda Triggers got a lot easier with recent releases. See this post for examples:
aws.amazon.com/en/blogs/mobile/amp...
You can also call other functions from the aws-sdk inside the lambda to get your desired information. adminAddUserToGroup is only one of many function you could use.
Great article - valuable resource for creating Cognito trigger functions
From the Recap... instead of "edit[ing] our Cognito CloudFormation template to define a PostConfirmation Trigger", can the post confirmation trigger function be selected/specified in the console of an existing Cognito user pool?
Similar with user groups in existing Cognito user pool console
Sorry for the late reply.
Yes, the trigger and the groups can very easily be specified or created in the AWS Management Console. The goal for me is not to do any thing manually like this, because if someone in the team spins up a new environment, each step has to be repeated in the console, documented and might be forgotten.
Hi Michael, thanks for the useful article!
I'm trying the second approach (CustomResources.json based) to generate the user groups but i'm having throubles with the references to the pool id ("Ref": "AuthCognitoUserPoolId").
I just need to create the groups in custom resources file, Which step i'm missing of the setup?.
Hey Michael, I just wanted to say thank you for this howto - really very helpful and I got it working thanks to you :) All the best mate!