Learn how to implement a Next.js login page with email OTP and SMS OTP, in this tutorial.
Vincent
Created: December 17, 2024
Updated: February 17, 2025
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
Next.js is a powerful framework that allows developers to build fast and user-friendly web applications. One of the most critical aspects of any web application is user authentication.
In this guide, we'll walk you through the process of implementing a login page in Next.js, with One-Time Password (OTP) authentication via SMS or Email.
If you want to see the complete code and check out other authenciation methods, use our Next.js login page repository on GitHub. There you will find information regarding:
Recent Articles
♟️
Testing Passkey Implementations (Enterprise Passkeys Guide 5)
♟️
Integrating Passkeys into Enterprise Stack (Enterprise Passkeys Guide 4)
♟️
Product, Design & Strategy Development (Enterprise Passkeys Guide 3)
🔑
Use Windows 11 Without Windows Hello / Microsoft Account
♟️
Stakeholder Engagement (Enterprise Passkeys Guide 2)
Before we jump into the One-Time Password (OTP) authentication, we need to perform some general project setup steps.
To follow this guide, we require some basic understanding of
Open your terminal and run the following command to create a new Next.js project:
npx create-next-app@latest nextjs-auth-methods
In the installation guide steps, we select the following:
Navigate to your project directory:
cd nextjs-login
To verify that your Next.js project is set up correctly, start the development server:
npm run dev
Open your browser and navigate to http://localhost:3000
. You should see the default Next.js welcome page.
Create a .env.local
file in the root of your project to store environment variables. Add your variables here:
MONGODB_URI=your_database_connection_string GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret TWILIO_ACCOUNT_SID=your_twilio_account_sid TWILIO_AUTH_TOKEN=your_twilio_auth_token TWILIO_PHONE_NUMBER=your_twilio_phone_number
Passwordless authentication eliminates traditional passwords by using a unique, time-sensitive OTP sent to a user's email or phone. This enhances security by reducing the risk of breaches, improves user experience by removing the need to remember passwords, and cuts support costs by minimizing password-related issues.
OTP authentication is a widely used security mechanism for verifying user identity by generating a unique passcode valid for a one-time usage. In this section, we will guide you through implementing One-Time Passcode.
The following steps are required to implement OTP-based authentication:
Good to Know: Understanding the OTP Flow
Implementing OTP authentication in your application involves several key steps. To ensure you have a clear understanding of this process, let's break down each step:
In this section, we'll analyze the specific files and structure needed for OTP-based authentication via email and SMS. Here's an overview of the relevant directory structure and files:
Key Files and Their Roles:
src/app/otp/page.tsx
: Contains the user interface for OTP authentication, including form for entering contact information and OTPs.src/models/Otp.ts
: Defines the MongoDB schema for storing OTPs, including fields for email, phone number, OTP, and creation date.src/pages/api/auth/otp/generate.ts
: API route to handle generating OTPs, including generating, hashing, storing OTPs, and sending them via email or SMS.src/pages/api/auth/otp/verify.ts
: API route to handle verifying OTPs, including retrieving, comparing, validating, and deleting OTPs from the database.To set up OTP authentication, you need to install the following dependencies:
Please note that you will need to create a Twilio account and obtain the relevant credentials (Account SID, Auth Token, and Twilio phone number) and add them to the environment variables file.
You can install these dependencies using the following command:
npm install nodemailer twilio
To store OTPs, we need to set up a MongoDB schema. In this section, we will guide you through the creation of an OTP model in MongoDB.
models/Otp.ts
in your project directory.email
: An optional string field to store the user's email.phoneNumber
: An optional string field to store the user's phone number.otp
: A required string field to store the OTP.createdAt
: A date field with a default value of the current date. This field has an index with an expiration time of 10 minutes, meaning the document will be automatically deleted after 10 minutes.Otp
is a model created from the OtpSchema
. If the model already exists in mongoose.models, it uses the existing model; otherwise, it creates a new one.Here is a high-level description of the otps
collection relevant for the OTP-based authentication method:
Here's the complete code for the OTP model:
import mongoose, { Document, Schema } from "mongoose"; // Interface defining the OTP document structure export interface IOtp extends Document { email?: string; // Optional email field phoneNumber?: string; // Optional phone number field otp: string; // OTP value createdAt: Date; // Creation timestamp } // Define the OTP schema const OtpSchema: Schema<IOtp> = new Schema({ email: { type: String }, // Email field phoneNumber: { type: String }, // Phone number field otp: { type: String, required: true }, // OTP field (required) createdAt: { type: Date, default: Date.now, index: { expires: "10m" } }, // Creation timestamp with 10-minute expiry }); // Ensure at least one of email or phoneNumber is provided OtpSchema.path("email").validate(function (value) { return this.email || this.phoneNumber; }, "Email or phone number is required"); // Create or reuse the OTP model const Otp = mongoose.models.Otp || mongoose.model<IOtp>("Otp", OtpSchema); export default Otp;
To generate OTPs and send them via email or SMS, we need to implement an API route. In this section, we will guide you through creating an API route that handles OTP generation and delivery.
pages/api/auth/otp/generate.ts
in your project directory. bcrypt
to hash the OTP for security.Nodemailer
to send the OTP via email.For testing purposes, we use an Ethereal email account to preview the email link in the console (this is implemented in the Generate API route). After clicking the Generate OTP
button, check the console for the Email Preview URL. Copy and paste this link into your browser to view your OTP code.
Copy and paste this link into your browser to view your OTP code.
- Send OTP via SMS: After setting up your Twilio account and adding the necessary credentials (Account SID, Auth Token, and Twilio phone number), the OTP will be sent to the entered phone number using Twilio's API.
This API route handles generating, hashing, and storing OTPs, and sends them to users via email or SMS based on the specified delivery method. Combine all the steps into the Generate API route:
import type { NextApiRequest, NextApiResponse } from "next"; import nodemailer from "nodemailer"; import bcrypt from "bcryptjs"; import Otp from "@/models/Otp"; import dbConnect from "@/lib/mongodb"; import twilio from "twilio"; // Function to generate a 6-digit OTP const generateOtp = () => Math.floor(100000 + Math.random() * 900000).toString(); // Function to create an Ethereal email account for testing purposes const createEtherealAccount = async () => { // Create a test account using Ethereal let testAccount = await nodemailer.createTestAccount(); // Configure the transporter using the test account return nodemailer.createTransport({ host: testAccount.smtp.host, port: testAccount.smtp.port, secure: testAccount.smtp.secure, auth: { user: testAccount.user, pass: testAccount.pass, }, }); }; // Function to send an email with the OTP const sendEmail = async (email: string, otp: string) => { // Create the transporter for sending the email let transporter = await createEtherealAccount(); // Define the email options const mailOptions = { from: "[app@test.com](mailto:app@test.com)", // Sender address to: email, // Recipient address subject: "Your OTP Code", // Subject line text: `Your OTP code is ${otp}`, // Plain text body }; // Send the email and log the preview URL let info = await transporter.sendMail(mailOptions); console.log("Email Preview URL: %s", nodemailer.getTestMessageUrl(info)); }; // Function to send an SMS with the OTP const sendSms = async (phoneNumber: string, otp: string) => { // Create a Twilio client const client = twilio( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN ); // Send the OTP via SMS await client.messages.create({ body: `Your OTP code is ${otp}`, // Message body from: process.env.TWILIO_PHONE_NUMBER, // Sender phone number to: phoneNumber, // Recipient phone number }); }; // API route handler for generating and sending OTP export default async function handler( req: NextApiRequest, res: NextApiResponse ) { // Connect to the MongoDB database await dbConnect(); // Extract email, phone number, and delivery method from the request body const { email, phoneNumber, deliveryMethod } = req.body; // Validate input if (!email && !phoneNumber) { return res .status(400) .json({ message: "Email or phone number is required" }); } if (!deliveryMethod || !["email", "sms"].includes(deliveryMethod)) { return res .status(400) .json({ message: "Valid delivery method is required" }); } // Generate a 6-digit OTP and hash it const otp = generateOtp(); const hashedOtp = bcrypt.hashSync(otp, 10); // Create a new OTP record in the database const newOtp = new Otp({ email: deliveryMethod === "email" ? email : undefined, // Store email if the delivery method is email phoneNumber: deliveryMethod === "sms" ? phoneNumber : undefined, // Store phone number if the delivery method is SMS otp: hashedOtp, // Store the hashed OTP }); await newOtp.save(); // Send the OTP via the selected delivery method if (deliveryMethod === "email" && email) { await sendEmail(email, otp); // Send OTP via email } else if (deliveryMethod === "sms" && phoneNumber) { await sendSms(phoneNumber, otp); // Send OTP via SMS } else { return re.status(400).json({ message: "Invalid delivery method or missing contact information", }); } // Respond with a success message return res.status(200).json({ message: "OTP sent successfully" }); }
To verify OTPs sent via email or SMS, we need to implement an API route. In this section, we will guide you through creating an API route that handles OTP verification.
pages/api/auth/otp/verify.ts
in your project directory.Combine all the steps into the Verify API route. Here’s the complete code:
import type { NextApiRequest, NextApiResponse } from "next"; import bcrypt from "bcryptjs"; import dbConnect from "@/lib/mongodb"; import Otp from "@/models/Otp"; // API route handler for verifying OTP export default async function handler( req: NextApiRequest, res: NextApiResponse ) { // Connect to the MongoDB database await dbConnect(); // Extract email, phone number, and OTP from the request body const { email, phoneNumber, otp } = req.body; // Validate input: Ensure either email or phone number, and OTP are provided if ((!email && !phoneNumber) || !otp) { return res .status(400) .json({ message: "Email or phone number and OTP are required" }); } // Find OTP record by email or phone number const otpRecord = email ? await Otp.findOne({ email }) // Find by email if email is provided : await Otp.findOne({ phoneNumber }); // Find by phone number if phone number is provided // Check if OTP record exists if (!otpRecord) { return res.status(400).json({ message: "OTP not found or expired" }); } // Compare provided OTP with the hashed OTP in the database const isMatch = bcrypt.compareSync(otp, otpRecord.otp); // If OTP does not match, return an error if (!isMatch) { return res.status(400).json({ message: "Invalid OTP" }); } // Delete the OTP record after successful verification if (email) { await Otp.deleteOne({ email }); } else if (phoneNumber) { await Otp.deleteOne({ phoneNumber }); } // Respond with a success message return res.status(200).json({ message: "OTP verified successfully" }); }
To create the user interface for OTP authentication via email and SMS, we need to implement a component that handles OTP generation and verification. In this section, we will guide you through creating a user-friendly interface for this purpose.
src/app/otp/page.tsx
in your project directory.validateContactInfo
: Is a function to validate the contact information based on the selected delivery method.handleGenerateOTP
: Is a function to handle OTP generation. It validates the email and phone number and sends a request to generate an OTP.handleVerifyOtp
: A function to handle OTP verification. It sends a request to verify the OTP.Here's the complete code for the OTP auth component:
"use client"; import React, { useState } from "react"; const OtpPage: React.FC = () => { const [contactInfo, setContactInfo] = useState(""); const [deliveryMethod, setDeliveryMethod] = useState("email"); const [otp, setOtp] = useState(""); const [message, setMessage] = useState(""); const [isOtpSent, setIsOtpSent] = useState(false); const [isOtpVerified, setIsOtpVerified] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const validateContactInfo = (info: string): boolean => { if (deliveryMethod === "email") { // Regular expression for email validation const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(info); } else if (deliveryMethod === "sms") { // Regular expression for phone number validation const re = /^\+?[1-9]\d{1,14}$/; return re.test(info); } return false; }; const handleGenerateOtp = async () => { if (!contactInfo) { setMessage("Contact information is required"); setIsSuccess(false); return; } if (!validateContactInfo(contactInfo)) { setMessage( deliveryMethod === "email" ? "Invalid email format" : "Invalid phone number format" ); setIsSuccess(false); return; } const res = await fetch("/api/auth/otp/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: deliveryMethod === "email" ? contactInfo : undefined, phoneNumber: deliveryMethod === "sms" ? contactInfo : undefined, deliveryMethod, }), }); const result = await res.json(); setMessage(result.message); if (res.status === 200) { setIsOtpSent(true); setIsSuccess(true); } else { setIsSuccess(false); } }; const handleVerifyOtp = async () => { const res = await fetch("/api/auth/otp/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: deliveryMethod === "email" ? contactInfo : undefined, phoneNumber: deliveryMethod === "sms" ? contactInfo : undefined, otp, }), }); const result = await res.json(); setMessage(result.message); if (res.status === 200) { setIsOtpVerified(true); setIsSuccess(true); } else { setIsSuccess(false); } }; return ( <div className="flex items-center justify-center min-h-screen bg-gray-100"> <div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full"> <h1 className="text-2xl font-semibold mb-6 text-center"> OTP Authentication </h1> {!isOtpSent ? ( <> <div className="mb-4"> <label className="block text-gray-800">OTP Delivery Method</label> <select value={deliveryMethod} onChange={(e) => setDeliveryMethod(e.target.value)} className="w-full p-3 border border-gray-300 rounded" > <option value="email">Email</option> <option value="sms">SMS</option> </select> </div> <input type={deliveryMethod === "email" ? "email" : "text"} placeholder={ deliveryMethod === "email" ? "Enter your email" : "Enter your phone number" } value={contactInfo} onChange={(e) => setContactInfo(e.target.value)} required className="w-full p-3 border border-gray-300 rounded mb-4" /> <button onClick={handleGenerateOtp} className="w-full bg-blue-500 text-white p-3 rounded hover:bg-blue-600" > Generate OTP </button> </> ) : ( <div> {!isOtpVerified ? ( <div> <input type="text" placeholder="Enter OTP" value={otp} onChange={(e) => setOtp(e.target.value)} required className="w-full p-3 border border-gray-300 rounded mb-4" /> <button onClick={handleVerifyOtp} className="w-full bg-blue-500 text-white p-3 rounded" > Verify OTP </button> </div> ) : ( <div className="text-center"> <h2 className="text-xl font-semibold">Welcome</h2> </div> )} </div> )} {message && ( <p className={`text-center mt-4 ${isSuccess ? "text-green-500" : "text-red-500"}`}> {message} </p> )} </div> </div> ); }; export default OtpPage;
To ensure the OTP authentication works correctly via both email and SMS, we will conduct some testing. This section covers the steps and routes involved in testing OTP authentication, along with screenshots for better clarity.
Route: http://localhost:3000/otp
Steps:
http://localhost:3000/otp
Generate OTP
button.- Check the console for the Ethereal email preview URL to view the OTP (since we're using Ethereal for testing).
- Please copy and paste it into your browser to get the OTP code.
Verify OTP
button.- Check for a success message indicating that the OTP was verified successfully.
Route:http://localhost:3000/otp
Steps:
http://localhost:3000/otp
SMS
from the dropdown menu for OTP delivery method.
4. Generate OTP:
Click the
Generate OTP
button. Then, check your phone for the SMS containing the OTP fron the Twilio trial account.
After the OTP is generated, an OTP record is saved to the otps
collection in the MongoDB database.
Verify OTP
button. Then, check for a success message indicating that the OTP was verified successfully.You've successfully implemented an OTP-based authentication system with Next.js and Tailwind CSS.
Event though we just looked at how to roll out OTP authencication, which is not the safest auth method, building a secure and user-friendly authentication system is crucial to protect user data and provide a great first impression of your app. Authentication is often the first interaction a user has with your product and the first thing they will complain about if something goes wrong. Besides the detailed steps laid out above, we also have some additional best practices for Next.js authentication and login pages:
By following these practices, you'll keep your application robust against common threats and ensure a safe environment for your users. Secure authentication not only protects your users but also builds trust and credibility for your application.
In this Next.js login page guide, we explored various authentication methods to secure your Next.js applications. Here’s a recap of what we covered:
OTP via email or SMS: We implemented OTP-based authentication by generating and verifying OTPs via email and SMS, setting up MongoDB to store OTPs, and creating front-end components to handle user interactions.
Choosing the right authentication method for your application depends on various factors, including security requirements, user convenience, and the nature of your application. Each method we covered has its strengths and can be used alone or in combination to provide a robust authentication system. Experiment with different methods, gather user feedback, and iterate on your implementation to achieve the best balance for your specific needs.
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.