User sign-up and session management using AWS Cognito
Let’s discuss today’s topic.
We are trying to build authentication with the Cognito service’s help. It is standard authentication, just like JWT, but as mentioned earlier, we are using Cognito.
Amazon Cognito offers user management, authentication, and authorization for your web and mobile apps.
Users can sign in directly with a username and password or through a third party like Facebook, Amazon, Google, or Apple.
Technologies used in this tutorial:
1. AWS services: AWS Cognito,
2. Database: PostgreSQL
3. ORM: Sequelize
The flow of the example we are going to build
First, let’s jump to the aws panel to create our first user pool.
Step 1: Go to the AWS panel and navigate to Cognito service.
Step 2: Click the “Create a UserPool” button in AWS-panel
Step 3: Create UserPool with default options.
Step 4: Create an app client with default options and uncheck the generate client secret box.
Step 5: After creating a user pool, it will give you the “UserPoolId” and “AppClientId.”
Look! how easily you have created your first user pool.
Store UserPoolId & ClientId somewhere safe for futher use.
Further, We will integrate this user pool with our express app. We will create different API endpoints to authenticate our users.
Let’s set up our express server
Prerequisite: Assuming you already have npm installed in your system
We initialize our app with “npm init -y” and install a few dependencies.
Dependencies:
npm i aws-sdk
npm i @aws-sdk/client-cognito-identity
npm i @aws-sdk/client-cognito-identity-provider
npm i amazon-cognito-identity-js
npm i express pg pg-hstore sequelize dotenv bcryptjs
Create a db.js file and initialize a database connection
import { Sequelize, DataTypes } from "sequelize";
import pg from "pg";
import { envConfig } from "../common/env.js";
export const dbConnection = new Sequelize(
envConfig.app.database.databaseName,
envConfig.app.database.username,
envConfig.app.database.password,
{
dialect: "postgres",
host: envConfig.app.database.host,
dialectModule: pg,
port: 5432,
}
);
Configure your index.js file
import express from "express";
import { envConfig } from "./common/env.js";
import { dbConnection } from "./database/db.js";
import { router } from "./routes/auth.route.js";
const app = express();
const PORT = envConfig.app.port || 3000;
app.use(express.json());
app.use("/api", router);
dbConnection.sync().then(() => {
console.log("Connected to db");
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
});
Then we will create an auth.route.js file for route handling.
import express from "express";
import {
signUp,
codeVerification,
login
} from "../controller/auth.controller.js";
export const router = express.Router();
router
.post("/signUp", signUp)
.post("/verifyCode", codeVerification)
.post("/login", login)
Let’s create an auth.controller.js file to write business logic
- Sign Up:
First, we import dependencies that will help us to create a sign-up method.
import {
SignUpCommand,
CognitoIdentityProviderClient,
ConfirmSignUpCommand,
GetUserCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import { userTable } from "../database/db.js";
import { envConfig } from "../common/env.js";
import { hashPassword } from "../utils/hashPassowrd.js";
- The signup method will take the username, email, and password as input and create the user in the Cognito directory. We will also save the user’s information for further need. It will help us to add some validation in our application, such as preventing duplicate entries.
export const signUp = async (req, res) => {
const { email, username, password } = req.body;
const cognito = new CognitoIdentityProviderClient({
region: envConfig.app.aws.region,
});
const existingUser = await userTable.findOne({
where: { email },
});
if (existingUser) {
return {
isError: true,
message: res.status(400).send("User already exists"),
};
}
const userData = await new Promise(async (res, rej) => {
try {
const hashedPassword = await hashPassword(password);
const newUser = await userTable.create({
email,
username,
password: hashedPassword,
});
res(newUser);
const params = {
ClientId: envConfig.app.aws.cognito.authClient,
Username: username,
Password: password,
UserAttributes: [
{
Name: "email",
Value: email,
},
],
};
const command2 = new SignUpCommand(params);
await cognito.send(command2);
} catch (error) {
rej(error);
}
});
return {
data: res.send({
id: userData.id,
name: userData.username,
email: userData.email,
}),
};
};
Output:
{
"data": {
"id": 21,
"name": "John",
"email": "Your registered emailId"
}
}
- This method will add the user to the Cognito directory and database as well and send a verification code to entered email address. If you don’t get any verification code, ensure you have white-listed your email address in Amazon’s simple email service.
2. Code Verification:
After the sign-up method, we need one more method to verify our user through the verification code
Here’s an example of the code that you will get.
As we can see, the user’s status is showing unconfirmed, and the email verified flag is false.
verifyCode method will take the verification code as input and return a user’s information.
export const codeVerification = async (req, res) => {
const { code, email } = req.body;
const cognito = new CognitoIdentityProviderClient({
region: envConfig.app.aws.region,
});
const existingUser = await userTable.findOne({ where: { email } });
if (!existingUser) {
return res.status(400).send({
isError: "true",
message: res.send("User not found"),
});
}
const commandConfirmSignUp = new ConfirmSignUpCommand({
ClientId: envConfig.app.aws.cognito.authClient,
ConfirmationCode: code,
Username: existingUser.username,
});
await cognito
.send(commandConfirmSignUp)
.then((data) => {
return data;
})
.catch((err) => {
return err;
});
return {
data: res.status(200).send({
message: "User verified successfully",
}),
};
};
After verifying the code, you will get a message like “User verified successfully,” Otherwise, it will throw an error like the user is not confirmed yet.
Once the email gets confirmed, the user’s status will change to confirm, and the email flag will be true.
Before going to the login API, we must create one more function to get tokens after successful verification.
We will create a getToken.js that will help us to get tokens.
import {
CognitoUserAttribute,
CognitoUserPool,
AuthenticationDetails,
CognitoUser,
} from "amazon-cognito-identity-js";
import { userTable } from "../database/db.js";
/**
This function will take userpoolId, clientId, email and password as input
and returns token object.
@params userPoolId - The id of created userPool
@params clientId - The id of app client
**/
export const getToken = async (userpoolid, clientid, email, password) => {
const poolData = {
UserPoolId: userpoolid,
ClientId: clientid,
};
const userPool = new CognitoUserPool(poolData);
const existingUser = await userTable.findOne({ where: { email } });
const attributeList = [];
const dataEmail = {
Name: "email",
Value: email,
};
const userData = {
Username: existingUser.username,
Pool: userPool,
};
const attributeEmail = new CognitoUserAttribute(dataEmail);
attributeList.push(attributeEmail);
const authData = {
Username: email,
Password: password,
};
const cognitoUser = new CognitoUser(userData);
const authenticationDetails = new AuthenticationDetails(authData);
const User_Tokens = await new Promise(async (res, rej) => {
try {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
const token = {
accessToken: result.getAccessToken().getJwtToken(),
idToken: result.getIdToken().getJwtToken(),
refreshToken: result.getRefreshToken().getToken(),
};
res(token);
},
onFailure: function (err) {
rej(err);
},
});
} catch (error) {
rej(error);
return error;
}
});
await userTable.findOne({
where: {
email: email,
},
})
.then((user) => {
user.tokens = [];
user.tokens.push(User_Tokens.accessToken, User_Tokens.refreshToken);
userTable.update(
{
tokens: user.tokens,
},
{
where: { email },
}
);
})
.catch((err) => {
return err;
});
return User_Tokens;
};
3. Login:
The login method will take email and password as input and return tokens.
export const login = async (req, res) => {
const { email, password } = req.body;
const existingUser = await userTable.findOne({ where: { email } });
if (!existingUser) {
res.status(400).send({
isError: "true",
message: res.send("User not found"),
});
}
const tokens = await getToken(
envConfig.app.aws.cognito.authUserPool,
envConfig.app.aws.cognito.authClient,
email,
password
);
const cognito = new CognitoIdentityProviderClient({
region: envConfig.app.aws.region,
});
const getUserFromCognito = new GetUserCommand({
AccessToken: tokens.accessToken,
});
await cognito.send(getUserFromCognito);
return {
body: res.send(tokens),
};
};
In the end, you will get a JSON of tokens that will contain accessToken, idToken, and refreshToken
Congratulations, you have successfully implemented the first authentication using Cognito.
“Essential insights and conclusions”
In addition, we can add token revocation by using the refresh token.
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html
See you in the next blog, will implement custom SSO using Cognito.
Follow Simform Engineering to keep up with all the latest trends in the development ecosystem.