AWS in Plain English

New AWS, Cloud, and DevOps content every day. Follow to join our 3.5M+ monthly readers.

Follow publication

Harnessing AWS Cognito for Node.js Apps: A Practical Guide

--

Authentication plays a vital role in securing modern web applications.It acts as the first line of defence, verifying user identities and safeguarding critical systems from unauthorised access.

Imagine you have a secret club, and you want to ensure only members can enter. To do that, you need a way to verify who they are — that’s what authentication does in the backend. Authentication is the process of checking if someone is who they say they are. It’s like when you enter a password or use your fingerprint to unlock your phone. In the digital world, the backend acts like the bouncer at the club, checking your credentials to ensure you’re a member.

Implementing robust authentication can be complex and time-consuming. That’s where AWS Cognito comes in, offering a simplified and scalable solution to manage user sign-ups, sign-ins, and access control. With AWS Cognito, developers can seamlessly integrate authentication into their applications, leveraging features like token-based authentication, multi-factor authentication, and identity federation to enhance security while reducing the burden of managing credentials manually.

In this post we will go through how you can setup the AWS cognito with nodejs ,complete with code examples and practical steps.

What is AWS Cognito?

You can think of Cognito as a service with three distinct feature sets.

Cognito User Pool

which is a managed identity service that handles everything related to user sign-up and sign-in.

It implements common user flows:

  • Registration
  • Sending verification codes to email or phone numbers
  • Signing in, Signing out
  • Forgotten passwords, Changing passwords and more

It supports user groups, allows custom attributes on users, and provides admin methods for creating users and finding users by email, username, etc. It supports identity federation, enabling social sign-in with Google, Facebook, Apple, and Amazon.

Cognito Identity Pools

which allow you to take authorization tokens issued by identity providers and exchange them for temporary AWS credentials.

In this case, you authenticate against one of the supported identity providers, including Cognito User Pool, and once authenticated, you receive an authorization token from the provider. This token can be sent to a Cognito Identity Pool, which validates it with the identity provider and issues temporary AWS credentials in return.

These credentials can be configured with the identity pool to provide access to AWS services.

Cognito Sync

which allows you to sync user profile data across multiple devices.

Setting Up AWS Cognito

Step 1: Creating a User Pool

  1. Access Cognito
  • Log in to your AWS Management Console, search for Cognito, and select it.

2. Start the User Pool Setup

  • Navigate to User Pools in the left-hand panel and click Create User Pool.

3. Set up resource

  • Provide application name
  • Choose sign-in identifiers here we will select (Email)
  • Click on Create user directory

4. Set up page

  • They have provided how you can setup you application for specific language. You can go through that. We will use Node js
  • Go to bottom and click on Go to overview. Where you will be able to see the details of your user pool.

5. Overview of user pool

  • Rename the user pool name if you want by clicking Rename button.

6. Authentication methods

  • Click on Authentication methods from side under Authentication.
  • Here you can update how you are going to confirm user a/c. (We will use email)
  • Configure password requirements (default: includes a number, special character, etc.).

Step 2: App Clients

  1. App Client walkthrough
  • Click in App clients in side bar under Applications.
  • In this section you will see all the details we need to integrate our application with cognito.

Setting Up Node.js app ( show me the code talk is cheap 😉)

We will be using Express and Typescript for this demo

  1. Creating a package.json file

Start by creating a new directory in your local development environment, and within it, use npm’s initializer command to create a package.json file. If you use a package manager other than npm, consider adhering to the init command provided by that specific package manager:

mkdir blog-cognito-demo
cd blog-cognito-demo/
npm init -y

2. Creating a minimal server with Express

After initializing the package.json file, add the Express and DotEnv packages to the project. In the terminal window, run the following command, where npm i is an alias to npm install.

npm i express dotenv

Then, create a directory called src at the project’s root to organize our application source files. Add a new file named index.js to it.

import express from "express";
import routes from "./routes";


const app = express();
const port = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(routes);

app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});

To start the server, execute the command node src/index.js in the terminal. This will execute the code that we just added to the index.js file and should spin-up the server

3. Installing TypeScript

We will begin by installing TypeScript as a development dependency. Additionally, we’ll install the @types declaration packages for Express and Node.js, which offer type definitions in the form of declaration files.

Launch the terminal and install the packages described above using the following command:

npm i -D typescript @types/express 

The -D, or --dev, flag directs the package manager to install these libraries as development dependencies.

Installing these packages will add a new devDependencies object to the package.json file, featuring version details for each package, as shown below:

{
...
"devDependencies": {
"@types/node": "^22.10.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
},
...
}

4. Generating the TypeScript configuration file

Every TypeScript project utilizes a configuration file to manage various project settings. The tsconfig.json file, which serves as the TypeScript configuration file, outlines these default options and offers the flexibility to modify or customize compiler settings to suit your needs.

The tsconfig.json file is usually placed at the project’s root. To generate this file, use the following tsc command, initiating the TypeScript Compiler:

npx tsc --init

Once you execute this command, you’ll notice the tsconfig.json file is created at the root of your project directory.

Upon opening the tsconfig.json file, you’ll notice several other commented-out compiler options. Among all of these options, compilerOptions is a mandatory field that must be specified. Here’s a summary of all the default options that belong inside the compilerOptions field:

  • target: Enables the specification of the target JavaScript version that the compiler will output
  • module: Facilitates the utilization of a module manager in the compiled JavaScript code. CommonJS is supported and is a standard in Node.js
  • strict: Toggles strict type-checking protocols
  • esModuleInterop: Enables the compilation of ES6 modules to CommonJS modules
  • skipLibCheck: When set to true, bypasses the type checking of default library declaration files
  • forceConsistentCasingInFileNames: When set to true, enforces case-sensitive file naming

One crucial option you will need to enable is outDir, which determines the destination directory for the compiled output. Locate this option in the tsconfig.json file and uncomment it.

By default, the value of this option is set to the project’s root. Change it to dist, as shown below:

{
"compilerOptions": {
...
"outDir": "./dist"
...
}
}

While there are probably other configuration options you can add to the TypeScript compiler, the options above are basic specifications that can help you get started.

You should now update the main field in the package.json file to dist/index.js because the TypeScript code will compile from the src directory to dist.

5. Running TypeScript in Node with ts-node

As previously discussed, executing a TypeScript file in Node is not supported by default. However, we can overcome this limitation by leveraging ts-node, a TypeScript execution environment for Node. Let’s first use ts-node with npx without installing it as a dependency and observe the output:

npx ts-node src/index.ts

6. Watching file changes

To enhance the development workflow for Node.js projects, I often use nodemon, a utility library that automatically restarts a Node-based application upon detecting file changes in the specified directories.

Because nodemon doesn’t work with TypSscript files out of the box, we will also install ts-node as a development dependency. This ensures nodemon automatically picks up ts-node to hot reload the Node server when changes are made to TypeScript files, streamlining the development process.

Execute the following command to integrate nodemon and ts-node as development dependencies:

npm i -D nodemon ts-node

After installing these dev dependencies, update the scripts in the package.json file as follows:

{
"scripts": {
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts"
}
}

Referring to the added script modifications above, the build command compiles the code into JavaScript and saves it in the dist directory using the TypeScript Compiler (tsc). The dev command is designed to run the Express server in development mode with the help of nodemon and ts-nod

Finally, return to the terminal window and execute npm run dev to initiate the development server.

Now let’s write some API routes

6. API Routes

  • Create routes folder inside src and create file auth.ts in it.
  • Let’s import few stuffs (will explain generateSecretHashlater part).
  • Install aws-sdk via npm and create config folder under src in that create aws.ts
import AWS from "aws-sdk";

AWS.config.update({
region: "ap-south-1",
});

const cognito = new AWS.CognitoIdentityServiceProvider();

export default cognito;
import { Request, Response, Router } from "express";
import cognito from "../config/aws";
import dotenv from "dotenv";
import { generateSecretHash } from "../middleware/generateSecretHash";

dotenv.config();
const router = Router();
  • Sign up route that will create the user in cognito
  • Create .env in root folder add these value in it. You can find these values in App client section which we have seen earlier.
COGNITO_USER_POOL_ID=<value>
COGNITO_CLIENT_ID=<value>
COGNITO_CLIENT_SECRET=<value>
router.post(
"/signup",
generateSecretHash,
async (req: Request, res: Response) => {
console.log("req.body", req.body);
const { username, password, name, secretHash } = req.body;

const params = {
ClientId: process.env.COGNITO_CLIENT_ID!,
Username: username,
Password: password,
SecretHash: secretHash,
UserAttributes: [
{
Name: "given_name",
Value: name,
},
],
};
try {
const data = await cognito.signUp(params).promise();
res.status(201).json({ message: "User created successfully", data });
} catch (error) {
res.status(400).json({ message: "Error signing up", error });
}
}
);

Here we need secretHash to verify so I have create the middleware for it

  • Create middleware folder inside the src in that create file generateSecretHash.ts
import { Request, Response, NextFunction } from "express";
import { createHmac } from "crypto";

export const generateSecretHash = (
req: Request,
res: Response,
next: NextFunction
) => {
const { username } = req.body;

if (!username) {
res.status(400).json({ message: "Username is required" });
return;
}

const hasher = createHmac("sha256", process.env.COGNITO_CLIENT_SECRET!);
hasher.update(`${username}${process.env.COGNITO_CLIENT_ID!}`);
req.body.secretHash = hasher.digest("base64");
next();
};
  • We need to confirm user so creating route for that
router.post(
"/confirm-signup",
generateSecretHash,
async (req: Request, res: Response) => {
const { username, confirmationCode, secretHash } = req.body;

const params = {
ClientId: process.env.COGNITO_CLIENT_ID!,
Username: username,
ConfirmationCode: confirmationCode,
SecretHash: secretHash,
};

try {
const data = await cognito.confirmSignUp(params).promise();
res.status(200).json({ message: "User confirmed successfully", data });
} catch (error) {
console.error("Error confirming user:", error);
res.status(400).json({ message: "Error confirming user", error });
}
}
);
  • Login route which we will use when user confirms email.
  • We are storing the AccessToken in the cookies so FE can access it from cookies to maintain the session.
router.post(
"/login",
generateSecretHash,
async (req: Request, res: Response) => {
const { username, password, secretHash } = req.body;

const params = {
AuthFlow: "USER_PASSWORD_AUTH",
ClientId: process.env.COGNITO_CLIENT_ID!,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
SECRET_HASH: secretHash,
},
};

try {
const data = await cognito.initiateAuth(params).promise();
const token = data?.AuthenticationResult?.AccessToken;

res.cookie("accessToken", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 3600 * 1000,
});

res.status(200).json({ message: "Login successful", data });
} catch (error) {
res.status(401).json({ message: "Authentication failed", error });
}
}
);
  • At last logout route to logout the user and clear the session.
router.post("/logout", async (req: Request, res: Response) => {
const { accessToken } = req.body;

const params = {
AccessToken: accessToken,
};

try {
await cognito.globalSignOut(params).promise();
res.status(200).json({ message: "Logout successful" });
} catch (error) {
res.status(400).json({ message: "Error logging out", error });
}
});

You can test all of this in postman. I am using postman vs code extension for testing purpose.

  • Under user mananagement you can see the user is create

Here you can see the conformation status is unconfirmed. We will confirm the user first.

  • Confirmation code you will receive in the email you have provided.
  • User login here we have received the Access token as a response which we will store inside the cookies

Now, you can consume these API endpoints in the FE. AWS also provides self hosted UI which you can directly integrate that in you FE.

I hope this article has helped you learn how to seamlessly integrate Amazon Cognito with Node.js, giving your applications the edge of secure and scalable user authentication. Whether you’re building your first app or enhancing an existing one, Cognito offers powerful features to simplify user management.

Thank you for taking the time to read through this article! If you have any questions, suggestions, or experiences to share, feel free to drop a comment below. Let’s continue learning and building amazing things together!

Twitter | LinkedIn | Portfolio

Thank you for being a part of the community

Before you go:

--

--

Published in AWS in Plain English

New AWS, Cloud, and DevOps content every day. Follow to join our 3.5M+ monthly readers.

Written by Pushkar Thakur

AWS Community Builder | Web Developer 👨‍💻 | Love to explore 🔍 new stuff

No responses yet