OTP Authentication
user profile avatar
Tech Wizard

Published on • 🕑5 min read

A Complete Guide to OTP Verification

1likes1

Blog views932

Listen to this blog

One major challenge associated with designing your authentication system flow is ensuring users enter valid email addresses. To avoid fake users, verifying that users have entered their email addresses is necessary. Verifying email can help in enhancing other security measures such as sending password reset links or notification emails. This blog explains how to implement an OTP verification system.

What is OTP Verification?

A One Time Password (OTP) is a simple and smart choice for enhancing security in applications. A one-time Password is a password or code that is automatically generated and sent to a digital device to allow a single login session or transaction. Also known as a One-time PIN, one-time authorization code (OTAC), One-Time-Pass Code, or dynamic password, OTP mitigates several risks of traditional static password-based authentication.

OTP plays an integral role in two-factor authentication systems and they are popular for validating new user accounts and resetting passwords. The user is required to input this code to confirm their identity and complete specific actions, such as accessing an account, conducting transactions, or retrieving sensitive information.

Implementing OTP Verification

For this tutorial, we are going to discuss how to implement OTP verification in a NextJS app using the Prisma ORM database and nodemailer. However, the tutorial could be applied to React and other databases ORM such as MongoDB and Resend.

1. Creating an OTP Table in the Database

If you are handling your own OTP verification without using an external provider, you will need to create a table to store the generated OTP codes. Codes must be associated with their emails, and also have an expiry time for security measures. 

//prisma database schema
model OTP {
  id        Int      @id @default(autoincrement())
  email     String
  code      String
  expiresAt DateTime
  createdAt DateTime @default(now())
}
//mongoDB
const mongoose = require('mongoose');
const OTPSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
  },
  code: {
    type: String,
    required: true,
  },
  expiresAt: {
    type: Date,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});
const OTP = mongoose.model('OTP', OTPSchema);
module.exports = OTP;

2. Generating OTP Codes

Since we now have a database table to store our OTP, we need to generate an OTP code when a user signs up or requests to reset a password. To do this, create a helper function that will be called when a user submits their email address.

//function to create OTP code in the database
export async function createOtpCode(email: string) {
  const otp = Math.floor(100000 + Math.random() * 900000).toString();
  try {
    const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes expiry
    await prisma.OTP.create({
      data: {
        email,
        code: otp,
        expiresAt: expiresAt,
      },
    });
    const response = await sendVerificationEmail(email, otp);
    return response.message;
  } catch (error) {
    console.error(error);
    throw new Error(error);
  }
}

Here, I have created a function that creates an OTP code in the database and calls the `sendVerificationEmail` function that sends the code to the user's email using nodemailer. The send email function looks as follows:

//emails/index.ts
"use server";
import nodemailer from "nodemailer";
import {
  otpTemplate,
  welcomeTemplate,
  adminPasswordResetTemplate,
  adminRegistrationTemplate,
} from "./template";
const transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASSWORD,
  },
});
const sender = `"TechTales" <admin@techtales.vercel.app>`;
export const sendEmail = async (emailOptions: {
  subject: string;
  from: string;
  to: string;
  html: any;
}) => {
  await transporter.sendMail(emailOptions);
};
export const sendVerificationEmail = async (email: string, otp: string) => {
  try {
    const response = await sendEmail({
      subject: `Your OTP Verification Code is ${otp}`,
      to: email,
      from: sender,
      html: otpTemplate(otp),
    });
    console.log("Email sent successfully");
    return { message: "Email sent successfully" };
  } catch (error) {
    console.error("Email delivery failed:", error);
    return { message: "Email delivery failed" };
  }
};

3. Verifying the OTP Code

The next step is for the users to get the OTP code and input it on the page to verify their email address. The verification must check whether the code is related to the email provided and it has not expired. Furthermore, to avoid database overload, I opted to delete the code once the verification was successful since the code could not be reused.

I implemented an API route `/auth/verify-token`, which uses a POST method to get the OTP and email from the user and verify the credentials.

//api/auth/verify-token
import prisma from "@/prisma/prisma";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest, res: NextResponse) {
  const { email, code } = await req.json();
  let otpEntry: any;
  try {
    otpEntry = await prisma.OTP.findFirst({
      where: {
        email: email,
        code: code,
      },
    });
    if (!otpEntry) {
      return NextResponse.json(
        { error: "Wrong OTP Provided" },
        { status: 404 }
      );
    }
    if (new Date() > otpEntry.expiresAt) {
      return NextResponse.json(
        { error: "The OTP code has expired" },
        { status: 401 }
      );
    }
    const response = NextResponse.json(
      { message: "OTP verified successfully" },
      { status: 200 }
    );
    return response;
  } catch (error) {
    return NextResponse.json({ error: error }, { status: 404 });
  } finally {
    if (otpEntry) {
      await prisma.OTP.delete({
        where: {
          id: otpEntry.id,
        },
      });
    }
    await prisma.$disconnect();
  }
}

Verifying the OTP is as simple as sending a post request to `/api/auth/verify-token` and providing the email and OTP code in the request body. You can save the email in the sessionStorage or append it to the URL when navigating the user to the verification page.

4. Resending the OTP

One final step is to ensure users can resend the OTP code in case it is expired or they did not receive the code. It is also important to inform users to check the spam folder in case the email gets flagged by the provider.

//lib/actions.ts
//function to resend OTP email
export async function resendOTPEmail(email: string) {
  const otp = Math.floor(100000 + Math.random() * 900000).toString();
  try {
    await prisma.OTP.deleteMany({
      where: {
        email: email,
      },
    });
    const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes expiry
    await prisma.OTP.create({
      data: {
        email,
        code: otp,
        expiresAt: expiresAt,
      },
    });
    const response = await sendVerificationEmail(email, otp);
    return response.message;
  } catch (error) {
    console.error(error);
    throw new Error("Error sending email!");
  }
}

A point to note is that when a user resends an OTP and their initial OTP has not expired, the find first method will find the initial OTP and thus the re-sent code will be invalid. To counter this problem, we first delete all the OTP codes associated with the user email, create a new code, and send it to the user email. This way, if the user enters their old code, it will be invalid.

Conclusion

In this blog, we saw how simple it is to implement an OTP verification system without relying on external third-party providers. OTPs can be used to reset forgotten passwords, complete a transaction, sign up or log into accounts, shield against spam and bots, and verify online purchases. OTP also helps reduce friction in the customer journey. For example, lost/forgotten passwords can lead to dropoffs, and OTPs can help users quickly regain access to their accounts.

Like what you see? Share with a Friend

1 Comments

1 Likes

Comments (1)

sort comments

Before you comment please read our community guidelines


Please Login or Register to comment

user profile avatar

diamond degesh

Published on

This makes OTP verification seem super simple to implement 👏👏