AWS Cognito auth server with AWS SDK for JavaScript (v3) using Node.js, Express.js

In this post, we’ll learn how to build authentication Rest API using Node.js, Express.js, and AWS Cognito.

· Prerequisites
· Overview
∘ What is AWS CloudFormation?
∘ What Is Amazon Cognito?
· Amazon Cognito Setup
· Setting Up the NodeJs project
∘ Install Express and other dependencies
∘ Define endpoints
· Test the API
· Conclusion
· References

Prerequisites

This is the list of all the prerequisites:

Overview

In this story, we’ll be using two AWS services: AWS CloudFormation and Amazon Cognito.

What is AWS CloudFormation?

AWS CloudFormation is an infrastructure as code (IaC) service that helps you model and set up your AWS resources so that you can spend less time managing those resources and more time focusing on your applications that run in AWS.

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html

What Is Amazon Cognito?

Amazon Cognito provides authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through a third party such as Facebook, Amazon, Google or Apple.

The two main components of Amazon Cognito are user pools and identity pools. User pools are user directories that provide sign-up and sign-in options for your app users. Identity pools enable you to grant your users access to other AWS services. You can use identity pools and user pools separately or together.

— https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html

Amazon Cognito Setup

We’re going to use the AWS CloudFormation template to create our AWS Cognito stack. There are three ways to create a CloudFormation template that contains configuration information about the AWS resources to include in the stack.

  • Upload your own template
  • Using a sample template that is provided by AWS
  • Create a temple in the Designer

We chose to create the model from scratch for the case. Below is the complete template.

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
# Domain cannot contain reserved word: "cognito". Error Code: InvalidParameterException;
  CognitoDomain:
    Type: String
    MinLength: 3
    MaxLength: 63
    AllowedPattern: ^[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?$
    Description: Enter a string. Must be alpha numeric 3-63 in length.

Resources:
# Creates a user pool in cognito for api
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AccountRecoverySetting:
        RecoveryMechanisms:
          - Name: verified_email
            Priority: 1    
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false      
      UsernameConfiguration: 
        CaseSensitive: false
      AutoVerifiedAttributes:
        - email
      UserPoolName: !Sub ${CognitoDomain}-user-pool
      MfaConfiguration: "OFF"  
      DeviceConfiguration:
        ChallengeRequiredOnNewDevice: true
        DeviceOnlyRememberedOnUserPrompt: false
      EmailVerificationMessage: The verification code to your new account is {####}
      EmailVerificationSubject: Verify your new account          
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          TemporaryPasswordValidityDays: 7      
      Schema:
        - Name: name
          AttributeDataType: String
          Mutable: true
          Required: true
        - Name: email
          AttributeDataType: String
          Mutable: false
          Required: true
        - Name: phone_number
          AttributeDataType: String
          Mutable: false
          Required: true

  AdminRoleGroup:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      GroupName: ROLE_ADMIN
      Description: A Admin role group
      Precedence: 1
      UserPoolId:
        Ref: UserPool

  UserRoleGroup:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      GroupName: ROLE_USER
      Description: A User role group
      Precedence: 2
      UserPoolId:
        Ref: UserPool

  GuestRoleGroup:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      GroupName: ROLE_GUEST
      Description: A Guest role group
      Precedence: 3
      UserPoolId:
        Ref: UserPool   

# Creates a User Pool Client to be used by the identity pool
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: UserPoolApi
      RefreshTokenValidity: 7
      GenerateSecret: false
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_ADMIN_USER_PASSWORD_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH  
      SupportedIdentityProviders:
        - COGNITO     
    

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Ref CognitoDomain
      UserPoolId: !Ref UserPool
      

Outputs:
  CognitoUserPoolID:
    Value: !Ref UserPool
    Description: ID of the Cognito User Pool
  CognitoAppClientID:
    Value: !Ref UserPoolClient
    Description: The app client
  1. Log in to the AWS Management Console and open the CloudFormation service.
  2. Then you press the “create stack” button and specify the template

Specify the stack name and cognito Domain name

The next wizard steps are “Configure Stack Options” and “Review”.

The CloudFormation template is deployed, as you can see in the stack events tab, our stack has been created successfully.

Amazon Cognito user pool is ready.

We are done with the AWS Cognito User Pool setup 👨🏼‍💻

Setting Up the NodeJs project

To set up a Node.js app with an Express.js server, we’ll first create a directory for our project to reside in:

mkdir node-cognito-auth-api && cd node-cognito-auth-api

Then, let’s create the package JSON file with the npm initcommand.

Install Express and other dependencies

Now we need to install all the dependencies needed to run our API.

npm install --save express
npm install @aws-sdk/client-cognito-identity-provider
npm install dotenv
npm install dotenv
npm install body-parser
npm install swagger-ui-express swagger-jsdoc

package.json

{
"name": "cognito-auth-api",
"version": "1.0.0",
"description": "AWS cognito auth server with AWS SDK for JavaScript (v3)",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/anicetkeric/node-cognito-auth-api.git"
},
"keywords": [
"jwt",
"nodejs",
"authentication",
"auth-server",
"aws-sdk",
"aws",
"amazon-cognito",
"expressjs"
],
"author": "aek",
"license": "ISC",
"bugs": {
"url": "https://github.com/anicetkeric/node-cognito-auth-api/issues"
},
"homepage": "https://github.com/anicetkeric/node-cognito-auth-api#readme",
"dependencies": {
"@aws-sdk/client-cognito-identity-provider": "^3.370.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"jwt-decode": "^3.1.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

Define endpoints

We will use the Node.JS AWS SDK for the SignUpconfirmSignUp, and SignIn endpoints.

The AWS SDK is modulized by clients and commands. CognitoIdentityProviderClient is used to send the request with a command with input parameters.

First, we need to Initiate the client with configuration (credentials, region). We will use the environment variable from the .env file in Node.JS.

require("dotenv").config();
const { CognitoIdentityProviderClient } = require('@aws-sdk/client-cognito-identity-provider');


const poolData = {
    userPoolId: process.env.AWS_COGNITO_USER_POOL_ID, // App pool Id 
    appClientId: process.env.AWS_COGNITO_CLIENT_ID, // The ID of the client associated with the user pool.
    appClientSecret: process.env.AWS_COGNITO_CLIENT_SECRET // App client Secret 
};

const accessKeyMetadata = {
    accessKeyId: process.env.AWS_ACCESS_KEY, // access key id 
    secretAccessKey: process.env.AWS_SECRET_KEY, // secret access key
    region: process.env.AWS_COGNITO_REGION // AWS region
};

const cognitoClient = new CognitoIdentityProviderClient({
    credentials: {
      accessKeyId: accessKeyMetadata.accessKeyId,
      secretAccessKey: accessKeyMetadata.secretAccessKey,
    },
    forcePathStyle: false,
    region: accessKeyMetadata.region,
});

module.exports = { cognitoClient, poolData};

sign-up.js: contain signUp and confirmSignup methods.

const { SignUpCommand, ConfirmSignUpCommand, AdminAddUserToGroupCommand } = require('@aws-sdk/client-cognito-identity-provider');
const { poolData, cognitoClient } = require('./config');
const crypto = require('crypto');
const hasher = crypto.createHmac('SHA256', poolData.appClientSecret)
var groups = ["ROLE_ADMIN", "ROLE_USER", "ROLE_GUEST"];


async function signUp(password, email, name, phoneNumber, roles) {

  hasher.update(`${email}${poolData.appClientId}`)

  const command = new SignUpCommand({
    ClientId: poolData.appClientId,
    Username: email,
    Password: password,
    SecretHash: hasher.digest('base64'),
    UserAttributes: [{ Name: "email", Value: email }, { Name: "name", Value: name }, { Name: "phone_number", Value: phoneNumber }],
  });

  const cognitoUser = await cognitoClient.send(command);

  console.log(`user signUp result : ${JSON.stringify(cognitoUser)} `);

  // add group to user
  roles.forEach(role => {
    if (groups.includes(role)) {
      addGroup(email, role);
    }
  });


  return {
    "message": "User created successfully",
    "data": cognitoUser.CodeDeliveryDetails
  };
}

// Adds the specified user to the specified group.
async function addGroup(email, groupName) {

  const command = new AdminAddUserToGroupCommand({
    UserPoolId: poolData.userPoolId,
    Username: email,
    GroupName: groupName
  });

  const cognitoUser = await cognitoClient.send(command);

  console.log(`add group to user result : ${JSON.stringify(cognitoUser)} `);

  return cognitoUser;

}

async function confirmSignup(email, code) {

  hasher.update(`${email}${poolData.appClientId}`)

  const command = new ConfirmSignUpCommand({
    ClientId: poolData.appClientId,
    ConfirmationCode: code,
    Username: email,
    SecretHash: hasher.digest('base64')
  });

  const response = await cognitoClient.send(command);

  console.log(`user confirm signUp result : ${JSON.stringify(response)} `);

  return {
    "message": "Confirmed user account",
    "data": response
  };
}

module.exports = {
  signUp, confirmSignup
}

initiate-auth.js: contain signIn and refreshToken methods

const { AuthFlowType, InitiateAuthCommand, RevokeTokenCommand } = require('@aws-sdk/client-cognito-identity-provider');
const { poolData, cognitoClient } = require('./config');
const crypto = require('crypto');
const hasher = crypto.createHmac('SHA256', poolData.appClientSecret)


async function initiateAuth(username, password) {

    const command = new InitiateAuthCommand({
        AuthFlow: AuthFlowType.USER_PASSWORD_AUTH,
        ClientId: poolData.appClientId,
        UserPoolId: poolData.userPoolId,
        AuthParameters: {
            USERNAME: username,
            PASSWORD: password,
            SECRET_HASH: getSecretHash(username),
        }
    });

    const cognitoUser = await cognitoClient.send(command);

    console.log(` cognitoUser result : ${JSON.stringify(cognitoUser)} `);

    return getAuthenticationToken(cognitoUser);
}

async function refreshToken(sub, requestRefreshToken) {

    const command = new InitiateAuthCommand({
        AuthFlow: AuthFlowType.REFRESH_TOKEN_AUTH,
        ClientId: poolData.appClientId,
        UserPoolId: poolData.userPoolId,
        AuthParameters: {
            REFRESH_TOKEN: requestRefreshToken,
            SECRET_HASH: getSecretHash(sub),
        }
    });

    const cognitoUser = await cognitoClient.send(command);

    console.log(`Refresh token result : ${JSON.stringify(cognitoUser)} `);

    return getAuthenticationToken(cognitoUser);
}

async function signOut(token) {

    const command = new RevokeTokenCommand({
        Token: token,
        ClientId: poolData.appClientId,
        ClientSecret: poolData.appClientSecret
    });

    const response = await cognitoClient.send(command);

    console.log(`Revoke token result : ${JSON.stringify(response)} `);
    return {
        "message": "Token deleted",
        "data": response
    };
}


function getAuthenticationToken(cognitoUser) {

    // extract tokens
    const accessToken = cognitoUser.AuthenticationResult.AccessToken;
    const refreshToken = cognitoUser.AuthenticationResult.RefreshToken;
    const idToken = cognitoUser.AuthenticationResult.IdToken;
    const expiresIn = cognitoUser.AuthenticationResult.ExpiresIn;

    return {
        "accessToken": accessToken,
        "refreshToken": refreshToken,
        "idToken": idToken,
        "expiresIn": expiresIn
    };
}

function getSecretHash(username) {
    hasher.update(`${username}${poolData.appClientId}`)
    return hasher.digest('base64');
}

module.exports = {
    initiateAuth, refreshToken, signOut
}

Test the API

Now we can run our API and test it.

npm run start

http://localhost:3080/api-docs/

Account Registration

Amazon Cognito sends a user account verification e-mail or SMS to confirm the user’s registration. In our case, the user received a verification code by e-mail.

Then you need to verify the email using /confirm-sign-up endpoint

The user account is verified as shown below in the screenshot 👏

Now that the user account has been created and verified, let’s try authenticating with the /login endpoint. It returns a jwt access token, a refresh token, and an identification token.

Conclusion

Well done !!. In this post, We have seen how to build authentication Rest API using Node.js, Express.js, and AWS Cognito.

The complete source code is available on GitHub.

You can reach out to me and follow me on MediumTwitterGitHub

References

👉 Link to Medium blog

Related Posts