Australian flagJoin us at the FIDO seminar in Melbourne – Feb 7, 2025!
nextjs totp bannerEngineering

How to create a Time-based One-Time Password (TOTP) in Next.js

Learn how to implement a Next.js login page with Time-based One-Time Passwords (TOTPs), in this tutorial.

Vincent Delitz

Vincent

Created: December 30, 2024

Updated: January 15, 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.

1. Introduction#

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:

  • Password-based authentication
  • Email OTP (one-time passcode)
  • SMS OTP (one-time passcode)
  • Social login (OAuth)
  • TOTP (via authenticator app)
  • Passkeys
Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

2. Setting Up Your Next.js Project#

Before we jump into the Time-based One-Time Password (TOTP) authentication, we need to perform some general project setup steps.

2.1 Prerequisites#

To follow this guide, we require some basic understanding of

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
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

3. 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.

TOTP login interface

The following steps cover the implementation of the TOTP-based authentication:

  1. Set Up TOTP-Based Authentication Project
  2. Installing the Relevant Dependencies
  3. Defining the TOTP Schema
  4. Implement API Route For Generating the TOTP Secret and QR Code
  5. Implement API Route For Checking the Two-factor Authentication Status
  6. Implement API Route For Verifying the TOTP
  7. Creating the TOTP Component
  8. Testing the TOTP-based authentication Flow

3.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:

  1. src/models/Totp.ts: This file defines the Mongoose schema for TOTP. It includes fields for email, secret, and TOTP-based authentication status.
  2. 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.
  3. src/pages/api/auth/totp/status.ts: This API endpoint checks whether TOTP-based authentication is enabled for a given user.
  4. src/pages/api/auth/totp/verify.ts: This API endpoint verifies the TOTP entered by the user.
  5. 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.

3.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

3.3 Defining the TOTP Schema#

  1. File Location: src/models/Totp.ts
  2. 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.
  3. 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:

High-level description of TOTPs collection

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;

3.4 Implement API Route For Generating the TOTP Secret and QR Code#

  1. File Location: src/pages/api/auth/totp/generate.ts
  2. 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.
  3. 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;

3.5 Implement API Route For Checking the TOTP Authentication Status#

  1. File Location: src/pages/api/auth/totp/status.ts
  2. 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.
  3. 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.

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;

3.6 Implement API Route For Verifying the TOTP#

  1. File Location: src/pages/api/auth/totp/verify.ts
  2. 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.
  3. 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.

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;

3.7 Creating the TOTP Component#

  1. File Location: src/app/totp/page.tsx
  2. 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.
  3. 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.

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> ); }

3.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
  1. 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.

TOTPs login with email

  1. Enter Your Email and Log In

    • Enter your email address in the email input field.
    • Click the Login with TOTP button.
  2. Generate QR Code for TOTP Setup

    • Click the Generate QR Code button. Generate QR code interface
  3. 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. TOTP code in Google Authenticator
    • Enter this code into the input field provided on the page.
    • Click the Verify Code button.

Verify TOTP interface

  1. 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
  2. Navigate to the TOTP Page

  3. 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.

Logging in with TOTP verification step

  1. 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.

TOTP authentication enabled

You have successfully set up TOTP-based authentication in your Next.js application.

4. Recommendation for Next.js Authentication & Login Pages#

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:

  • Don’t Roll out 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.

5. 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:

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.

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.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free