Learn how to implement a Next.js login page with Time-based One-Time Passwords (TOTPs), in this tutorial.
Vincent
Created: December 30, 2024
Updated: February 17, 2025
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
Next.js is a powerful framework that allows developers to build fast and user-friendly web applications. One of the most critical aspects of any web application is user authentication.
In this guide, we'll walk you through the process of implementing a login page in Next.js, with Time-based One-Time Password (TOTP) authentication.
If you want to see the complete code and check out other authenciation methods, use our Next.js login page repository on GitHub. There you will find information regarding:
Recent Articles
♟️
Initial Assessment & Planning (Enterprise Passkeys Guide 1)
♟️
Stakeholder Engagement (Enterprise Passkeys Guide 2)
♟️
Product, Design & Strategy Development (Enterprise Passkeys Guide 3)
♟️
Integrating Passkeys into Enterprise Stack (Enterprise Passkeys Guide 4)
♟️
Testing Passkey Implementations (Enterprise Passkeys Guide 5)
Before we jump into the Time-based One-Time Password (TOTP) authentication, we need to perform some general project setup steps.
To follow this guide, we require some basic understanding of
Open your terminal and run the following command to create a new Next.js project:
npx create-next-app@latest nextjs-auth-methods
In the installation guide steps, we select the following:
Navigate to your project directory:
cd nextjs-login
To verify that your Next.js project is set up correctly, start the development server:
npm run dev
Open your browser and navigate to http://localhost:3000
. You should see the default Next.js welcome page.
Create a .env.local
file in the root of your project to store environment variables. Add your variables here:
MONGODB_URI=your_database_connection_string GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret TWILIO_ACCOUNT_SID=your_twilio_account_sid TWILIO_AUTH_TOKEN=your_twilio_auth_token TWILIO_PHONE_NUMBER=your_twilio_phone_number
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 cover 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:
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.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
src/models/Totp.ts
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;
src/pages/api/auth/totp/generate.ts
connectDb
to connect to the MongoDB database.
qrcode
to generate a QR code from the TOTP secret key's URL.totpEnabled
to false for the user in the database.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;
src/pages/api/auth/totp/status.ts
connectDb
to connect to the MongoDB database.Totp
collection by email.totpEnabled
as a JSON response if the user is found, otherwise returns an error message.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;
src/pages/api/auth/totp/verify.ts
connectDb
to connect to the MongoDB database.totpEnabled
status to true for the user in the database.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;
src/app/totp/page.tsx
useState
to manage state variables such as email, QR code, token, verification status, errors, and TOTP status.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.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> ); }
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.
http://localhost:3000/totp
http://localhost:3000/totp
- You should see the login page with an email input field and a Login with TOTP
button.Enter Your Email and Log In
Login with TOTP
button.Generate QR Code for TOTP Setup
Generate QR Code
button.
Scan the QR Code with an Authenticator App
Verify Code
button.Verification and Enabling TOTP
Logout
button will be displayed.Navigate to the TOTP Page
Enter the TOTP Code from the Authenticator App
Logout
button will be displayed.You have successfully set up TOTP-based authentication in your Next.js application.
Event though we just looked at how to roll out TOTP authencication, which is not the safest auth method, building a secure and user-friendly authentication system is crucial to protect user data and provide a great first impression of your app. Authentication is often the first interaction a user has with your product and the first thing they will complain about if something goes wrong. Besides the detailed steps laid out above, we also have some additional best practices for Next.js authentication and login pages:
By following these practices, you'll keep your application robust against common threats and ensure a safe environment for your users. Secure authentication not only protects your users but also builds trust and credibility for your application.
In this Next.js login page guide, we explored various authentication methods to secure your Next.js applications. Here’s a recap of what we covered:
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.
Choosing the right authentication method for your application depends on various factors, including security requirements, user convenience, and the nature of your application. Each method we covered has its strengths and can be used alone or in combination to provide a robust authentication system. Experiment with different methods, gather user feedback, and iterate on your implementation to achieve the best balance for your specific needs.
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.