Tutorial: Implement Authentication in Next.js
vdelitz
Posted on September 19, 2024
1. Introduction: Next.js Authentication
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.
Read full, original blog post here
In this guide, we'll walk you through the process of implementing a login page in Next.js, covering various authentication methods including:
- passwords
- email OTPs
- SMS OTPs
- social logins (OAuth)
- TOTP via authenticator apps
- passkeys
If you want to see the complete code, please check out our Next.js login page repository on GitHub.
2. Setting Up Your Next.js Project
Before we jump into the specific authentication methods, we need to perform some general project setup steps.
2.1 Prerequisites
To follow this guide, we require some basic understanding of
- Next.js
- React
- JavaScript
- TypeScript
- Node.js
- NPM
- HTML
- MongoDB
2.2 Create a New Next.js Project
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:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- src/ directory: Yes
- App Router: Yes
- Default import alias: No
Navigate to your project directory:
cd nextjs-login
2.3 Start the Development Server
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.
2.4 Set Up Environment Variables
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
3. Choose your Authentication Approach
In 2024, there are several ways to securely authenticate your users. Every application has a different user base and thus different requirements. The following table should help you find your best authentication method:
Approach | Security | Biggest risk | Methods |
---|---|---|---|
Password-based | Low | Credential stuffing | Password |
Password-less | Medium | Phishing | Email OTP, SMS OTP, social login |
Multi-factor authentication (MFA) | High | Spear phishing | Combination of two of the following methods: Password, email OTP, SMS OTP, TOTP (via authenticator apps) |
Phishing-resistant MFA | Highest | Weak fallbacks | Passkeys |
4. Overview of the Next.js Login Page Repository
The Next.js login page repository covers various authentication methods, all implemented in the same project:
- Password-based authentication
- Email OTP (one-time passcode)
- SMS OTP (one-time passcode)
- Social login (OAuth)
- TOTP (via authenticator app)
- Passkeys
Each method has its own directory structure and relevant files, which will be detailed in the sections below.
5. Implementing Password-Based Authentication
Let’s start with password-based authentication as the first authentication method we’ll implement. We will guide you step-by-step through creating password-based authentication using Next.js and Tailwind CSS. Whether you're building a new app or enhancing an existing one, you'll learn how to implement sign-up and login features with responsive design.
The following steps are required to implement Password-based authentication:
- Set up password-based project
- Creating the Auth component
- Creating the Signup Component
- Creating the Login Component
- Setting Up API Routes
- Connecting to MongoDB
- Testing the Password Authentication
5.1 Set Up Password-Based Project
In this section, we'll dive into the specific files and structure needed for password-based authentication. Here's a clear overview of the relevant directory structure and files:
Key Files and Their Roles:
-
app/password/login/page.tsx
: Contains the login form component. -
app/password/signup/page.tsx
: Contains the signup form component. -
components/AuthForm.tsx
: Reusable form component for login and signup. -
lib/mongodb.ts
: Sets up the MongoDB connection. -
models/User.ts
: Defines the user schema. -
pages/api/auth/password/login.ts
: API route to handle login requests. -
pages/api/auth/password/register.ts
: API route to handle signup requests.
5.2 Installing the Relevant Dependencies
To set up password-based authentication, you need to install the following dependencies:
bcryptjs: This library allows you to hash passwords securely.
mongoose: This library helps you model your data in MongoDB. It provides a straightforward, schema-based solution to model your application data. You can install these dependencies using npm:
npm install bcryptjs mongoose
5.3 Creating the Auth Component
In this section, we will create a reusable Auth component that will be used for both the login and signup forms. This component will handle the form structure, styling, and state management.
This component will be reused in both the login and signup components, reducing code duplication and making the forms easy to manage and style.
- File Location: Place the
AuthForm
component in thecomponents
directory. - Purpose: This component will render the form fields (email and password), handle form submission, and display messages.
- Explanation
-
Props: The
AuthForm
component takesmode
,onSubmit
, andresetForm
as props.mode
is used to differentiate between login and signup forms,onSubmit
handles form submission, andresetForm
resets the form fields after submission. -
State Management: The component uses
useState
to manage email and password input states. -
Form Handling: The
handleSubmit
function prevents the default form submission, gathers the input data, and calls theonSubmit
function passed as a prop.
-
Props: The
Here's the complete code for the AuthForm
component:
"use client";
import { useState, FormEvent, useEffect } from "react";
interface AuthFormProps {
mode: "Signup" | "Login";
onSubmit: (data: { email: string, password: string }) => void;
resetForm?: boolean;
}
const AuthForm: React.FC<AuthFormProps> = ({ mode, onSubmit, resetForm }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
if (resetForm) {
setEmail("");
setPassword("");
}
}, [resetForm]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<h2 className="text-2xl font-bold mb-4 text-center">{mode}</h2>
<div>
<label className="block text-gray-700 dark:text-gray-300">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 p-2 w-full border rounded-md focus:outline-none focus:ring focus:border-blue-300 dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600"
/>
</div>
<div>
<label className="block text-gray-700 dark:text-gray-300">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 p-2 w-full border rounded-md focus:outline-none focus:ring focus:border-blue-300 dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600"
/>
</div>
<button
type="submit"
className="w-full py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300"
>
{mode}
</button>
</form>
);
};
export default AuthForm;
5.4 Creating the Signup Component
In this section, we will detail how to create the Signup component for user registration. This component will handle user input, submission, and display relevant messages.
- File Location: Place the
Signup
component in theapp/password/signup
directory. - Purpose: The Signup component renders the signup form, handles form submission, and displays success or error messages.
- Explanation
-
State Management: The component uses
useState
to manage the message and success states. -
Form Handling: The
handleSignup
function manages the form submission, sends a POST request to the server, and updates the state based on the response.
-
State Management: The component uses
Here's the complete code for the Signup component:
"use client";
import { useState } from "react";
import AuthForm from "../../../components/AuthForm";
import Link from "next/link";
const Signup: React.FC = () => {
const [message, setMessage] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleSignup = async (data: { email: string, password: string }) => {
const res = await fetch("/api/auth/password/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const result = await res.json();
setMessage(result.message);
if (res.status === 201) {
setIsSuccessful(true);
setIsSuccess(true);
} else {
setIsSuccess(false);
}
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md w-full max-w-md">
{isSuccessful ? (
<>
<p className="text-green-500 text-center text-lg font-semibold">
Welcome!
</p>
</>
) : (
<AuthForm mode="Signup" onSubmit={handleSignup} />
)}
{message && (
<p
className={`text-center mt-4 ${
isSuccess ? "text-green-500" : "text-red-500"
}`}
>
{message}
</p>
)}
{isSuccessful && (
<Link href="/password/login">
<p className="text-center text-blue-500 font-bold underline py-4">
Back to login
</p>
</Link>
)}
</div>
</div>
);
};
export default Signup;
5.5 Creating the Login Component
Next, we will create the Login component for user authentication. This component will handle user input, submission, and display relevant messages.
- File Location: Place the
Login
component in theapp/password/login
directory. - Purpose: The Login component renders the login form, handles form submission, and displays success or error messages.
- Explanation
-
State Management: The component uses
useState
to manage the message and success states. -
Form Handling: The
handleLogin
function manages the form submission, sends a POST request to the server, and updates the state based on the response.
-
State Management: The component uses
Here's the complete code for the Login
component:
import type { NextApiRequest, NextApiResponse } from "next";
import dbConnect from "@/lib/mongodb";
import User from "@/models/User";
import bcrypt from "bcryptjs";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await dbConnect();
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: "Email and password are required" });
}
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: "Invalid credentials" });
}
const isValidPassword = bcrypt.compareSync(password, user.password);
if (!isValidPassword) {
return res.status(400).json({ message: "Invalid credentials" });
}
return res.status(200).json({ message: "Login successful" });
}
5.6 Setting Up API Routes
In this section, we will create API endpoints to handle user registration and login requests. The API routes handle incoming HTTP requests for user registration and login.
5.6.1 Register API Route
- File Location: Place the
register.ts
file in thepages/api/auth/password
directory. - Purpose: Handles user registration by receiving email and password, hashing the password, and storing the user data in the database
- Explanation:
- Checks for the presence of email and password.
- Validates if the user already exists.
- Hashes the password using
bcryptjs
. - Saves the new user to the database.
- Returns a success message upon successful registration.
import type { NextApiRequest, NextApiResponse } from "next";
import dbConnect from "@/lib/mongodb";
import User from "@/models/User";
import bcrypt from "bcryptjs";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await dbConnect();
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: "Email and password are required" });
}
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: "User already exists" });
}
const hashedPassword = bcrypt.hashSync(password, 10);
const newUser = new User({ email, password: hashedPassword });
await newUser.save();
return res.status(201).json({ message: "Signup successful!" });
}
5.6.2 Login API Route
- File Location: Place the
login.ts
undersrc/pages/api/auth/password
directory - Purpose: Handles user login by receiving email and password, verifying the user, and checking the password against the stored hash.
- Explanation:
- Checks for the presence of email and password.
- Validates if the user exists.
- Compares the provided password with the stored hashed password.
- Returns a success message if credentials are correct; otherwise, returns an error message.
import type { NextApiRequest, NextApiResponse } from "next";
import dbConnect from "@/lib/mongodb";
import User from "@/models/User";
import bcrypt from "bcryptjs";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await dbConnect();
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: "Email and password are required" });
}
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: "Invalid credentials" });
}
const isValidPassword = bcrypt.compareSync(password, user.password);
if (!isValidPassword) {
return res.status(400).json({ message: "Invalid credentials" });
}
return res.status(200).json({ message: "Login successful" });
}
5.7 Connecting to MongoDB
In this section, we will set up a connection to MongoDB, which is crucial for handling user data in our authentication system.
5.7.1 Setting Up MongoDB Locally
- Download and Install MongoDB:
- Go to the MongoDB Server Download
- Select your operating system and download the MongoDB installer.
- Follow the installation instructions for your OS.
- Install MongoDB Compass:
- Download MongoDB Compass from the MongoDB website.
- Install it to easily manage your MongoDB databases visually.
- Create a Database and Collection:
- Open MongoDB Compass and connect to your local MongoDB server (default connection string is
mongodb://localhost:27017
).
- Open MongoDB Compass and connect to your local MongoDB server (default connection string is
Create a new database named user_management
.
- Create a new collection within this database named `users`.
Here is a high-level description of the users
collection relevant for the password-based authentication method:
5.7.2 Database Connection File
import mongoose from "mongoose";
const MONGODB_URI: string | undefined = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error("Please define the MONGODB_URI environment variable");
}
interface MongooseCache {
conn: mongoose.Connection | null;
promise: Promise<mongoose.Connection> | null;
}
declare global {
var mongoose: MongooseCache;
}
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function dbConnect(): Promise<mongoose.Connection> {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = { bufferCommands: false };
cached.promise = mongoose
.connect(MONGODB_URI as string, opts)
.then((mongoose) => {
return mongoose.connection;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default dbConnect;
To avoid TypeScript errors regarding the global cache, add the following to a global.d.ts
file for Global Type Declarations
import mongoose from "mongoose";
declare global {
var mongoose: {
conn: mongoose.Connection | null;
promise: Promise<mongoose.Connection> | null;
};
}
5.8 Testing the Authentication Flow
In this section, we'll guide you on how to start the application and test the signup and login flows.
5.8.1 Testing the Signup Flow
Route: http://localhost:3000/password/signup
Steps:
- Navigate to the signup page.
- Enter an email and password.
- Click the submit button.
- Observe the success or error message.
Screenshot of Signup Form:
5.8.2 Testing the Login Flow
Route: http://localhost:3000/password/login
Steps:
- Navigate to the login page.
- Enter the registered email and password.
- Click the submit button.
- Observe the success or error message.
Screenshot of Login Form:
If the user enters invalid credentials (incorrect email or password), the system provides an error message.
You've successfully implemented a password-based authentication system with Next.js and Tailwind CSS.
Let’s now have a look at the passwordless authentication methods.
6. OTP (One-Time Passcode) via Email or SMS
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:
- Set up OTP-based authentication project
- Implement API Route for Generating OTP via Email and SMS
- Implement API Route for Verifying OTP
- Create OTP Authentication component
- Testing the OTP Authentication Flow
- 5.1 Testing OTP Authentication via Email
- 5.2 Testing OTP Authentication via SMS
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:
- Trigger: The OTP flow starts with a trigger event. This can be an action such as a user attempting to log in or register.
- OTP Generation: Upon receiving the trigger, the server generates a unique OTP. This OTP is usually a random, time-sensitive code.
- OTP Delivery: The generated OTP is delivered to the user through their chosen medium, such as email or SMS.
- User Input: The user receives the OTP and enters it into the application.
- OTP Verification: If the OTP verification is successful, the user is authenticated and allowed to proceed with their intended action.
6.1 Set Up OTP-Based Authentication Project
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.
6.2 Installing the Relevant Dependencies
To set up OTP authentication, you need to install the following dependencies:
- Nodemailer: Nodemailer is a Node.js module used for sending emails. It simplifies the process of using SMTP and other transport methods. In our application, we will use Nodemailer to send OTP codes via email.
- Twilio: Twilio is a cloud communications platform that enables sending SMS messages from your application. We will use Twilio to send OTP codes via SMS.
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
6.3 Set Up MongoDB for OTP Storage
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.
- File Location: Create a new file at
models/Otp.ts
in your project directory. - Purpose: The OTP model will define the structure of the OTP documents stored in the MongoDB collection. It includes fields for email or phone number, OTP, and the creation date, ensuring each OTP expires after a specified time.
- Explanation
- Interface Definition : IOtp is an interface extending Document, defining the structure of an OTP document.
- Schema Definition : OtpSchema is a new schema defining the structure of OTP documents. It includes the following fields:
-
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. - Model Creation :
Otp
is a model created from theOtpSchema
. 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;
6.4 Implement Backend API Route for Generating OTPs via Email and SMS
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.
- File Location: Create a new file at
pages/api/auth/otp/generate.ts
in your project directory. - Purpose: The API route will generate an OTP, hash it for security, store it in MongoDB, and send it to the user via email or SMS based on the specified delivery method.
- Explanation
- Generate OTP Code: A function to generate a 6-digit OTP.
-
Hash OTP: Use
bcrypt
to hash the OTP for security. - Store OTP in MongoDB: Store the hashed OTP along with the email or phone number in MongoDB.
-
Send OTP via Email: Use
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" });
}
6.5 Implement Backend API Route for Verifying OTP
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.
- File Location: Create a new file at
pages/api/auth/otp/verify.ts
in your project directory. - Purpose: The API route will verify the OTP by comparing it with the stored hashed OTP in MongoDB. If the OTP is valid, it will delete the OTP record from the database.
- Explanation
- Connect to MongoDB: Establish a connection to the MongoDB database.
- Extract Data from Request: Extract email, phone number, and OTP from the request body.
- Validate Input: Ensure either email or phone number, and OTP are provided.
- Find OTP Record: Find the OTP record by email or phone number.
- Check OTP Record Existence: Check if the OTP record exists.
- Compare OTP: Compare the provided OTP with the hashed OTP in the database.
- Return Error if OTP is Invalid: If the OTP does not match, return an error.
- Respond with Success Message: If the OTP does match, respond with a success message
- Delete OTP Record: Delete the OTP record after successful verification.
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" });
}
6.6 Create Frontend OTP Authentication component
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.
- File Location: Create a new file at
src/app/otp/page.tsx
in your project directory. - Purpose: The user interface allows users to select their preferred delivery method (email or SMS), enter their contact information (email or phone number), generate an OTP, and verify the OTP.
- Explanation
- State Variables: We use state variables to manage contact information, OTP, messages, delivery method, and the status of OTP generation and verification.
-
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;
6.7 Testing the OTP Authentication Flow
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.
6.7.1 Testing OTP Authentication via Email
Route: http://localhost:3000/otp
Steps:
- Navigate to the OTP Authentication Page: Open your browser and navigate to the OTP authentication page
http://localhost:3000/otp
- Select Email as Delivery Method: Select "Email" from the dropdown menu for OTP delivery method.
- Enter Email: Enter a valid email address in the input field.
- Generate OTP: Click the
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.
- Enter OTP: Copy the OTP from the Ethereal email preview and enter it in the OTP input field.
- Verify OTP: Click the
Verify OTP
button.
- Check for a success message indicating that the OTP was verified successfully.
6.7.2 Testing OTP Authentication via SMS
Route:http://localhost:3000/otp
Steps:
- Navigate to the OTP Authentication Page:
Open your browser and navigate to the OTP authentication page
http://localhost:3000/otp
- Select SMS as Delivery Method:
Select
SMS
from the dropdown menu for OTP delivery method. - Enter Phone Number: Enter a valid phone number in the input field.
- 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.
- Enter OTP: Enter the OTP received via SMS in the OTP input field.
- Verify OTP:
Click the
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.
7. Social login (OAuth)
Using OAuth for third-party authentication is a very popular and user-friendly solution. In this section, we'll explore how to integrate Google authentication into a Next.js application using NextAuth.js.
We chose NextAuth.js for Google authentication because it's easy to integrate and offers robust security features. NextAuth.js simplifies adding Google OAuth, providing a seamless and secure login experience. It handles the complex parts of authentication, so we can focus on building our app. Plus, it's highly customizable, making it a perfect fit for our needs.
The following steps covers the implementation of the Google authentication:
- Set Up Google-Based Authentication Project
- Google Cloud Setup for OAuth 2.0 Credentials
- Installing the Relevant Dependencies
- Configuring NextAuth.js
- Creating Authentication Components
5.1 Creating
SignInButton
Component 5.2 CreatingLogoutButton
Component 5.3 CreatingClientProvider
Component - Creating Google Authentication Page
- Testing the Google Authentication Flow
7.1 Set Up Google-Based Authentication Project
In this section, we'll detail how to set up Google-based authentication (Google social login) for your Next.js project. We'll break down the specific files and structure. Let's start with an overview of the relevant directory structure and the key files involved:
Key Files and Their Roles:
-
.env.local
: In this file will store the environment variables which are your Google client IDGOOGLE_CLIENT_ID
and secretGOOGLE_CLIENT_SECRET
-
src/pages/api/auth/[...nextauth].ts
: This file sets up NextAuth.js for handling authentication. It includes configuration for Google as an authentication provider. -
src/components/ClientProvider.tsx
: A wrapper component that provides session management using NextAuth.js. It ensures that session data is available throughout your application. -
src/app/layout.tsx
: The root layout for your application, which wraps all pages with the ClientProvider to manage session state globally. -
src/components/SignInButton.tsx
: A component that renders a button for signing in with Google. It handles the sign-in process when clicked. -
src/components/LogoutButton.tsx
: A component that renders a logout button. It handles signing the user out and redirecting them appropriately. -
src/app/googleLogin/page.tsx
: The main page for handling Google login. It displays different content based on whether the user is signed in or not.
7.2 Google Cloud Setup for OAuth 2.0 Credentials
- Create a New Project in Google Cloud Console
- Go to the Google Cloud Console. https://console.cloud.google.com/apis
- Click on the project dropdown in the top-left corner and select "New Project".
- Enter a project name and click "Create".
- Configure our OAuth Consent Screen
- Once your project is created, navigate to the "OAuth Consent Screen" section.
- Select External users
- Add the app name, select the user support email and add the developer contact information
- Create OAuth 2.0 Credentials
- In the "APIs & Services" section, click on "Credentials" in the left sidebar.
- Click on "Create Credentials" and select "OAuth 2.0 Client ID".
- You may be prompted to configure the OAuth consent screen if you haven't done so already.
- Click on "OAuth consent screen" and fill out the required fields.
- After configuring the consent screen, choose "Web application" as the application type.
- Enter a name for the OAuth client and add your application's URLs in the "Authorized redirect URIs" field. Typically, this would be something like `http://localhost:3000/api/auth/callback/google` for local development.
- Click "Create" and note down your Client ID and Client Secret.
- Add Credentials to .env.local
- Open your
.env.local
file and add the following environment variables:
- Open your
GOOGLE_CLIENT_ID=<YOUR_GOOGLE_CLIENT_ID>
GOOGLE_CLIENT_SECRET=<YOUR_GOOGLE_CLIENT_SECRET>
7.3 Installing the Relevant Dependencies
First, install the necessary dependencies:
- NextAuth.js: A complete open-source authentication solution for Next.js applications. It supports various authentication methods, including OAuth providers like Google, GitHub, Twitter, and more.
- react-icons: A popular library for including icons in your React application. We'll use it to enhance the UI of the sign in button by adding a Google icon.
You can install the dependencies using the following command:
npm install next-auth react-icons
7.4 Configuring NextAuth.js
In this section, we will configure NextAuth.js to handle Google authentication in our Next.js application. We will walk through the specific file used for this configuration, its location, purpose, and explain the main concepts involved. Additionally, we’ll discuss the reason behind the file naming convention [...nextauth].
Good to know: Reason for the Naming Convention [...nextauth]
The file is named [...nextauth].ts to leverage Next.js's dynamic routing feature. The square brackets [ ] denote a dynamic route segment, and the three dots ... indicate a catch-all route. This means that any API route starting with /api/auth/ (e.g., /api/auth/signin, /api/auth/signout, /api/auth/callback) will be handled by this file. This naming convention provides a flexible way to manage all authentication-related routes in a single file.
- File Location: Create a new file at
src/pages/api/auth/[...nextauth].ts
in your project directory - Purpose: This file sets up and configures NextAuth.js to handle authentication using Google provider. It also includes callback functions to manage user sessions.
- Explanation
-
Import Statements:
-
NextAuth
: The core module for handling authentication in Next.js. -
GoogleProvider
: A provider module for handling Google OAuth authentication.
-
- Session Interface: Session: An interface to type the user data returned in the session object. It ensures that the session contains a user object with name and email properties.
-
NextAuth Configuration:
-
providers: This array contains authentication providers. Here, we configure Google as the provider using the
GoogleProvider
module. TheclientId
andclientSecret
are fetched from environment variables to ensure they are not hardcoded and remain secure. -
callbacks: This object contains the
session
function called during the authentication process. Thesession
function is called whenever a session is checked or created. It modifies the session object to include the user's name and email from the token.
-
providers: This array contains authentication providers. Here, we configure Google as the provider using the
-
Import Statements:
Here’s the complete code of the [...nextauth].ts
file:
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
// Define the Session interface to type the user data
export interface Session {
user: {
name: string | null;
email: string | null;
};
}
export default NextAuth({
// Define the authentication providers
providers: [
GoogleProvider({
// Set the Google client ID and client secret from environment variables
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
}),
],
// Define callback functions to manage session and token
callbacks: {
// Modify the session object before it is returned to the client
async session({ session, token }) {
// Ensure the session user object exists and set name and email from token
if (session.user) {
session.user.name = token.name || null;
session.user.email = token.email || null;
}
return session;
},
},
});
7.5 Creating Authentication Components
In this section, we'll create the essential components needed for handling authentication in our Next.js application. These components include a sign-in button for Google authentication and a logout button for signing out. We'll also create a client provider to manage session state across the application.
Here are the components we'll create:
- 7.5.1
SignInButton.tsx
- 7.5.2
LogoutButton.tsx
- 7.5.3
ClientProvider.tsx
These components will be placed in the src/components
directory. Each of these components serves a specific purpose in the authentication flow.
7.5.1 Creating SignInButton Component
- File Location: create a new file at
src/components/SignInButton.tsx
- Purpose: This component renders a button that allows users to sign in with their Google account.
- Explanation:
-
signIn
function: From next-auth/react, it handles the sign-in process with Google. - Button Element: Triggers the
signIn
function with Google as the provider and includes the prompt: "select_account" parameter to ensure the account chooser appears.
-
Here’s the complete code of the SignInButton.tsx
file:
"use client";
import { signIn } from "next-auth/react";
import { FaGoogle } from "react-icons/fa";
function SignInButton() {
return (
<button
onClick={() => signIn("google", { prompt: "select_account" })}
className="flex items-center justify-center px-4 py-2 rounded border-gray-900 border hover:bg-blue-100"
>
<FaGoogle className="mr-2" />
Sign in with Google
</button>
);
}
export default SignInButton;
7.5.2 Creating LogoutButton Component
- File Location: create a new file at
src/components/LogoutButton.tsx
- Purpose: This component renders a button that allows users to sign out of their account.
- Explanation:
-
signOut
function: Fromnext-auth/react
, it handles the sign-out process. - Button Element: Triggers the
signOut
function and redirects the user to the/googleLogin
page after signing out.
-
Here’s the complete code of the LogoutButton.tsx
file:
"use client";
import { signOut } from "next-auth/react";
function LogoutButton() {
return (
<button
onClick={() => signOut({ callbackUrl: "/googleLogin" })}
className="px-4 py-2 bg-blue-300 rounded hover:bg-blue-400"
>
Logout
</button>
);
}
export default LogoutButton;
7.5.3 Creating ClientProvider Component
- File Location: create a new file at
src/components/ClientProvider.tsx
- Purpose: This component wraps the application with the
SessionProvider
to manage session state globally. - Explanation:
- Props Interface: Defines the type for the component's props to ensure it receives children as a React node.
-
SessionProvider: From
next-auth/react
, it wraps the children components to provide session management.
Here’s the complete code of the ClientProvider.tsx
file:
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
// Define a type for the component's props
interface Props {
children: ReactNode;
}
const ClientProvider = ({ children }: Props) => {
return (
// Wrap the children with the SessionProvider to manage session state
<SessionProvider>{children}</SessionProvider>
);
};
export default ClientProvider;
7.6 Configuring the Root Layout
In this section, we'll configure the root layout of your Next.js application to ensure that session state is managed globally. This involves using the ClientProvider
component, which wraps the entire application and provides session management using NextAuth.js.
- File Location:
src/app/layout.tsx
- Purpose: This file sets up the root layout for your Next.js application. It ensures that all pages are wrapped with the
ClientProvider
, enabling global session management. - Explanation:
-
Props:
- children: The children prop represents all the child components that will be wrapped by this layout. This includes all the pages and components in your application.
-
<ClientProvider>
: Wraps the children components with theClientProvider
to manage session state globally. This ensures that session information is accessible across all pages and components in your application.
-
Props:
Here’s the complete code of the layout.tsx
file:
import "./globals.css";
import ClientProvider from "@/components/ClientProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* Wrap the children with ClientProvider to manage session state */}
<ClientProvider>{children}</ClientProvider>
</body>
</html>
);
}
7.7 Creating Google Authentication Page
In this section, we will focus on how to handle user sign-in and sign-out actions on the main login page. We will use the previously created SignInButton
and LogoutButton
components and ensure they are integrated seamlessly with our authentication flow.
- File Location: create a new file at
src/app/googleLogin/page.tsx
- Purpose: This file represents the main page for handling Google login. It displays different content based on whether the user is signed in or not.
- Explanation:
-
Import Statements:
-
useSession: A hook from
next-auth/react
that provides session information. - SignInButton: The sign-in button component created earlier.
- LogoutButton: The logout button component created earlier.
- Session Handling:
- The
useSession
hook fetches the current session data. - If a session exists (i.e., the user is logged in), it displays a welcome message and the
LogoutButton
. - If no session exists (i.e., the user is not logged in), it displays the
SignInButton
.
-
useSession: A hook from
Here’s the complete code of the page.tsx
file:
"use client";
import { useSession } from "next-auth/react";
import SignInButton from "../../components/SignInButton";
import LogoutButton from "../../components/LogoutButton";
function GoogleLoginPage() {
const { data: session } = useSession();
if (session) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-6 rounded-lg shadow-md text-center">
<h1 className="text-2xl font-semibold mb-4">
Welcome, {session.user?.name}!
</h1>
<LogoutButton />
</div>
</div>
);
}
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-6 rounded-lg shadow-md text-center">
<h1 className="text-2xl font-semibold mb-4">Social Login</h1>
<SignInButton />
</div>
</div>
);
}
export default GoogleLoginPage;
7.8 Testing the Google Authentication Flow
By following these steps, you can test the Google authentication flow in your Next.js application. Ensure that each step works correctly:
-
Route:
http://localhost:3000/googleLogin
- Steps:
- Open your browser and navigate to the login page
http://localhost:3000/googleLogin
. You should see the Google Sign-In button.
- Click on the
Sign in with Google
button. This action should redirect you to Google's authentication page. - On the Google authentication page, sign in with your Google account
- After signing in, you should be redirected back to your application. If the sign-in was successful, you should see a welcome message along with the Logout button.
By following these steps, you will have successfully set up Google authentication in your Next.js application using NextAuth.js. This setup includes creating necessary components, configuring authentication providers, and managing session states.
8. Authenticator App (TOTP)
TOTP, or Time-based One-Time Password, is a popular method for two-factor authentication (2FA). It enhances security by requiring users to enter a unique, time-sensitive code. This code changes every 30 seconds, making it highly secure against interception and replay attacks
In this section, we'll explore how to implement a TOTP authentication in your Next.js application.
The following steps covers the implementation of the TOTP-based authentication:
- Set Up TOTP-Based Authentication Project
- Installing the Relevant Dependencies
- Defining the TOTP Schema
- Implement API Route For Generating the TOTP Secret and QR Code
- Implement API Route For Checking the Two-factor Authentication Status
- Implement API Route For Verifying the TOTP
- Creating the TOTP Component
- Testing the TOTP-based authentication Flow
8.1 Set Up TOTP-Based Authentication Project
In this section, we'll explain how to set up TOTP-based authentication for your Next.js project. We'll break down the specific files and structure. Let's start with an overview of the relevant directory structure and the key files involved:
-
src/models/Totp.ts
: This file defines the Mongoose schema for TOTP. It includes fields for email, secret, and TOTP-based authentication status. -
src/pages/api/auth/totp/generate.ts
: This API endpoint generates a TOTP secret and a corresponding QR code for the user to scan with their authenticator app. -
src/pages/api/auth/totp/status.ts
: This API endpoint checks whether TOTP-based authentication is enabled for a given user. -
src/pages/api/auth/totp/verify.ts
: This API endpoint verifies the TOTP entered by the user. -
src/app/totp/page.tsx
: This is the frontend component that handles the user interface for TOTP-based authentication. It allows users to login, generate a QR code, and verify their TOTP.
8.2 Installing the Relevant Dependencies
To set up TOTP-based authentication in your Next.js project, you'll need a few essential dependencies. Let's go through the installation and purpose of each one.
- speakeasy is a library for generating and verifying one-time passcodes, specifically TOTP in our case. It will be used to handle the generation and verification of TOTP codes.
- qrcode is a library to generate QR codes. This will be used to create a QR code that users can scan with their authenticator app to set up TOTP.
You can install all the dependencies using the following command:
npm install speakeasy qrcode
8.3 Defining the TOTP Schema
- File Location:
src/models/Totp.ts
- Purpose: This file defines the schema for storing TOTP-related data in your MongoDB database using Mongoose. This schema includes the email, secret, and two-factor authentication status of the user. It ensures that each user's TOTP information is stored securely and uniquely.
- Explanation:
-
Email: Stores the user's email and ensures it's unique.
-
secret
: Stores the TOTP secret key. -
totpEnabled
: Indicates whether TOTP is enabled for the user.
-
Here is a high-level description of the totps
collection relevant for the TOTP-based authentication method:
Here's the Totp.ts
file:
import mongoose, { Document, Model, Schema } from "mongoose";
interface ITotp extends Document {
email: string;
secret: string;
totpEnabled: boolean;
}
const TotpSchema: Schema = new Schema({
email: { type: String, required: true, unique: true },
secret: { type: String, required: true },
totpEnabled: { type: Boolean, default: false },
});
const Totp: Model<ITotp> =
mongoose.models.Totp || mongoose.model<ITotp>("Totp", TotpSchema);
export default Totp;
8.4 Implement API Route For Generating the TOTP Secret and QR Code
- File Location:
src/pages/api/auth/totp/generate.ts
- Purpose: This file defines an API route in Next.js for generating a TOTP secret and a corresponding QR code. This API endpoint is called when a user sets up TOTP-based authentication, providing them with a secret key and QR code to scan with their authenticator app.
- Explanation:
-
Connect to Database: Uses a helper function
connectDb
to connect to the MongoDB database.- Generate Secret: Uses speakeasy to generate a TOTP secret key.
- Find User: Checks if the user already exists and if TOTP-based authentication is already enabled.
-
Generate QR Code: Uses
qrcode
to generate a QR code from the TOTP secret key's URL. -
Update Database: Stores the TOTP secret and sets
totpEnabled
to false for the user in the database. - Response: Returns the TOTP secret and QR code as a JSON response if successful.
Here's the implementation of the generate.ts
file:
import { NextApiRequest, NextApiResponse } from "next";
import speakeasy from "speakeasy";
import qrcode from "qrcode";
import Totp from "../../../../models/Totp";
import connectDb from "../../../../lib/mongodb";
// Generate TOTP secret and QR code
const generateTOTP = async (req: NextApiRequest, res: NextApiResponse) => {
await connectDb();
const { email } = req.body;
const secret = speakeasy.generateSecret({
length: 20,
name: "Time-based One-time Password",
});
const user = await Totp.findOne({ email });
if (user && user.totpEnabled) {
res.status(400).json({ error: "TOTP already enabled" });
return;
}
if (secret.otpauth_url) {
qrcode.toDataURL(secret.otpauth_url, async (err, data_url) => {
if (err) {
res.status(500).json({ error: "Error generating QR code" });
} else {
await Totp.updateOne(
{ email },
{ email, secret: secret.base32, totpEnabled: false },
{ upsert: true }
);
res.status(200).json({ secret: secret.base32, qrCode: data_url });
}
});
} else {
res.status(500).json({ error: "Error generating OTP auth URL" });
}
};
export default generateTOTP;
8.5 Implement API Route For Checking the TOTP Authentication Status
- File Location:
src/pages/api/auth/totp/status.ts
- Purpose: This file defines an API route in Next.js for checking the status of TOTP for a user. This endpoint is used to determine whether TOTP is enabled for a given email address, providing necessary information to the frontend to guide user interactions.
- Explanation:
-
Connect to Database: Uses a helper function
connectDb
to connect to the MongoDB database. - Extract Email: Retrieves the user's email from the request body.
-
Find User: Looks up the user in the
Totp
collection by email. - Check TOTP Status Determines if TOTP is enabled for the user.
-
Response: Returns the status of
totpEnabled
as a JSON response if the user is found, otherwise returns an error message.
-
Connect to Database: Uses a helper function
Here's the implementation of the status.ts
file:
import { NextApiRequest, NextApiResponse } from "next";
import connectDb from "../../../../lib/mongodb";
import Totp from "../../../../models/Totp";
const check2FAStatus = async (req: NextApiRequest, res: NextApiResponse) => {
await connectDb();
const { email } = req.body;
const user = await Totp.findOne({ email });
if (user) {
res.status(200).json({ twoFactorEnabled: user.twoFactorEnabled });
} else {
res.status(404).json({ error: "User not found" });
}
};
export default check2FAStatus;
8.6 Implement API Route For Verifying the TOTP
- File Location:
src/pages/api/auth/totp/verify.ts
- Purpose: This file defines an API route in Next.js for verifying the TOTP (Time-based One-Time Password) entered by the user. This endpoint is called when the user submits their TOTP code during the login or verification process, ensuring that the code is correct and enabling two-factor authentication if it is.
- Explanation:
- Connect to Database: Uses a helper function
connectDb
to connect to the MongoDB database. - Extract Email and Token: Retrieves the user's email and TOTP token from the request body.
- Find User: Looks up the user in the TOTP collection by email.
- Verify TOTP: Uses speakeasy to verify the TOTP token against the stored secret key.
- Update TOTP Status: If the token is verified, updates the
totpEnabled
status to true for the user in the database. - Response: Returns the verification status as a JSON response.
- Connect to Database: Uses a helper function
Here's the implementation of the verify.ts
file:
import { NextApiRequest, NextApiResponse } from "next";
import speakeasy from "speakeasy";
import Totp from "../../../../models/Totp";
import connectDb from "../../../../lib/mongodb";
const verifyTOTP = async (req: NextApiRequest, res: NextApiResponse) => {
await connectDb();
const { email, token } = req.body;
const user = await Totp.findOne({ email });
if (!user || !user.secret) {
res.status(400).json({ error: "TOTP not setup for this user" });
return;
}
const verified = speakeasy.totp.verify({
secret: user.secret,
encoding: "base32",
token,
});
if (verified) {
await Totp.updateOne({ email }, { totpEnabled: true });
}
res.status(200).json({ verified });
};
export default verifyTOTP;
8.7 Creating the TOTP Component
- File Location:
src/app/totp/page.tsx
- Purpose: This file defines the component for managing TOTP-based authentication. This component handles user interactions for logging in, generating a TOTP QR code, and verifying the TOTP code. It ensures users can easily set up and verify their TOTP for enhanced security.
- Explanation:
- State Management: Uses
useState
to manage state variables such as email, QR code, token, verification status, errors, and TOTP status. - Effect Hook: Uses
useEffect
to check the TOTP status whenever the email changes. -
handleLogin
function: Validates email input and sets the logged-in state. -
generateQrCode
function: Fetches the QR code from the backend when the user opts to set up TOTP. -
verifyToken
function: Verifies the entered TOTP token by calling the backend API. - UI Component: Renders different UI elements based on the authentication status and interactions.
- State Management: Uses
Here’s the complete code for the TOTP component:
"use client";
import Image from "next/image";
import { useState, useEffect } from "react";
export default function TOTP() {
const [email, setEmail] = useState("");
const [qrCode, setQrCode] = useState("");
const [token, setToken] = useState("");
const [verified, setVerified] = useState(false);
const [error, setError] = useState("");
const [totpEnabled, setTotpEnabled] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
const [emailError, setEmailError] = useState("");
// Check the TOTP status when the email changes
useEffect(() => {
const checkTOTPStatus = async () => {
if (email) {
const res = await fetch("/api/auth/totp/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
setTotpEnabled(data.totpEnabled);
}
};
checkTOTPStatus();
}, [email]);
// Handle login process
const handleLogin = async () => {
if (!email) {
setEmailError("Email is required");
return;
}
setEmailError("");
setLoggedIn(true);
};
// Generate QR code for TOTP setup
const generateQrCode = async () => {
const res = await fetch("/api/auth/totp/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
setQrCode(data.qrCode);
setToken("");
setVerified(false);
setError("");
};
// Verify the token entered by the user
const verifyToken = async () => {
const res = await fetch("/api/auth/totp/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, token }),
});
const data = await res.json();
if (data.verified) {
setVerified(true);
setError("");
setTotpEnabled(true);
} else {
setVerified(false);
setError("Invalid Token. Please try again.");
}
};
// Handle logout process
const handleLogout = () => {
window.location.href = "http://localhost:3000/totp";
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
<h1 className="text-xl font-bold mb-4 text-center">
Time-based One-Time Passwords Login
</h1>
{/* Render login form if the user is not logged in */}
{!loggedIn && (
<>
{emailError && (
<p className="text-red-500 text-center mb-1">{emailError}</p>
)}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
className="border rounded py-2 px-3 text-gray-700 w-full mb-4"
/>
<button
onClick={handleLogin}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4 w-full"
>
Login with TOTP
</button>
</>
)}
{/* Show the generate QR code button if the user is logged in but TOTP is not enabled */}
{loggedIn && !totpEnabled && !qrCode && (
<button
onClick={generateQrCode}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4 w-full"
>
Generate QR Code
</button>
)}
{/* Show the token input and verify button if TOTP is enabled but not yet verified */}
{loggedIn && totpEnabled && !verified && !qrCode && (
<>
<input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter the code from the app"
className="border rounded py-2 px-3 text-gray-700 w-full mb-4"
/>
<button
onClick={verifyToken}
className="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded mb-4 w-full"
>
Verify Code
</button>
{error && <p className="text-red-500 text-center">{error}</p>}
</>
)}
{/* Show the QR code and token input fields for verification if not yet verified */}
{qrCode && !verified && (
<>
<div className="mb-4 text-center">
<Image
src={qrCode}
alt="QR Code"
width={200}
height={200}
className="mx-auto"
/>
<p className="mt-2">
1. Scan this QR code with your authenticator app.
</p>
<p className="mt-2">2. Enter the code from the app.</p>
</div>
<div className="mb-4">
<input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter the code from the app"
className="border rounded py-2 px-3 text-gray-700 w-full"
/>
<button
onClick={verifyToken}
className="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded mt-4 w-full"
>
Verify code
</button>
</div>
{error && <p className="text-red-500 text-center">{error}</p>}
</>
)}
{/* Show the TOTP enabled card and logout button if verification is successful */}
{verified && totpEnabled && (
<>
<div className="border border-green-500 bg-green-100 p-4 rounded-lg text-center mt-8 mb-4">
<h5 className="font-bold text-green-700">Your TOTP is enabled</h5>
</div>
<button
onClick={handleLogout}
className="bg-blue-200 hover:bg-blue-400 font-bold py-2 px-4 rounded mt-4 w-full"
>
Logout
</button>
</>
)}
</div>
</div>
);
}
8.8 Testing the TOTP-based authentication Flow
In this section, we will walk through the process of testing the TOTP-based authentication flow in your Next.js application. We'll specify the routes, outline the steps involved, and provide detailed descriptions for each step along with relevant screenshots.
-
Route:
http://localhost:3000/totp
- Steps for Setting Up TOTP for a New User
- Navigate to the TOTP Page
- Open your browser and navigate to
http://localhost:3000/totp
- You should see the login page with an email input field and a
Login with TOTP
button.
- Open your browser and navigate to
-
Enter Your Email and Log In
- Enter your email address in the email input field.
- Click the
Login with TOTP
button.
-
Generate QR Code for TOTP Setup
- Click the
Generate QR Code
button.
- Click the
-
Scan the QR Code with an Authenticator App
- A QR code will be displayed on the screen.
- Open your authenticator app (e.g., Google Authenticator) and scan the QR code.
- After scanning the QR code, your authenticator app will generate a TOTP code.
- Enter this code into the input field provided on the page.
- Click the
Verify Code
button.
- Verification and Enabling TOTP
- If the TOTP code entered is correct, you will see a message indicating that TOTP authentication is enabled. A record will be saved to the TOTP collection in MongoDB indicating that TOTP is enabled for the user.
- A
Logout
button will be displayed. - Steps for Logging in with TOTP
- Navigate to the TOTP Page
- Enter the TOTP Code from the Authenticator App
- If TOTP authentication is already enabled for this email, you will not see the option to generate a QR code. Instead, you will be prompted to enter the TOTP code directly.
- Open your authenticator app and retrieve the current TOTP code.
- Enter this code into the input field provided on the page.
- Click the "Verify Code" button.
- Verification
- If the TOTP code entered is correct, you will see a message indicating successful verification. Otherwise, you will see an error message.
- If the TOTP is verified, a
Logout
button will be displayed.
You have successfully set up TOTP-based authentication in your Next.js application.
9. Passkeys
In this section, we will explore the concept of passkeys, a form of passwordless authentication. Passkeys are the new standard for consumer authentication as they are more secure and user-friendly compared to other authentication alternatives.
We have covered the detailed implementation of passkey in Next.js apps already in various other blogs. Therefore, we won’t explain their implementation here but refer to our detailed guide in this blog post. To see the finished code, look at our Next.js passkey example repository on GitHub.
10. Recommendation for Next.js Authentication & Login Pages
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:
- Don’t Roll Your Own Auth: Use libraries and packages for tested security solutions and to save implementation time. Libraries like NextAuth.js or Auth0 are excellent choices as they offer robust and secure authentication solutions out of the box.
- Implement Multi-Factor Authentication (MFA): Combine authentication methods to achieve MFA, e.g., password plus TOTP to add an extra layer of security.
- Validate Email Addresses: Use APIs to check if email addresses are valid and belong to real users. Avoid allowing one-time or disposable email addresses to prevent spam and ensure genuine user engagement.
- Prevent SMS Pumping Attacks: Implement rate limits on SMS verifications and restrict access to certain areas to prevent SMS pumping attacks. Use services that can help identify and block fraudulent requests.
- Limit Login Attempts: To prevent brute force attacks, implement rate limiting on authentication routes. Temporarily block IP addresses after a set number of failed login attempts to protect against automated attacks.
- Never Store Passwords in Plain Text: Ensure that passwords are stored securely using hashing algorithms like bcrypt. Never store passwords in plain text.
- Implement Proper Session Management: Authentication is only the first step. After successfully authenticating your users, ensure you have proper session management in place to prevent session hijacking.
- Use HTTPS Everywhere: Always use HTTPS to encrypt data in transit. This protects user credentials and other sensitive data from being intercepted by attackers.
- Monitor and Log Authentication Events: Keep track of authentication events and monitor them for suspicious activities. Implement logging and alerting to quickly detect and respond to potential security threats.
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.
11. Conclusion
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:
- Passwords: We set up a password-based authentication system with Next.js, Tailwind CSS, and MongoDB. This included creating reusable form components, setting up API routes for registration and login, and connecting to MongoDB.
- 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.
- Social login (OAuth): We set up Google-based authentication using NextAuth.js, configured Google Cloud for OAuth 2.0 credentials, and created components for signing in and out.
- TOTP (via authenticator app): We implemented TOTP-based authentication by generating and verifying TOTP secrets, creating QR codes for easy setup, and building front-end components to handle the TOTP process.
- Passkeys: We gave a short introduction to passkeys and provided detail resources for implementing passkeys.
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.
Posted on September 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.