OTP based password reset functionality in Node.js and Express.js

OTP based password reset functionality in Node.js and Express.js

·

4 min read

Password reset functionality is crucial for any web application that handles user authentication. Today, we will walk through the implementation of an OTP (One-Time Password) based password reset system using Node.js. This feature ensures that only users with access to their registered email can reset their passwords, adding an extra layer of security.

Prerequisites

Before we start, ensure you have the following set up:

  • Node.js installed

  • Express framework

  • MongoDB for user data storage

  • crypto for generating OTPs

  • An email service for sending OTP notifications

I am using EJS to render my templates, you can integrate the templates with a framework of your choice. The scope of the article is limited to the backend part only.

1. Sending the OTP

Our first task is to create a function that generates a random OTP, saves it to the user’s record along with an expiry time, and sends it to the user’s email.

I have defined my User schema as following -

const userSchema = new mongoose.Schema({
    name : {
        type : String , 
        required:true,
    },

    email : {
        type: String,
        required : true,
        unique : true
    },

    password : {
        type:String, 
        required : true
    },

    resetOtp : {
        type:String,
    },
    otpExpiry : {
        type:Date
    }

});

Storing the OTP in the database allows you to compare the OTP that the user has entered and also check its expiry.

Now the controller to handle sending the OTP to user and saving it to DB should look like this -

async function handleSendOtp(req,res) {
    const { email } = req.body;

    const otp = crypto.randomInt(100000,999999);
    const user = await User.findOne({email});

    if (!user) {
        return res.render("resetPassword", {
            userNotFoundMsg : "User not found!"
        });
    }

    user.resetOtp = otp;
    user.otpExpiry = Date.now() + 5 * 60 * 1000; //5 min expiry
    await user.save();

    await sendOtpNotification(email,user);

    return res.render("resetPassword", {
        otpSuccessfulMsg : "OTP sent to your mail",
        email: email
    });
}

Explanation

  • Generate OTP: We use crypto.randomInt to generate a secure random OTP.

  • Find User: Look up the user by email. If the user doesn’t exist, return an error message.

  • Save OTP: Attach the OTP and its expiry time to the user’s record and save it.

  • Send Email: Use an email service to send the OTP to the user's email address.

2. Verifying the OTP and Resetting the Password

Now, we need a function to verify the OTP and reset the user's password.

async function handleVerifyOtpAndResetPassword(req, res) {
    const { email, otp, newPassword, confirmNewPassword } = req.body;

    try {

        const user = await User.findOne({ email });

        if (!user) {
            return res.render("resetPassword", {
                userNotFoundMsg : "User not found!"
            })
        }

        // Check if OTP is expired
        if (Date.now() > user.otpExpiry) {
            return res.render("resetPassword", {
                email: email,
                otpExpiredMSg : "OTP has expired"
            })
        }

        // Check if OTP is correct
        if (user.resetOtp !== otp) {
            return res.render("resetPassword", {
                email : email,
                otpIncorrectMsg : "Incorrect OTP"
            })
        }

        // Check if new password and confirm password match
        if (newPassword !== confirmNewPassword) {
            return res.render("resetPassword", {
                email : email,
                passwordNotMatchMsg : "Passwrods do not match"
            })
        }

        // Update the user's password and clearing the otp from DB
        user.password = newPassword;
        user.resetOtp = undefined;
        user.otpExpiry = undefined; 

        await user.save();
        res.render("resetPassword", {
            passwordResetSuccessMsg : "Password reset successfully. You can login now."
        });
    } catch (error) {
        console.log(error);
        res.render("resetPassword", {
            passwordResetErrorMsg : "Error resetting password."
        });
    }
}

Remember to not store passwords directly to the database. I have a pre save method for my User schema that hashes the password and then stores it in the DB.

Explanation

  1. Find User: Look up the user by email. If the user doesn’t exist, return an error message.

  2. Check OTP Expiry: Verify if the OTP is still valid.

  3. Check OTP: Ensure the provided OTP matches the one in the user’s record.

  4. Verify Passwords: Confirm the new password and confirmation password match.

  5. Update User Record: Save the new password and clear the OTP fields.

3. Email service

You can implement your own SMTP server or similar technologies or use the nodemailer library that does the same under the hood. A sample code would look like this -

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
    service: 'gmail',
    auth: {
        user: process.env.MAIL_ID,
        pass: process.env.MAIL_PASS
    }
});

async function sendOtpNotification(userMail, user) {
    const mailOptions = {
        from: process.env.MAIL_ID,
        to: userMail,
        subject: 'OTP to Reset password',
        text: `Your OTP for password reset is: ${user.resetOtp}. 
        Do not share your OTP with anyone else. Validity 5 Mins`
    };
    try {
        await transporter.sendMail(mailOptions);
        console.log('OTP email sent to:', userMail);
    } catch (error) {
        console.error('Error sending OTP email:', error);
    }
}

You can setup your MAIL_ID from which you want to send the OTP and the MAIL_PASS in the .env file.

By following these steps, you've built a robust and secure password reset system that can be a valuable addition to any web application. Happy coding!