DEV Community

Cover image for Implement OTP Verification using Redis and Node.js
Desmond Obisi
Desmond Obisi

Posted on

Implement OTP Verification using Redis and Node.js

Introduction

In today's digital space, user authentication and verification are crucial components of any application's security measure. One-time password (OTP) systems provide an extra layer of security by generating unique codes that can be used once and are valid for a short period.

This article will guide you through the process of building an OTP and verification feature using Redis and Node.js. Redis, a powerful in-memory data structure store, will be utilized to store and manage the OTPs efficiently.

Overview of OTP-based Authentication

In this section, we will go through an overview of OTP-based authentication and discuss its advantages over traditional password-based systems.

What is OTP-based Authentication?

One-time password (OTP) authentication is a mechanism that provides an additional layer of security during an authentication process.

Unlike traditional password-based authentication, which relies on fixed passwords, OTP-based authentications generate random and unique codes that are valid for a short period (typically a few minutes). Users are required to provide the OTP along with their username or email to gain access to the system.

Advantages of OTP-based Authentication:

OTP-based authentication offers several advantages over traditional password-based systems, a few of which are:

  • Increased Security
  • Mitigation of Password-related Risks
  • Two-factor Authentication (2FA)

By implementing OTP-based authentication and verification in your applications, you can significantly enhance the security of user authentication while providing a seamless, user-friendly experience.

Let's move on to the next section, where we will set up the development environment for our project.

Setting Up the Development Environment

In this section, we will go through the process of setting up the development environment for building the OTP and verification feature with Redis and Node.js. We will first cover the installation of Node.js, setting up Redis, and creating a new Node.js project.

Setting Up Node.js:

To begin, you need to install Node.js on your system. Node.js is available for multiple operating systems and can be downloaded from the official Node.js website. Follow these steps to install Node.js:

  • Visit the Node.js website using your web browser.
  • Download the appropriate Node.js installer for your operating system (Windows, macOS, or Linux).
  • Run the installer and follow the on-screen instructions to complete the installation process.

To verify that Node.js is installed correctly, open a command prompt or terminal and run the following command:

    node --version
Enter fullscreen mode Exit fullscreen mode

You should see the version number of Node.js printed in the console.
Node Version

Setting Up Redis

Next, you need to set up Redis, the in-memory data structure store that will be used to store and manage the OTPs. Follow these steps to install and configure Redis:

  • Visit the Redis website using your web browser.
  • Download the Redis distribution package for your operating system. Since Redis does not run directly on Windows, you can either run it through WSL2 (Windows Subsystem for Linux) or by downloading this port of Redis for Windows here and installing it. In this tutorial, we will be using the latter.
  • Open a command prompt or terminal and run the Redis server by executing the following command:
    redis-cli
Enter fullscreen mode Exit fullscreen mode

This will start the Redis server on the default port 6379.
Redis CLI

Creating a New Node.js Project

Now that you have Node.js and Redis all setup, let's create a new Node.js project for building the OTP and verification feature.

Follow these steps:

  • Open a command prompt or terminal and create or navigate to the directory where you want to create your project.
    mkdir tutorials && cd tutorials
Enter fullscreen mode Exit fullscreen mode
  • Run the following command to clone the template that I made for this project in my GitHub repository and after that, run it.
    git clone https://github.com/DesmondSanctity/node-redis-otp.git && npm install
Enter fullscreen mode Exit fullscreen mode

This will give you most of the project setup except the various logic for the OTP generation, Redis caching and data retrieval, which we will be implementing in the next section.

Generating and Caching OTPs with Redis

In this section, we will explore the implementation details of generating OTPs and caching the data with Redis. We will use a user signup/login system to demonstrate this concept. We would also use a Node.js client library redis and otp-generator to interact with the local Redis instance and generate unique OTPs respectively.

Connecting to the Local Redis Instance

To utilize the caching capabilities of Redis, set up and connect to the local Redis instance you installed on your machine. In the server.js file, set up the connection as shown below and export it for use in other files.

    ...
    // create a client connection
    export const client = redis.createClient();

    // on the connection
    client.on("connect", () => console.log("Connected to Redis"));

    await client.connect();
    ...
Enter fullscreen mode Exit fullscreen mode

Generating OTP for User and Storing to Redis

Now that you have established a connection with your local Redis installation, you can test generating OTPs and storing them in Redis. In the userController.js file where the user logic is, add a method to generate OTP and store OTP using the packages we installed earlier.

    ...
    export async function register(req, res) {

        try {
            const { username, password, email, firstName, lastName } = req.body;

            // Generate a random OTP using the otp-generator package
            const otp = otpGenerator.generate(4, {
                lowerCaseAlphabets: false,
                upperCaseAlphabets: false,
                specialChars: false
            });

            // check the existing user
            const existUsername = new Promise((resolve, reject) => {
                UserModel.findOne({ username: username }, function (err, user) {
                    if (err) reject(new Error(err))
                    if (user) reject({ error: "Please use unique username" });

                    resolve();
                })
            });

            // check for existing email
            const existEmail = new Promise((resolve, reject) => {
                UserModel.findOne({ email: email }, function (err, email) {
                    if (err) reject(new Error(err))
                    if (email) reject({ error: "Please use unique Email" });

                    resolve();
                })
            });

            await Promise.all([existUsername, existEmail])
                .then(() => {
                    if (password) {
                        bcrypt.hash(password, 10)
                            .then(hashedPassword => {

                                const user = new UserModel({
                                    username,
                                    password: hashedPassword,
                                    email,
                                    firstName,
                                    lastName
                                });

                                // Store the OTP in Redis, with the user's email as the key
                                client.set(email, otp);

                                const { password, ...responseUser } = user._doc;
                                // return save result as a response
                                user.save()
                                    .then(result => res.status(201).send({
                                        msg: "User Register Successfully",
                                        OTP: otp, User: responseUser
                                    }))
                                    .catch(error => res.status(500).send({ error }))

                            }).catch(error => {
                                return res.status(500).send({
                                    error: "Enable to hashed password"
                                })
                            })
                    }
                }).catch(error => {
                    return res.status(500).send({ error })
                })

        } catch (error) {
            return res.status(500).send(error);
        }

    }
    ...
Enter fullscreen mode Exit fullscreen mode

From the above code, the otpGenerator function from otp-generator package was used to generate four unique integers for the OTP and to store them in the Redis client using client.set(key, value); where value is the OTP we are storing and key is a unique identifier we will use to identify each stored value. In this case, the email of the user will be used as the key because it is unique per user.

You can check your local instance to see if the OTP is saved by querying Redis with the key/identifier. Run the command below to see the resulting OTP for a user with email example@gmail.com

    redis-cli
    GET example@gmail.com 
Enter fullscreen mode Exit fullscreen mode

Redis CLI Result

Verifying OTP for User

So far, a unique OTP has been generated on user registration, now we need to verify the user by writing another block of code. The logic here is to check the OTP to be supplied to the code block with the one in the local Redis instance, if they match, the user will be verified. If there is no match, the user will be notified that the OTP is incorrect.

    ...
    export async function verifyUser(req, res, next) {
        try {

            const { username, email, otp } = req.body;

            // check the user existance
            let exist = await UserModel.findOne({ username: username });
            if (!exist) return res.status(404).send({ error: "Can't find User!" });

            // Retrieve the stored OTP from Redis, using the user's email as the key
            const storedOTP = await client.get(email);

            if (storedOTP === otp) {
                // If the OTPs match, delete the stored OTP from Redis
                client.del(email);

                // Update the user's isVerified property in the database
                await UserModel.findOneAndUpdate({ username }, { isVerified: true });

                // Send a success response
                res.status(200).send('OTP verified successfully');
            } else {
                // If the OTPs do not match, send an error response
                res.status(400).send('Invalid OTP');
            }
            next();

        } catch (error) {
            return res.status(500).send({ error });
        }
    }
    ...
Enter fullscreen mode Exit fullscreen mode

As you can see from the code, the client.get(key) method was used to get the stored OTP and to compare it with the one input by the user. If there is a match, the OTP will be deleted using client.del(key) method to avoid any vulnerabilities and the user info isVerified will be updated to true.

Detecting Missed Verification

Sometimes, a user might register successfully but will fail to complete OTP verification. In such a case, the database will not record them as verified users yet, any attempt by such a user to log in will initiate a response with a notification for them to complete OTP verification. Also, a new code will be generated for them to use. An example code is below:

    ...
    export async function login(req, res) {

        const { email, password } = req.body;

        try {

            UserModel.findOne({ email })
                .then(user => {
                    bcrypt.compare(password, user.password)
                        .then(passwordCheck => {

                            if (!passwordCheck) return res.status(400).send({
                                error: "Don't have Password"
                            });

                            if (!user.isVerified) {
                                // Generate a random OTP using the otp-generator package
                                const otp = otpGenerator.generate(4, {
                                    lowerCaseAlphabets: false,
                                    upperCaseAlphabets: false,
                                    specialChars: false
                                });

                                // Store the OTP in Redis, with the user's email as the key
                                client.set(email, otp);

                                return res.status(400).send({
                                    error: "User is not verified, please finish verification using this OTP",
                                    OTP: otp
                                })
                            }

                            // create jwt token
                            const token = jwt.sign({
                                userId: user._id,
                                username: user.username
                            }, process.env.JWT_SECRET, { expiresIn: "24h" });

                            return res.status(200).send({
                                msg: "Login Successful...!",
                                user: user,
                                token
                            });

                        })
                        .catch(error => {
                            return res.status(400).send({ error: "Password does not Match" })
                        })
                })
                .catch(error => {
                    return res.status(404).send({ error: "Username not Found" });
                })

        } catch (error) {
            return res.status(500).send({ error });
        }
    }
    ...
Enter fullscreen mode Exit fullscreen mode

In the code block above, after the login credentials has been checked and ascertained correct, the isVerified parameter will also be checked to see if the user is verified. If they are not verified, a new OTP is generated for them with a notification to complete the verification using the verify endpoint that was done initially. Once they are verified, they can successfully log in to the system.

This is a short video demonstrating how the whole code put together works.

Conclusion

Implementing OTP-based authentication and verification is an effective way to strengthen the security of your applications. By using Redis as a data store and Node.js as the backend, you can easily build a robust OTP system.

Throughout this article, we went through step-by-step instructions and code examples to guide you in building this feature. With the knowledge gained from this tutorial, you are now equipped with adequate knowledge to implement OTP-based authentication and verification in your own projects, ensuring a safer user experience.

Resources

Top comments (0)