DEV Community

Cover image for AWS Amplify CLI, how to automatically add a user to a Cognito User Pool with a Lambda-Trigger
Michael Gustmann
Michael Gustmann

Posted on • Edited on

AWS Amplify CLI, how to automatically add a user to a Cognito User Pool with a Lambda-Trigger

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:

  1. Each user to log in to use our app and grant read access to our API
  2. To have managers, that can additionally create and update a task
  3. 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:

  1. Create all user groups ('Users', 'Managers', 'Admins')
  2. Add a trigger function to automatically add a user to a group
  3. Allow the function to add a user to a group
  4. 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)

Collapse
 
daviddeslauriers profile image
daviddeslauriers

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!

Collapse
 
brothatru profile image
michael trieu

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

Collapse
 
ale_annini profile image
Alessandro Annini

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 the event object in lambda function? How can I dynamically assign the group property?

Thanks!

Collapse
 
beavearony profile image
Michael Gustmann

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.

Collapse
 
johnbwilliams profile image
john williams • Edited

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

Collapse
 
beavearony profile image
Michael Gustmann

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.

Collapse
 
vrebo profile image
Victor Daniel Rebolloso Degante

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?.

Collapse
 
winstonn profile image
Winston Nolan

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!