Implementing Email Verification in Node.js with JWT and Nodemailer

November 2, 2024 (2w ago)

Implementing Email Verification in Node.js with JWT and Nodemailer

Email verification is essential for securing the registration process in web applications. It ensures that users are genuine and prevents spam or fraudulent sign-ups. By combining JWT (JSON Web Token) with Nodemailer, you can implement email verification efficiently in a Node.js application. This guide will walk you through setting up email verification, covering JWT generation for verification links, sending emails, and verifying users.


Overview of the Email Verification Process

The email verification process typically involves the following steps:

  1. User Registration: The user registers with their email.
  2. Verification Email: The server sends an email containing a verification link with a unique token.
  3. Email Verification: When the user clicks the link, the server validates the token and activates the user’s account.

By using JWT for the verification token, you ensure the process is stateless and secure. JWT’s expiration feature also allows you to set a time limit for verification.


Setting Up the Project

We’ll build upon a basic Node.js, Express, and Mongoose setup with jsonwebtoken for JWT and nodemailer for sending emails.

Step 1: Initialize the Project

If you’re starting fresh, initialize a new project and install the necessary dependencies.

mkdir email-verification
cd email-verification
npm init -y
npm install express mongoose jsonwebtoken bcryptjs nodemailer dotenv

Configuring Environment Variables

Create a .env file in the root directory to store configuration details, including your JWT secret, MongoDB URI, and email credentials.

MONGODB_URI=mongodb://localhost:27017/email_verification
JWT_SECRET=your_jwt_secret
PORT=5000
EMAIL_USER=your_email@example.com
EMAIL_PASS=your_email_password

For testing, you can use Ethereal Email to set up temporary email credentials.


Defining the User Model

Create a User model in the models folder with Mongoose, including fields for email verification.

models/User.js

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
 
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  isVerified: { type: Boolean, default: false }, // Verification status
});
 
// Hash password before saving
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});
 
module.exports = mongoose.model("User", userSchema);

In this schema:


Setting Up Nodemailer for Sending Emails

To send verification emails, configure Nodemailer with your email credentials.

config/email.js

const nodemailer = require("nodemailer");
 
const transporter = nodemailer.createTransport({
  service: "gmail", // Use the service for your email provider
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
});
 
module.exports = transporter;

Note: For Gmail, you may need to enable "Less secure app access" in your Google Account settings or use an app password if you have 2-step verification enabled.


Setting Up the Registration and Email Verification Routes

We’ll create a registration route that sends a verification email and a verification route that verifies the token in the email link.

Step 1: Creating the Registration Route

In the auth.js route file, add a registration route that generates a JWT token for email verification.

routes/auth.js

const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const transporter = require("../config/email");
 
const router = express.Router();
 
// Generate email verification token
const generateVerificationToken = (user) => {
  return jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
 
// Registration route
router.post("/register", async (req, res) => {
  try {
    const { username, email, password } = req.body;
    const existingUser = await User.findOne({ email });
    if (existingUser) return res.status(400).json({ message: "Email already registered" });
 
    const user = new User({ username, email, password });
    await user.save();
 
    // Generate verification token and send email
    const token = generateVerificationToken(user);
    const verificationLink = `http://localhost:${process.env.PORT}/auth/verify-email?token=${token}`;
 
    // Send verification email
    await transporter.sendMail({
      from: process.env.EMAIL_USER,
      to: user.email,
      subject: "Verify Your Email",
      html: `<p>Click <a href="${verificationLink}">here</a> to verify your email.</p>`,
    });
 
    res.status(201).json({ message: "Registration successful. Check your email for verification link." });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
module.exports = router;

In this code:

  1. The generateVerificationToken function creates a JWT with a 1-hour expiration.
  2. A verification email containing the token is sent to the user’s email address with a verification link.

Step 2: Creating the Email Verification Route

Add a route to verify the email using the token in the link.

routes/auth.js

// Email verification route
router.get("/verify-email", async (req, res) => {
  const { token } = req.query;
  if (!token) return res.status(400).json({ message: "Token is required" });
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id);
    if (!user) return res.status(404).json({ message: "User not found" });
 
    user.isVerified = true; // Mark user as verified
    await user.save();
 
    res.status(200).json({ message: "Email verified successfully. You can now log in." });
  } catch (error) {
    res.status(400).json({ message: "Invalid or expired token" });
  }
});

In this route:

  1. The token from the query string is verified.
  2. If the token is valid, the isVerified field is set to true, activating the user’s account.

Protecting Routes for Verified Users Only

To prevent unverified users from accessing certain resources, create middleware that checks the isVerified status.

middleware/verifyMiddleware.js

const verifyMiddleware = (req, res, next) => {
  if (!req.user.isVerified) {
    return res.status(403).json({ message: "Email verification required to access this resource." });
  }
  next();
};
 
module.exports = verifyMiddleware;

This middleware can be used alongside authentication middleware to restrict access to verified users.


Using the Verification Middleware

You can apply the verifyMiddleware to routes that only verified users should access.

routes/profile.js

const express = require("express");
const authMiddleware = require("../middleware/authMiddleware");
const verifyMiddleware = require("../middleware/verifyMiddleware");
 
const router = express.Router();
 
router.get("/profile", authMiddleware, verifyMiddleware, (req, res) => {
  res.status(200).json({ message: "Welcome to your profile!" });
});
 
module.exports = router;

In this example, both authMiddleware (for authentication) and verifyMiddleware (for email verification) are used to secure the route.


Testing the Email Verification Process

  1. Register a new user through the /register endpoint.
  2. Check the Email: Open the email inbox and find the verification email.
  3. Click the Verification Link: Click the link in the email or paste it into your browser to verify the email.
  4. Access Verified-Only Routes: Once verified, the user can access restricted routes.

Best Practices for Email Verification

  1. Set Expiration for Verification Tokens: Use a short expiration time to limit token misuse.
  2. Secure Email Link: Use HTTPS in production for secure token transmission.
  3. Resend Verification Email: Provide an option to resend the verification email if the user didn’t receive it.
  4. Prevent Duplicate Accounts: Check if the email is already registered before sending the verification email.
  5. Limit Verification Attempts: Implement rate limiting or temporary lockout after multiple failed verification attempts.

Conclusion

Implementing email verification in Node.js using JWT and Nodemailer enhances the security and authenticity of user registrations. By requiring

users to verify their email before accessing sensitive resources, you protect your application from spam accounts and ensure valid user participation.

With this setup, you can provide a secure and user-friendly registration process, allowing users to verify their accounts and enjoy full access to your application. Integrate these steps into your project to secure the sign-up process and enhance your app’s reliability.