Basic Authentication with AWS Cognito in Node.js using AWS SDK v3

Basic Authentication with AWS Cognito in Node.js using AWS SDK v3

Table of Contents

  1. Introduction

  2. What is AWS Cognito?

  3. Prerequisites

  4. Step 1: Setting Up an AWS Cognito User Pool

  5. Step 2: Using AWS SDK for JavaScript v3 in Node.js

  6. Step 3: Project Setup

  7. Step 4: Sign-Up, Confirm, Sign-In, Resend Code, and Password Reset

    • 1. Sign-Up User

    • 2. Confirm User Sign-Up

    • 3. Sign-In User

    • 4. Resend Confirmation Code

    • 5. Password Reset (Forgot Password)

  8. Step 5: Shared Cognito Utilities and Error Handling

    • 1. Shared Utilities (cognitoUtil.js)

    • 2. Error Handling (cognitoErrors.js)

  9. Conclusion


Introduction

Authentication is a critical aspect of any application that requires user interaction. AWS Cognito provides scalable and secure user authentication and access control. By integrating AWS Cognito with Node.js applications, we can handle user sign-up, sign-in, and password resets efficiently. AWS SDK for JavaScript v3 offers a modular approach, allowing developers to integrate AWS services with reduced overhead.

In this article, we’ll go through the process of integrating AWS Cognito into a Node.js application, with a complete authentication flow including user registration, email confirmation, sign-in, password reset, and resending validation codes.


What is AWS Cognito?

AWS Cognito is a service that provides user authentication, authorization, and user management for web and mobile applications. It handles common authentication use cases such as user registration, login, and password recovery, while allowing developers to implement additional security features like multi-factor authentication (MFA).

  1. User Pools: Manages user registration and authentication. You can add multi-factor authentication (MFA), social identity providers (Facebook, Google, etc.), and email/phone verification.

  2. Identity Pools: Helps users get access to AWS services like S3 or DynamoDB after authentication.

In this tutorial, we will focus on Cognito User Pools for handling user authentication.


Prerequisites

Before diving into code, you'll need:

  • An AWS account

  • Node.js installed on your machine


Step 1: Setting Up an AWS Cognito User Pool

  • Go to the AWS Cognito Console

  • Click Create a User Pool

  • Follow the steps to configure the pool:

    1. Step 1: Configure sign-in experience

    2. Step 2: Configure security requirements

    3. Step 3: Configure sign-up experience

    4. Step 4: Configure message delivery

    5. Step 5: Integrate your app

    6. Step 6: Review and create

  • Choose the necessary attributes (e.g., email) and configure policies, MFA, and email verification settings.

  • App Client Creation: Create an App Client under your User Pool, and for this tutorial, enable the client secret.
    Make sure to note down:

    • User Pool ID

    • App Client ID

    • App Client Secret


Step 2: Using AWS SDK for JavaScript v3 in Node.js

AWS SDK for JavaScript v3 is modular and lightweight, providing better performance and the ability to import only the necessary components. We'll use @aws-sdk/client-cognito-identity-provider from AWS SDK v3 to interact with Cognito for user authentication.

Setup your environment:

Before using the AWS SDK in your Node.js application, create an .env file to store sensitive credentials:

AWS_REGION=ap-south-1
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY
COGNITO_CLIENT_ID=YOUR_COGNITO_APP_CLIENT_ID
COGNITO_CLIENT_SECRET=YOUR_COGNITO_APP_CLIENT_SECRET

Install the required dependencies:

npm init -y
npm install dotenv crypto @aws-sdk/client-cognito-identity-provider

Step 3: Project Setup

Your project folder should follow this structure:

/project-directory
├── .env                       # Environment variables
├── cognitoUtil.js             # Shared Cognito utilities
├── cognitoErrors.js           # Centralized error handling
├── signup.js                  # User sign-up
├── signup_confirm.js          # Confirm sign-up
├── signin.js                  # User sign-in
├── resend_code.js             # Resend confirmation code
├── verify_passwd_reset.js     # Password reset (initiate)
├── confirm_passwd_reset.js    # Confirm password reset
├── package.json  
├── package-lock.json

Step 4: Sign-Up, Confirm, Sign-In, Resend Code, and Password Reset

We will now write Node.js code to handle the user authentication flow sign-up, confirmation, and sign-in using AWS SDK v3.

  1. Sign-Up User

The sign-up process allows users to create an account using their email and password. The signup.js file contains a script that handles user registration:

const { SignUpCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handleCognitoSignUpErrors } = require('./cognitoErrors');
require('dotenv').config(); // Load .env variables

const cognitoClient = initializeCognitoClient();

const signUpUser = async (email, password) => {
  const secretHash = calculateSecretHash(email);

  const command = new SignUpCommand({
    ClientId: process.env.COGNITO_CLIENT_ID,
    Username: email,
    Password: password,
    SecretHash: secretHash,
    UserAttributes: [{ Name: 'email', Value: email }],
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("Sign-up successful! Check your email to verify your account.");
  } catch (error) {
    handleCognitoSignUpErrors(error);
  }
};

signUpUser('user@example.com', 'Password123!');
  1. Confirm User Sign-Up

Once the user receives a confirmation code via email, they must confirm their account. The signup_confirm.js handles this:

const { ConfirmSignUpCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handleCognitoSignUpErrors } = require('./cognitoErrors');
require('dotenv').config(); 

const cognitoClient = initializeCognitoClient();

const confirmSignUp = async (email, confirmationCode) => {
  const secretHash = calculateSecretHash(email);

  const command = new ConfirmSignUpCommand({
    ClientId: process.env.COGNITO_CLIENT_ID,
    Username: email,
    ConfirmationCode: confirmationCode,
    SecretHash: secretHash,
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("User confirmed successfully!");
  } catch (error) {
    handleCognitoSignUpErrors(error);
  }
};

confirmSignUp('user@example.com', '123456');
  1. Sign-In User

After confirming the user, they can sign in to their account. The signin.js handles authentication:

const { InitiateAuthCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handleCognitoSignInErrors } = require('./cognitoErrors');
require('dotenv').config(); 

const cognitoClient = initializeCognitoClient();

const signInUser = async (email, password) => {
  const secretHash = calculateSecretHash(email);

  const command = new InitiateAuthCommand({
    AuthFlow: 'USER_PASSWORD_AUTH',
    ClientId: process.env.COGNITO_CLIENT_ID,
    AuthParameters: {
      USERNAME: email,
      PASSWORD: password,
      SECRET_HASH: secretHash,
    }
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("Sign-in successful!", data);
  } catch (error) {
    handleCognitoSignInErrors(error);
  }
};

signInUser('user@example.com', 'Password123!');
  1. Resend Confirmation Code

If a user hasn’t confirmed their account, this script resend_code.js can resend the verification code:

const { ResendConfirmationCodeCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handleResendCodeErrors } = require('./cognitoErrors');
require('dotenv').config(); 

const cognitoClient = initializeCognitoClient();

const resendCode = async (email) => {
  const secretHash = calculateSecretHash(email);

  const command = new ResendConfirmationCodeCommand({
    ClientId: process.env.COGNITO_CLIENT_ID,
    Username: email,
    SecretHash: secretHash,
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("Code resent successfully!", data);
  } catch (error) {
    handleResendCodeErrors(error);
  }
};

resendCode('user@example.com');
  1. Password Reset (Forgot Password)

Users can initiate a password reset verify_passwd_reset.js if they’ve forgotten their password:

const { ForgotPasswordCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handlePasswordResetErrors } = require('./cognitoErrors');
require('dotenv').config(); 

const cognitoClient = initializeCognitoClient();

const initiatePasswordReset = async (email) => {
  const secretHash = calculateSecretHash(email);

  const command = new ForgotPasswordCommand({
    ClientId: process.env.COGNITO_CLIENT_ID,
    Username: email,
    SecretHash: secretHash,
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("Password reset initiated. Check your email for the confirmation code.");
  } catch (error) {
    handlePasswordResetErrors(error);
  }
};

initiatePasswordReset('user@example.com');
  1. Confirm Password Reset

After receiving the confirmation code confirm_passwd_reset.js, users can confirm their new password:

const { ConfirmForgotPasswordCommand } = require("@aws-sdk/client-cognito-identity-provider");
const { initializeCognitoClient, calculateSecretHash } = require('./cognitoUtil');
const { handleConfirmPasswordErrors } = require('./cognitoErrors');
require('dotenv').config(); 

const cognitoClient = initializeCognitoClient();

const confirmPasswordReset = async (email, confirmationCode, newPassword) => {
  const secretHash = calculateSecretHash(email);

  const command = new ConfirmForgotPasswordCommand({
    ClientId: process.env.COGNITO_CLIENT_ID,
    Username: email,
    ConfirmationCode: confirmationCode,
    Password: newPassword,
    SecretHash: secretHash,
  });

  try {
    const data = await cognitoClient.send(command);
    console.log("Password reset successful!", data);
  } catch (error) {
    handleConfirmPasswordErrors(error);
  }
};

confirmPasswordReset('user@example.com', '123456', 'NewPassword123!');

Step 5: Shared Cognito Utilities and Error Handling

  1. Shared Utilities (cognitoUtil.js)

Instead of initializing the Cognito client and calculating the secret hash in every file, we’ll create a shared utility file:

require('dotenv').config(); // Load environment variables
const crypto = require('crypto'); // For hashing
const { CognitoIdentityProviderClient } = require("@aws-sdk/client-cognito-identity-provider");

/**
 * Initialize the AWS Cognito Identity Provider Client
 */
const initializeCognitoClient = () => {
  return new CognitoIdentityProviderClient({
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
  });
};

/**
 * Calculate the secret hash for the given username
 * @param {string} username - The username (email)
 * @returns {string} The calculated secret hash
 */
const calculateSecretHash = (username) => {
  const clientSecret = process.env.COGNITO_CLIENT_SECRET;
  const clientId = process.env.COGNITO_CLIENT_ID;

  const hmac = crypto.createHmac('sha256', clientSecret);
  hmac.update(username + clientId);
  return hmac.digest('base64');
};

module.exports = {
  initializeCognitoClient,
  calculateSecretHash,
};
  1. Error Handling (cognitoErrors.js)

To enhance the user experience, we'll add error handling to all our scripts. This will provide better feedback when things go wrong (e.g., invalid credentials, user not confirmed).

We’ll centralize error handling for all our operations in the cognitoErrors.js file. This allows for better maintainability and cleaner code across our project.

Each script will have a dedicated error-handling function:

// Handle errors during Cognito sign-up process
const handleCognitoSignUpErrors = (error) => {
  switch (error.name) {
    case 'UsernameExistsException':
      console.error('This username already exists. Please try a different one.');
      break;
    case 'InvalidPasswordException':
      console.error('The password does not meet the security requirements.');
      break;
    case 'InvalidParameterException':
      console.error('An invalid parameter was passed. Please check your input.');
      break;
    case 'TooManyRequestsException':
      console.error('Too many requests. Please try again later.');
      break;
    default:
      console.error(`An unknown error occurred: ${error.message}`);
  }
};

// Handle errors during Cognito sign-in process
const handleCognitoSignInErrors = (error) => {
  switch (error.name) {
    case 'NotAuthorizedException':
      console.error('Invalid username or password.');
      break;
    case 'UserNotConfirmedException':
      console.error('User is not confirmed. Please check your email.');
      break;
    case 'TooManyRequestsException':
      console.error('Too many sign-in attempts. Please try again later.');
      break;
    default:
      console.error(`An unknown error occurred: ${error.message}`);
  }
};

// Handle errors during the resend confirmation code process
const handleResendCodeErrors = (error) => {
  switch (error.name) {
    case 'UserNotFoundException':
      console.error('No user found with the provided email.');
      break;
    case 'CodeDeliveryFailureException':
      console.error('Failed to deliver the verification code.');
      break;
    default:
      console.error(`An unknown error occurred: ${error.message}`);
  }
};

// Handle errors during password reset initiation process
const handlePasswordResetErrors = (error) => {
  switch (error.name) {
    case 'UserNotFoundException':
      console.error('No user found with the provided email.');
      break;
    case 'TooManyRequestsException':
      console.error('Too many requests. Please try again later.');
      break;
    default:
      console.error(`An unknown error occurred: ${error.message}`);
  }
};

// Handle errors during password reset confirmation process
const handleConfirmPasswordErrors = (error) => {
  switch (error.name) {
    case 'CodeMismatchException':
      console.error('The confirmation code you entered is incorrect.');
      break;
    case 'ExpiredCodeException':
      console.error('The confirmation code has expired. Please request a new code.');
      break;
    case 'InvalidPasswordException':
      console.error('The new password does not meet the security requirements.');
      break;
    case 'TooManyRequestsException':
      console.error('Too many attempts. Please try again later.');
      break;
    default:
      console.error(`An unknown error occurred: ${error.message}`);
  }
};

module.exports = {
  handleCognitoSignUpErrors,
  handleCognitoSignInErrors,
  handleResendCodeErrors,
  handlePasswordResetErrors,
  handleConfirmPasswordErrors,
};

Conclusion

In this article, we have successfully integrated AWS Cognito with a Node.js application using AWS SDK for JavaScript v3. We implemented basic authentication features like user sign-up, confirmation, sign-in, and password reset. Additionally, we modularized the code using a shared utility file for initializing the Cognito client and calculating the secret hash.

This project can serve as a boilerplate for any Node.js application that requires user authentication with AWS Cognito. With robust error handling and modularized code, this structure ensures maintainability and scalability.

By using AWS Cognito, you can secure your applications with minimal effort while leveraging AWS’s powerful, scalable infrastructure


Code File: GitHub