When building modern web applications, One-Time Passwords (OTPs) are critical in two-factor authentication and user verification workflows. This guide will teach us how to generate, send, and verify OTPs using Next.js and a mock email service. By the end, you’ll understand how OTPs can enhance security and improve user experience.
What is an OTP?
An OTP (One-Time Password) is a temporary password used for a single login or transaction. It expires after a short time, providing additional security beyond regular passwords.
Common use cases:
- Account Registration
- Password Resets
- Two-factor Authentication (2FA)
How OTP Generation Works
The general workflow for OTP generation and verification is:
- Generate a random OTP.
- Send the OTP to the user via email (or SMS).
- Verify the OTP when the user submits it.
- Activate or proceed with the next action only if OTP is correct.
Project Setup
To demonstrate OTP generation and verification, we’ll build:
- A backend API route to generate OTPs.
- A Redis store to temporarily save OTPs.
- An API route to verify OTPs.
Prerequisites:
1- Next.js project initialized:
npx create-next-app@latest otp-nextjs-demo
cd otp-nextjs-demo
2- Install necessary dependencies:
npm install redis nodemailer crypto
Step 1: Set Up Redis
We’ll use Redis to temporarily store the OTP. For this guide, we’ll use Upstash, a serverless Redis service.
- Create a free Upstash Redis instance at https://upstash.com.
- Copy your Redis connection string and set it in your .env.local:
REDIS_URL=<your-upstash-redis-url>
EMAIL_USER=<your-email>
EMAIL_PASS=<your-email-password>
Step 2: Create the OTP Generation API
Next, let’s create an API route to generate and send OTPs.
// /pages/api/generateOTP.ts
import { NextApiRequest, NextApiResponse } from 'next';
import Redis from 'ioredis';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
// Initialize Redis
const redis = new Redis(process.env.REDIS_URL!);
// Configure Nodemailer for sending emails
const transporter = nodemailer.createTransport({
service: 'gmail', // Use your preferred email service
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: 'Email is required' });
}
try {
// Generate a 6-digit OTP
const otp = crypto.randomInt(100000, 999999).toString();
// Store OTP in Redis for 5 minutes
await redis.setex(email, 300, otp);
// Send OTP to user's email
await transporter.sendMail({
from: process.env.EMAIL_USER,
to: email,
subject: 'Your OTP Code',
text: `Your OTP is ${otp}. It will expire in 5 minutes.`,
});
return res.status(200).json({ message: 'OTP sent to email' });
} catch (error) {
console.error('Error generating OTP:', error);
return res.status(500).json({ message: 'Error generating OTP' });
}
}
How This Code Works:
- Redis Setup: We connect to Redis to store OTPs with a 5-minute expiration.
- OTP Generation: We use the crypto module to generate a random 6-digit OTP.
- Sending OTP via Email: Nodemailer is used to send the OTP to the user’s email.
Step 3: Verify OTP API
Now, let’s create the OTP verification endpoint.
// /pages/api/verifyOTP.ts
import { NextApiRequest, NextApiResponse } from 'next';
import Redis from 'ioredis';
// Initialize Redis
const redis = new Redis(process.env.REDIS_URL!);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { email, otp } = req.body;
if (!email || !otp) {
return res.status(400).json({ message: 'Email and OTP are required' });
}
try {
// Retrieve OTP from Redis
const storedOtp = await redis.get(email);
if (!storedOtp) {
return res.status(400).json({ message: 'OTP expired or not found' });
}
if (storedOtp !== otp) {
return res.status(400).json({ message: 'Invalid OTP' });
}
// OTP is valid; proceed with registration or action
await redis.del(email); // Remove OTP after successful verification
return res.status(200).json({ message: 'OTP verified successfully' });
} catch (error) {
console.error('Error verifying OTP:', error);
return res.status(500).json({ message: 'Error verifying OTP' });
}
}
How This Code Works:
- OTP Retrieval: We retrieve the OTP from Redis using the provided email.
- OTP Validation: If the OTP matches, we delete it from Redis and return a successful response. If not, we return an error message.
Step 4: Frontend Form to Request OTP
Create a simple form to request an OTP.
// /pages/index.tsx
'use client';
import { useState } from 'react';
export default function Home() {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/generateOTP', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
setMessage(data.message);
};
return (
<div>
<h1>Request OTP</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Get OTP</button>
</form>
<p>{message}</p>
</div>
);
}
Step 5: Frontend Form to Verify OTP
Create another form to verify the OTP.
// /pages/verify.tsx
'use client';
import { useState } from 'react';
export default function Verify() {
const [email, setEmail] = useState('');
const [otp, setOtp] = useState('');
const [message, setMessage] = useState('');
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/verifyOTP', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otp }),
});
const data = await res.json();
setMessage(data.message);
};
return (
<div>
<h1>Verify OTP</h1>
<form onSubmit={handleVerify}>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="text"
placeholder="Enter OTP"
value={otp}
onChange={(e) => setOtp(e.target.value)}
required
/>
<button type="submit">Verify OTP</button>
</form>
<p>{message}</p>
</div>
);
}
Conclusion
In this guide, we built a complete OTP generation and verification flow in Next.js. We used:
- Redis to store OTPs temporarily.
- Nodemailer to send OTPs via email.
- API routes to generate and verify OTPs.
This workflow ensures that your application can securely verify users during registration or password resets. You can expand this by integrating OTP with two-factor authentication (2FA) for even better security.