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:
- Node.js installed and npm
- An active AWS account
- Basic knowledge of Node.js and Express.js
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
- Log in to the AWS Management Console and open the CloudFormation service.
- 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 SignUp, confirmSignUp, 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 Medium, Twitter, GitHub
References
- https://docs.aws.amazon.com/fr_fr/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html
- https://github.com/aws/aws-sdk-js-v3/blob/171254fbf7a9d9ea90d02b611962c8a5a2d2150c/clients/client-cognito-identity-provider/README.md
- https://boottechnologies-ci.medium.com/multi-tenancy-architecture-using-aws-cognito-part-2-a1bc468d3812
- https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-cognito-identity-provider/
- https://expressjs.com/