Hi everyone,
Today we will be discussing the authentication flow setup, that I used in my recent Express and Mongoose application.
In today's digital landscape, where security breaches and unauthorized access are persistent threats, implementing a robust authentication flow is crucial for any web application. Whether it's protecting user data, safeguarding sensitive information, or ensuring authorized access to specific resources, a secure authentication system forms the bedrock of a trustworthy and reliable application.
Today we will discuss the intricacies of building a secure authentication flow in an Express server using the power of Mongoose and MongoDB. Express provides a flexible and scalable framework for building web applications, while Mongoose acts as a powerful Object Data Modeling (ODM) library that simplifies working with MongoDB, a widely-used NoSQL database.
Throughout this article, we will explore the different steps involved in setting up a secure authentication flow, including user registration, login, session management, and access control. By following this article, we will gain the knowledge and tools necessary to create a robust authentication system that protects user accounts, verifies credentials, and restricts access to authorized users only.
The authentication flow we will be discussing consists of Registration, Verification, Login, Session Management, Access Control, and Logout.
Before I start, I would like to demonstrate the user schema that I am using for this app
interface TokenDetails {
token: string;
sessionsDetails: UAParser.IResult;
}
export interface UserDocument extends mongoose.Document {
email: string;
password: string;
name: string;
isOTPVerified: boolean;
otp: string;
otpGeneratedTime?: string;
sessionsDetails: TokenDetails[];
}
So we will be storing the user email, password, name, isOTPVerified, otp, otpGeneratedTime and sessionsDetails
The registration step allows users to create new accounts and provide credentials for future authentication. Users will typically provide their email, password, and name.
The primary step is to apply validations. I have used JOI for validating the user payload sent over the network call about which you can read more here.
Since using Mongoose, I have hashed the password in a lifecycle method using bcrypt, which you can read more about here.
Once data is validated we will send an OTP to the user's email for verification and save the user in our database.
Note, the user is saved but not verified yet.
We will accept the user's email and OTP as the payload in our API call for verification. We will first check the verification time using otpGeneratedTime
. In my case, I have set the time for OTP expiry as 30 min. If OTP is still valid then I will compare the OTP from payload to the one in database.
If valid we can just update the user details to the following.
await User.updateOne(
{ _id: user._id },
{ isOTPVerified: true, otp: null, otpGeneratedTime: null }
);
At this point, we will also generate the token and session details for the user, since we will track the number of sessions per user. In my app, I allow the maximum number of sessions per user to be 3. You can update as per your requirement
userSchema.methods.updateUserSessionDetails = async function (headers: {
[x: string]: string | string[] | undefined;
}) {
const user = this;
if (user.sessionsDetails.length > MAX_SESSIONS_ALLOWED - 1) {
throw `Max ${MAX_SESSIONS_ALLOWED} sessions allowed`;
} else {
const token = await user.generateToken();
const sessionsDetails = [
...user.sessionsDetails,
{ token, sessionDetails: parser(headers["user-agent"] as any) },
];
await User.updateOne(
{ _id: user._id },
{
sessionsDetails,
}
);
}
We can now always count the number of sessions the user has by checking the sessionsdetails
in the database..
Once registered, users need to authenticate themselves to access protected resources. The login step involves verifying the user's credentials against the stored information. We will be accepting the user's email and password as payload.
We will verify these details, mainly that the email is verified and the password sent is correct. One more step will again be adding a new session on every app.
So once these routes are set up, we can start making the protected routes. Those routes will only be accessible to the users who send a valid token in their network calls.
The authorization tokens are attached to the API headers. We will make a middleware at our end which will validate all protected routes, which we have already discussed here.
import jwt, { JwtPayload } from "jsonwebtoken";
import { NextFunction, Request, Response } from "express";
import { User } from "../models/user";
export const auth = async (req: Request, res: Response, next: NextFunction) => {
try {
const token = req.headers.authorization?.replace("Bearer ", "");
if (token) {
const decoded: JwtPayload = jwt.verify(
token,
process.env.JWT_SECRET || ""
) as JwtPayload;
const user = await User.findOne({ _id: decoded._id });
if (!user) {
throw new Error();
} else {
let isValidSession = false;
user.sessionsDetails.forEach((session) => {
if (session.token === token) {
isValidSession = true;
}
});
if (isValidSession) {
req.body._userAssociatedWithToken = user;
req.body._token = token;
next();
} else {
res.status(401).send({ error: "Invalid Token" });
}
}
} else {
res.status(401).send({ error: "Authorization key missing" });
}
} catch (error) {
res.status(401).send({ error: "Please authenticate." });
}
};
Our middleware not only checks for the validity of the token but also verifies if the token is present in sessionsDetails
We will also implement logout route, where we will accept the token in the headers. On successful logouts, we will just remove the corresponding token from the session details.
Note: From the client side, always make sure to attach the correct token to the headers in case of logout.
We can also provide our users with /sessions
route, which will give them information about all their sessions, from any device that they are using. So users can Logout from anywhere, i.e., clear any of their sessions from any device. This further secures our application.
Note: The sessions make take up to 24 hours to update.
With the completion of these steps, we have created a robust and secure authentication flow using the Express server with Mongoose.
Docker: Managing data with Volumes (Part 2) Hello everyone, In the last post, we discussed the concept of volumes. We discussed the use of named and anonymous volumes. In this post, we will discuss the need and use of bind mounts We will learn the use of .dockerignore files. We will also see working with environment variables and .env files. We will also learn the use of build arguments.
Docker: Managing data with Volumes (Part 1) Hello everyone, In the last post, we learned to perform various operations with containers and images. In this post, we will explore volumes, which are like state management for running applications with docker. We will discuss different kinds of data we encounter while working with docker. We will learn about different kinds of volumes, and explore anonymous and named volumes in detail by discussing their implementation in a node server application. We will create these volumes in our system.