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:
If you want to see the complete code, please check out our Next.js login page repository on GitHub.
Before we jump into the specific authentication methods, 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
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 |
The Next.js login page repository covers various authentication methods, all implemented in the same project:
Each method has its own directory structure and relevant files, which will be detailed in the sections below.
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:
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:
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
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.
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= ({ 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 ( ); }; export default AuthForm;
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.
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 (); }; export default Signup;{isSuccessful ? (Welcome!
> ) : ()} {message && ( {message}
)} {isSuccessful && (Back to login
)}
Next, we will create the Login component for user authentication. This component will handle user input, submission, and display relevant messages.
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" }); }
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.
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!" }); }
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" }); }
In this section, we will set up a connection to MongoDB, which is crucial for handling user data in our authentication system.
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:
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| null; } declare global { var mongoose: MongooseCache; } let cached = global.mongoose; if (!cached) { cached = global.mongoose = { conn: null, promise: null }; } async function dbConnect(): Promise { 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| null; }; }
In this section, we'll guide you on how to start the application and test the signup and login flows.
Route: http://localhost:3000/password/signup
Steps:
Screenshot of Signup Form:
Route: http://localhost:3000/password/login
Steps:
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.
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:
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.
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= 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 ("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.
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: "[[email protected]](mailto:[email protected])", // 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.
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.
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 (); }; export default OtpPage;OTP Authentication
{!isOtpSent ? (setContactInfo(e.target.value)} required className="w-full p-3 border border-gray-300 rounded mb-4" /> > ) : ({!isOtpVerified ? ()} {message && (setOtp(e.target.value)} required className="w-full p-3 border border-gray-300 rounded mb-4" />) : ()}Welcome
{message}
)}
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:
- 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.
- Check for a success message indicating that the OTP was verified successfully.
Route:http://localhost:3000/otp
Steps:
After the OTP is generated, an OTP record is saved to the otps collection in the MongoDB database.
You've successfully implemented an OTP-based authentication system with Next.js and Tailwind CSS.
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:
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:
- Enter a project name and click "Create".
- 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.
GOOGLE_CLIENT_ID=GOOGLE_CLIENT_SECRET=
First, install the necessary dependencies:
You can install the dependencies using the following command:
npm install next-auth react-icons
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.
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; }, }, });
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:
These components will be placed in the src/components directory. Each of these components serves a specific purpose in the authentication flow.
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 ( ); } export default SignInButton;
Here’s the complete code of the LogoutButton.tsx file:
"use client"; import { signOut } from "next-auth/react"; function LogoutButton() { return ( ); } export default LogoutButton;
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{children} ); }; export default ClientProvider;
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.
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 ( {/* Wrap the children with ClientProvider to manage session state */}{children} ); }
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.
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 (); } return (Welcome, {session.user?.name}!
); } export default GoogleLoginPage;Social Login
By following these steps, you can test the Google authentication flow in your Next.js application. Ensure that each step works correctly:
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.
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:
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:
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.
You can install all the dependencies using the following command:
npm install speakeasy qrcode
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= mongoose.models.Totp || mongoose.model ("Totp", TotpSchema); export default Totp;
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;
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;
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;
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 (); }Time-based One-Time Passwords Login
{/* Render login form if the user is not logged in */} {!loggedIn && ( {emailError && ({emailError}
)} setEmail(e.target.value)} placeholder="Enter your email" className="border rounded py-2 px-3 text-gray-700 w-full mb-4" /> > )} {/* Show the generate QR code button if the user is logged in but TOTP is not enabled */} {loggedIn && !totpEnabled && !qrCode && ( )} {/* Show the token input and verify button if TOTP is enabled but not yet verified */} {loggedIn && totpEnabled && !verified && !qrCode && ( 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" /> {error &&{error}
} > )} {/* Show the QR code and token input fields for verification if not yet verified */} {qrCode && !verified && (1. Scan this QR code with your authenticator app.
2. Enter the code from the app.
setToken(e.target.value)} placeholder="Enter the code from the app" className="border rounded py-2 px-3 text-gray-700 w-full" />{error &&{error}
} > )} {/* Show the TOTP enabled card and logout button if verification is successful */} {verified && totpEnabled && (> )}Your TOTP is enabled
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.
Enter Your Email and Log In
Generate QR Code for TOTP Setup
Scan the QR Code with an 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.
You have successfully set up TOTP-based authentication in your Next.js application.
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.
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:
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.
Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.
Copyright© 2022 湘ICP备2022001581号-3