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 OTPsAn 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
Find User: Look up the user by email. If the user doesn’t exist, return an error message.
Check OTP Expiry: Verify if the OTP is still valid.
Check OTP: Ensure the provided OTP matches the one in the user’s record.
Verify Passwords: Confirm the new password and confirmation password match.
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!