Building an Email Verification System with Node.js

Email verification is a crucial aspect of user authentication and security for any online platform. It ensures that the email addresses provided by users during registration are valid and accessible. In this blog post, we'll walk you through building an email verification system using Node.js. We'll cover the following topics:

  1. Introduction to Email Verification
  2. Setting Up Our Node.js Environment
  3. Building the Backend Logic
  4. Sending Verification Emails
  5. Handling Verification Links
  6. Testing Our System
  7. Best Practices and Security Tips

Let's get started!

Introduction to Email Verification

Email verification is a process where a system sends an email to the user to confirm their email address. This can help to:

  • Prevent spam accounts
  • Reduce the risk of phishing
  • Ensure the user can receive important notifications

The typical workflow is as follows:

  1. A user signs up and provides an email address.
  2. The system generates a unique verification token and stores it.
  3. An email with a verification link containing the token is sent to the user.
  4. The user clicks the link, and the system verifies the token.
  5. If the token is valid, the user's email address is marked as verified.

Setting Up Our Node.js Environment

First, let's set up a basic Node.js project.

mkdir email-verification-system
cd email-verification-system
npm init -y
npm install express nodemailer mongoose dotenv jsonwebtoken body-parser cors

Here’s a brief look at the packages we’ll use:

  • express: For building our web server.
  • nodemailer: For sending emails.
  • mongoose: For interacting with MongoDB.
  • dotenv: For managing environment variables.
  • jsonwebtoken: For creating and verifying tokens.
  • body-parser: For parsing incoming request bodies.
  • cors: For handling CORS.

Next, create a file named .env and add your configuration details:

PORT=3000
MONGO_URI=your_mongodb_connection_uri
SMTP_HOST=smtp.your-email-provider.com
SMTP_PORT=587
SMTP_USER=your_email@example.com
SMTP_PASS=your_email_password
JWT_SECRET=your_jwt_secret

Building the Backend Logic

Let's start by setting up our server and connecting to MongoDB.

// server.js
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
   .then(() => console.log('MongoDB connected'))
   .catch(err => console.error(err));

app.use(express.json());
app.use(require('body-parser').urlencoded({ extended: false }));

app.listen(PORT, () => {
   console.log(`Server running on port ${PORT}`);
});

Next, let's create our user model.

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

const UserSchema = new Schema({
   email: { type: String, required: true, unique: true },
   password: { type: String, required: true },
   isVerified: { type: Boolean, default: false },
   verificationToken: { type: String }
});

module.exports = mongoose.model('User', UserSchema);

Sending Verification Emails

Create a utility function for sending verification emails using Nodemailer.

// utils/sendEmail.js
const nodemailer = require('nodemailer');
const { google } = require('googleapis');
const OAuth2 = google.auth.OAuth2;

const sendEmail = async (to, subject, text) => {
   const oauth2Client = new OAuth2(
       process.env.CLIENT_ID, // ClientID
       process.env.CLIENT_SECRET, // Client Secret
       "https://developers.google.com/oauthplayground" // Redirect URL
   );

   oauth2Client.setCredentials({
       refresh_token: process.env.REFRESH_TOKEN
   });

   const accessToken = await oauth2Client.getAccessToken();

   const transporter = nodemailer.createTransport({
       service: 'gmail',
       auth: {
           type: 'OAuth2',
           user: process.env.EMAIL,
           clientId: process.env.CLIENT_ID,
           clientSecret: process.env.CLIENT_SECRET,
           refreshToken: process.env.REFRESH_TOKEN,
           accessToken: accessToken.token
       }
   });

   const mailOptions = {
       from: process.env.EMAIL,
       to,
       subject,
       text
   };

   await transporter.sendMail(mailOptions);
};

module.exports = sendEmail;

Handling User Registration

Now let's create the registration route.

// routes/auth.js
const express = require('express');
const User = require('../models/User');
const sendEmail = require('../utils/sendEmail');
const jwt = require('jsonwebtoken');

const router = express.Router();

router.post('/register', async (req, res) => {
   const { email, password } = req.body;

   try {
       const user = new User({ email, password });
       const token = jwt.sign({ email }, process.env.JWT_SECRET, { expiresIn: '1h' });
       user.verificationToken = token;
       await user.save();

       const verificationLink = `http://localhost:${process.env.PORT}/verify/${token}`;
       await sendEmail(email, 'Email Verification', `Click here to verify your email: ${verificationLink}`);

       res.status(200).send('Registration successful! Please check your email to verify your account.');
   } catch (error) {
       res.status(500).send('Error registering user.');
   }
});

module.exports = router;

Verifying the Token

Next, let's create the verification route.

// routes/verify.js
const express = require('express');
const User = require('../models/User');
const jwt = require('jsonwebtoken');

const router = express.Router();

router.get('/verify/:token', async (req, res) => {
   const token = req.params.token;

   try {
       const decoded = jwt.verify(token, process.env.JWT_SECRET);
       const email = decoded.email;

       const user = await User.findOne({ email, verificationToken: token });

       if (!user) return res.status(400).send('Invalid token.');

       user.isVerified = true;
       user.verificationToken = null;
       await user.save();

       res.status(200).send('Email verified successfully!');
   } catch (error) {
       res.status(500).send('Error verifying email.');
   }
});

module.exports = router;

Integrating Routes into the Server

Now, let's integrate our routes into the server.

// server.js
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const authRoutes = require('./routes/auth');
const verifyRoutes = require('./routes/verify');

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
   .then(() => console.log('MongoDB connected'))
   .catch(err => console.error(err));

app.use(express.json());
app.use(require('body-parser').urlencoded({ extended: false }));

app.use('/auth', authRoutes);
app.use('/verify', verifyRoutes);

app.listen(PORT, () => {
   console.log(`Server running on port ${PORT}`);
});

Testing Our System

  1. Register a New User: Use a tool like Postman to send a POST request to http://localhost:3000/auth/register with JSON data containing an email and password.
  2. Check Your Email: After registering, check your email for a verification link.
  3. Verify Email: Click the verification link. If everything is set up correctly, you should see a message stating that the email was verified successfully.

Best Practices and Security Tips

  1. Password Hashing: Always hash user passwords using a library like bcrypt before storing them in your database.
  2. Rate Limiting: Implement rate limiting on your routes to prevent abuse.
  3. Token Expiry: Ensure that your JWT tokens have an expiration time to minimize the risk of token misuse.
  4. Error Handling: Implement robust error handling to handle potential edge cases and failures gracefully.
  5. Email Validation: Validate email format and domain using regex and email validation libraries to improve accuracy.

Setting up an email verification system in Node.js provides an additional layer of security and improves user experience. By following this guide, you can ensure that only legitimate users gain access to your platform. Happy coding!