AWS Amplify CLI, how to automatically add a user to a Cognito User Pool with a Lambda-Trigger
Tags: awsamplify
Published: 2019-04-30 13:47:23

---
title: AWS Amplify CLI, how to automatically add a user to a Cognito User Pool with a Lambda-Trigger
published: true
description: We learn how to use CloudFormation to create Cognito UserPool groups and a Lambda trigger to add a user to a group, when the user signs up
tags: awsamplify
cover_image: https://thepracticaldev.s3.amazonaws.com/i/v7jqlpcnaqqhkqoguou4.jpg
---

# 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](https://aws-amplify.github.io/docs/cli/graphql#auth) 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

```sh
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 :`

```yaml
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"_:

```json
  "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:

```sh
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.

```javascript
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.

```json
{
  "userPoolId": "the-user-pool-id",
  "userName": "user1",
  "userPoolGroupName": "Users"
}
```

To run a test use:

```sh
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`

```json
{
  "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`.

```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:

```json
{
  "Parameters": {
    // ... other parameters
    "AuthCognitoUserPoolArn": {
      "Type": "String"
    }
  }
}
```

Then we add a statement to`lambdaexecutionpolicy.Properties.PolicyDocument.Statement` which references the **"AuthCognitoUserPoolArn"** parameter and allows the action **"cognito-idp:AdminAddUserToGroup"**:

```json
{
  "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:

```yaml
# 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

```sh
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.