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
- 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
- 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
- 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 outputmodule
: Facilitates the utilization of a module manager in the compiled JavaScript code. CommonJS is supported and is a standard in Node.jsstrict
: Toggles strict type-checking protocolsesModuleInterop
: Enables the compilation of ES6 modules to CommonJS modulesskipLibCheck
: When set totrue
, bypasses the type checking of default library declaration filesforceConsistentCasingInFileNames
: When set totrue
, 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 insidesrc
and create fileauth.ts
in it. - Let’s import few stuffs (will explain
generateSecretHash
later part). - Install
aws-sdk
via npm and createconfig
folder under src in that createaws.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 thesrc
in that create filegenerateSecretHash.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:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast
- Check out CoFeed, the smart way to stay up-to-date with the latest in tech 🧪
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com