HowTo: Implement user sign up and login with AWS Cognito
Tags: aws, cognito, userpool
Published: 2020-05-17 02:29:39

AWS supports user management and authentication with Cognito. You can save user information in Cognito securely.

To focus on Cognito itself, this tutorial will not include `express`, `express-session` and `passport`. Also, we are doing server side authentication instead of client side.

Imagine we have a page to ask user to input email address and password to sign up. No email verification is required.

After user signed up, they can login with the same credential. We don’t ask them to change password on 1st login (because the password is provided by them).

# Setup Cognito

First we need to create a User Pool which stores our users. We will name it `MyUserPool`.

```bash
export USER_POOL_ID=$(aws cognito-idp create-user-pool \
  --pool-name MyUserPool \
  --query UserPool.Id \
  --output text)
```

Next, we need to create a client which can access our User pool. We will name it `MyUserPoolClient`. We also specify `explicit-auth-flows` as `ADMIN_NO_SRP_AUTH`, so we can simply provide username and password to do authentication.

```bash
export CLIENT_ID=$(aws cognito-idp create-user-pool-client \
  --user-pool-id $USER_POOL_ID \
  --client-name MyUserPoolClient \
  --explicit-auth-flows ADMIN_NO_SRP_AUTH \
  --query UserPoolClient.ClientId \
  --output text)
```

# Setup IAM

We need to create an IAM user for our server to access Cognito. We name this user as `MyCognitoIAMUser` and attach managed policy `AmazonCognitoPowerUser` to it. You should create **IAM role** instead of IAM user if you run on AWS EC2 or Lambda.

```bash
IAM_USER_NAME=MyCognitoIAMUser

aws iam create-user --user-name $IAM_USER_NAME

aws iam  attach-user-policy \
  --user-name $IAM_USER_NAME \
  --policy-arn arn:aws:iam::aws:policy/AmazonCognitoPowerUser

CREDENTIALS=$(aws iam create-access-key --user-name $IAM_USER_NAME \
  --query 'AccessKey.[AccessKeyId,SecretAccessKey]' \
  --output text)

export ACCESS_KEY_ID=$(echo $CREDENTIALS | awk '{print $1}')

export SECRET_ACCESS_KEY=$(echo $CREDENTIALS | awk '{print $2}')
```

# Setup AWS client

We need to install `aws-sdk` to call Cognito API.

```bash
npm i aws-sdk
```

So now we can start coding. First we need to setup our AWS client first.

```javascript
const AWS = require('aws-sdk')
// You don't need it if you are running on EC2 or Lambda
if (process.env.NODE_ENV === 'development') {
  AWS.config.update({
    region: process.env['AWS_REGION'],
    accessKeyId: process.env['ACCESS_KEY_ID'],
    secretAccessKey: process.env['SECRET_ACCESS_KEY']
  })
}
```

# Sign up

We first create our `signUp` function first.

```javascript
async function signUp(email, password) {
  try {
    const cognito = new AWS.CognitoIdentityServiceProvider()
    await cognito.adminCreateUser({
      UserPoolId: process.env.USER_POOL_ID,
      Username: email,
      MessageAction: 'SUPPRESS',
      TemporaryPassword: password,
    }).promise()
  } catch (err) {
    throw err
  }
}
```

As you can see, the password is passed as a temporary password. It means user needs to change on 1st login. But we don’t want to do this because the password is already provided by the user. We need to programmatically to change the password for them.

This is how our final version of `signUp` function looks like.

```javascript
async function signUp(email, password) {
  try {
    const cognito = new AWS.CognitoIdentityServiceProvider()
    await cognito.adminCreateUser({
      UserPoolId: process.env.USER_POOL_ID,
      Username: email,
      MessageAction: 'SUPPRESS',
      TemporaryPassword: password,
    }).promise()

    const initAuthResponse = await cognito.adminInitiateAuth({
      AuthFlow: 'ADMIN_NO_SRP_AUTH',
      ClientId: process.env.CLIENT_ID,
      UserPoolId: process.env.USER_POOL_ID,
      AuthParameters: {
        USERNAME: email,
        PASSWORD: password
      }
    }).promise()

    if (initAuthResponse.ChallengeName === 'NEW_PASSWORD_REQUIRED') {
      await cognito.adminRespondToAuthChallenge({
        ChallengeName: 'NEW_PASSWORD_REQUIRED',
        ClientId: process.env.CLIENT_ID,
        UserPoolId: process.env.USER_POOL_ID,
        ChallengeResponses: {
          USERNAME: email,
          NEW_PASSWORD: password,
        },
        Session: initAuthResponse.Session
      }).promise()
    }
  } catch (err) {
    throw err
  }
}
```

# Login

Login is much simpler. Our function will return the response from AWS. You can get the access token, ID token, refresh token from the response.

```javascript
async function login(email, password) {
  try {
    const cognito = new AWS.CognitoIdentityServiceProvider()
    return await cognito.adminInitiateAuth({
      AuthFlow: 'ADMIN_NO_SRP_AUTH',
      ClientId: process.env.CLIENT_ID,
      UserPoolId: process.env.USER_POOL_ID,
      AuthParameters: {
        USERNAME: email,
        PASSWORD: password
      }
    }).promise()
  } catch (err) {
    throw err
  }
}
```

You can check the schema of the response in [here](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminInitiateAuth.html).

```javascript
{
   "AuthenticationResult": { 
      "AccessToken": "string",
      "ExpiresIn": number,
      "IdToken": "string",
      "NewDeviceMetadata": { 
         "DeviceGroupKey": "string",
         "DeviceKey": "string"
      },
      "RefreshToken": "string",
      "TokenType": "string"
   },
   "ChallengeName": "string",
   "ChallengeParameters": { 
      "string" : "string" 
   },
   "Session": "string"
}
```