How to Implement OTP Verification in Authentication System with Express.js and MongoDB

Sandeep Singh (Full Stack Dev.)
9 min readJun 14, 2023

In this tutorial, we will learn how to implement email OTP (One-Time Password) verification in user authentication using Express.js, MongoDB, and Nodemailer. OTP verification adds an extra layer of security to the user registration process by validating the user’s email address before allowing them to create an account.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Folder Structure
  5. Implementing the User Model
  6. Implementing the OTP Model
  7. Creating the MailSender Utility
  8. Implementing the OTP Verification Process
  9. Implementing the User Registration Process
  10. Conclusion
  11. FAQs (Frequently Asked Questions)

1. Introduction

In modern web applications, ensuring secure user authentication is crucial. Email OTP verification is a widely adopted method to verify the authenticity of user-provided email addresses. By implementing OTP verification, we can validate the user’s email address and prevent the registration of fake or invalid accounts.

In previous article we have successfully created a secure and scalable Authentication and Authorization system. This article is extension of that article. In this article we will add the OTP verification feature in it

2. Prerequisites

Before we begin, make sure you have the following prerequisites:

  • Node.js installed on your machine
  • MongoDB installed and running
  • Basic knowledge of JavaScript and Express.js
  • Basic understanding of MongoDB and Mongoose
  • Our Previous Article on “How to Build Secure & Scalable Authentication System” Because this is extension of that Article

3. Project Setup

Let’s start by setting up our project. Open your preferred terminal and follow these steps:

Step 1: Create a new directory for your project:

mkdir otp-verification-project

Step 2: Navigate to the project directory & Initialize a new Node.js project:

cd otp-verification-project
npm init -y

Step 3: Install the required dependencies:

npm install express mongoose nodemailer bcrypt otp-generator

4. Folder Structure

To maintain a well-organized project, let’s create a folder structure as follows:

- otp-verification-project
- controllers
- models
- routes
- utils

The controllers folder will contain the controller files responsible for handling different actions and business logic of our application. Create controller files such as authController.js, otpController.js, and userController.js based on your application's needs.

The models folder will store the Mongoose models for our application. Create model files such as otpModel.js and userModel.js to define the schemas and interact with the corresponding collections in the MongoDB database.

The routes folder will contain the route files that handle the routing and request handling for our application. Create route files such as authRoutes.js, otpRoutes.js, and userRoutes.js to define the API endpoints and connect them to the respective controller actions.

The utils folder will store utility files and helper functions used throughout our application. Create a file called mailSender.js in this folder to handle sending emails using Nodemailer.

5. Implementing the User Model

In the models/userModel.js file, we will define the Mongoose schema for the user model. The user model will have fields such as name, email, password, and role.

// models/userModel.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['Admin', 'Student', 'Visitor']
}
});
module.exports = mongoose.model('User', userSchema);

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

6. Implementing the OTP Model

In the models/otpModel.js file, we will define the Mongoose schema for the OTP model. The OTP model will have fields such as email, otp, and createdAt. Additionally, we will define a pre-save hook to automatically send the verification email when a new OTP document is created.

// models/otpModel.js
const mongoose = require('mongoose');
const mailSender = require('../utils/mailSender');

const otpSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
otp: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
expires: 60 * 5, // The document will be automatically deleted after 5 minutes of its creation time
},
});
// Define a function to send emails
async function sendVerificationEmail(email, otp) {
try {
const mailResponse = await mailSender(
email,
"Verification Email",
`<h1>Please confirm your OTP</h1>
<p>Here is your OTP code: ${otp}</p>`
);
console.log("Email sent successfully: ", mailResponse);
} catch (error) {
console.log("Error occurred while sending email: ", error);
throw error;
}
}
otpSchema.pre("save", async function (next) {
console.log("New document saved to the database");
// Only send an email when a new document is created
if (this.isNew) {
await sendVerificationEmail(this.email, this.otp);
}
next();
});
module.exports = mongoose.model("OTP", otpSchema);

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

7. Creating the MailSender Utility

In the utils/mailSender.js file, we will define the mailSender function that uses Nodemailer to send emails. This utility function will be used to send the verification email with the OTP code.

// utils/mailSender.js
const nodemailer = require('nodemailer');

const mailSender = async (email, title, body) => {
try {
// Create a Transporter to send emails
let transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
}
});
// Send emails to users
let info = await transporter.sendMail({
from: 'www.sandeepdev.me - Sandeep Singh',
to: email,
subject: title,
html: body,
});
console.log("Email info: ", info);
return info;
} catch (error) {
console.log(error.message);
}
};
module.exports = mailSender;

8. Implementing the OTP Verification Process

To implement the OTP verification process, we will create an otpController.js file in the controllers folder and an otpRoutes.js file in the routes folder.

In the otpController.js file, we will define the sendOTP function that generates and sends the OTP code to the user's email address for verification.

// controllers/otpController.js
const otpGenerator = require('otp-generator');
const OTP = require('../models/otpModel');
const User = require('../models/userModel');

exports.sendOTP = async (req, res) => {
try {
const { email } = req.body;
// Check if user is already present
const checkUserPresent = await User.findOne({ email });
// If user found with provided email
if (checkUserPresent) {
return res.status(401).json({
success: false,
message: 'User is already registered',
});
}
let otp = otpGenerator.generate(6, {
upperCaseAlphabets: false,
lowerCaseAlphabets: false,
specialChars: false,
});
let result = await OTP.findOne({ otp: otp });
while (result) {
otp = otpGenerator.generate(6, {
upperCaseAlphabets: false,
});
result = await OTP.findOne({ otp: otp });
}
const otpPayload = { email, otp };
const otpBody = await OTP.create(otpPayload);
res.status(200).json({
success: true,
message: 'OTP sent successfully',
otp,
});
} catch (error) {
console.log(error.message);
return res.status(500).json({ success: false, error: error.message });
}
};

In the otpRoutes.js file, we will define the route for sending the OTP code.

// routes/otpRoutes.js
const express = require('express');
const otpController = require('../controllers/otpController');
const router = express.Router();
router.post('/send-otp', otpController.sendOTP);
module.exports = router;

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

9. Implementing the User Registration Process

To implement the user registration process with OTP verification, we will create an authController.js file in the controllers folder and an authRoutes.js file in the routes folder.

In the authController.js file, we will define the signup function that handles the user registration process.

// controllers/authController.js
const bcrypt = require('bcrypt');
const User = require('../models/userModel');
const OTP = require('../models/otpModel');

exports.signup = async (req, res) => {
try {
const { name, email, password, role, otp } = req.body;
// Check if all details are provided
if (!name || !email || !password || !otp) {
return res.status(403).json({
success: false,
message: 'All fields are required',
});
}
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User already exists',
});
}
// Find the most recent OTP for the email
const response = await OTP.find({ email }).sort({ createdAt: -1 }).limit(1);
if (response.length === 0 || otp !== response[0].otp) {
return res.status(400).json({
success: false,
message: 'The OTP is not valid',
});
}
// Secure password
let hashedPassword;
try {
hashedPassword = await bcrypt.hash(password, 10);
} catch (error) {
return res.status(500).json({
success: false,
message: `Hashing password error for ${password}: ` + error.message,
});
}
const newUser = await User.create({
name,
email,
password: hashedPassword,
role,
});
return res.status(201).json({
success: true,
message: 'User registered successfully',
user: newUser,
});
} catch (error) {
console.log(error.message);
return res.status(500).json({ success: false, error: error.message });
}
};

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

In the authRoutes.js file, we will define the route for user registration.

// routes/authRoutes.js
const express = require('express');
const authController = require('../controllers/authController');
const router = express.Router();

router.post('/signup', authController.signup);
module.exports = router;

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Make sure to provide the correct SMTP details in the mailSender.js file. You can set these details as environment variables.

Finally, create a .env file in the project's root directory and add the following environment variables:

MAIL_HOST=your-smtp-host 
MAIL_USER=your-email-address
MAIL_PASS=your-email-password

Replace your-smtp-host, your-email-address, and your-email-password with the appropriate values for your SMTP server.

Testing the OTP Verification

Start your Node.js server by running the following command:

node app.js

Use an API testing tool like Postman to send a POST request to http://localhost:3000/api/auth/send-otp with the following JSON payload:

{   "email": "user@example.com" }

This will send an OTP to the provided email address.

To complete the user registration process, send another POST request to http://localhost:3000/api/auth/signup with the following JSON payload:

{   "name": "John Doe",   
"email": "user@example.com",
"password": "password123",
"role": "Student",
"otp": "<enter-the-received-otp>"
}

Replace <enter-the-received-otp> with the OTP you received in your email.

If the registration is successful, you should receive a JSON response with a success message and the newly created user object.

Congratulations! You have successfully implemented email OTP verification during user registration using Express.js, MongoDB, and Nodemailer.

Now use it with Frontend✅

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Let’s Also Connect👋 on Linkedin: https://www.linkedin.com/in/sandeep-singh55/

10. Conclusion

In this tutorial, we learned how to implement email OTP verification in user authentication using Express.js, MongoDB, and Nodemailer. By following the steps outlined in this tutorial, you can add an extra layer of security to your user registration process and ensure that only valid email addresses are used for account creation.

Remember to always handle errors and edge cases appropriately and further enhance the implementation based on your specific requirements.

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Did you find this article helpful in implementing OTP verification for user authentication? If you’re interested in a video tutorial where I guide you through the entire process, let me know in the comment section below!

Your feedback is crucial to me, and I’m eager to hear your thoughts. I’m here to help you succeed, so if there’s anything specific you’d like to see in a video tutorial or if you have any questions, don’t hesitate to reach out.

Stay tuned for more exciting content and tutorials to level up your development skills!

Happy coding!

11. FAQs (Frequently Asked Questions)

Q: What is OTP verification? A: OTP verification is a process where a user is required to enter a unique code sent to their registered email address or phone number to verify their identity.

Q: Why is OTP verification important?

A: OTP verification adds an additional layer of security to the authentication process, ensuring that only users with access to the registered email address or phone number can create accounts.

Q: Can I use SMS for OTP verification instead of email?

A: Yes, you can use SMS as an alternative to email for OTP verification. The implementation may vary, but the basic concept remains the same.

Q: Are there any limitations to the OTP code’s validity period?

A: The validity period of the OTP code depends on the application’s requirements. It should be long enough for users to receive and enter the code but short enough to maintain security. Common validity periods range from a few minutes to several hours.

Q: How can I improve the security of OTP verification?

A: To enhance security, you can implement measures such as rate limiting, throttling, and CAPTCHA verification to prevent abuse and brute-force attacks. Additionally, consider encrypting sensitive data and using secure communication protocols.

Q: Are there any npm packages available for OTP generation and email sending?

A: Yes, there are several npm packages available for OTP generation and email sending. Some popular ones include otp-generator, nodemailer, and bcrypt.

Q: Can I use a different database instead of MongoDB?

A: Yes, you can use a different database of your choice. Just make sure to install the appropriate database driver and modify the Mongoose configurations accordingly.

Q: How can I handle errors and edge cases in OTP verification?

A: It is essential to handle errors and edge cases effectively in OTP verification. Validate user inputs, handle database errors, and provide meaningful error messages to the users to guide them through the process.

Remember, the provided implementation is a starting point, and you should adapt it to fit your specific application requirements and security needs.

--

--

Sandeep Singh (Full Stack Dev.)

Fullstack Developer | MERN & Flutter | Passionate about Open Source | Engaged in Contributing & Collaborating for a Better Tech Community. 🚀