How to Implement Two-Factor Authentication (2FA) in Next.js

Complete guide to implementing TOTP-based two-factor authentication in your Next.js application with otplib, QR codes, and backup codes.

December 9, 2024
3 min read
By FastSaaS Team

How to Implement Two-Factor Authentication (2FA) in Next.js

Two-factor authentication (2FA) is essential for securing user accounts. This guide shows you how to implement TOTP-based 2FA in your Next.js application.

What is TOTP?

Time-based One-Time Password (TOTP) is an algorithm that generates a unique 6-digit code every 30 seconds using a shared secret key. Popular apps like Google Authenticator and Authy use this standard.

Prerequisites

Install the required packages:

npm install otplib qrcode @types/qrcode

Step 1: Generate Secret and QR Code

// lib/auth/two-factor.ts
import { authenticator } from "otplib";
import QRCode from "qrcode";

export async function generateTwoFactorSecret(email: string) {
  const secret = authenticator.generateSecret();
  const appName = "FastSaaS";

  const otpAuthUrl = authenticator.keyuri(email, appName, secret);
  const qrCodeDataUrl = await QRCode.toDataURL(otpAuthUrl);

  return {
    secret,
    qrCodeDataUrl,
    otpAuthUrl,
  };
}

Step 2: Verify TOTP Codes

export function verifyTOTP(secret: string, token: string): boolean {
  return authenticator.verify({
    token,
    secret,
  });
}

Step 3: Generate Backup Codes

import crypto from "crypto";

export function generateBackupCodes(count: number = 8): string[] {
  const codes: string[] = [];

  for (let i = 0; i < count; i++) {
    const code = crypto.randomBytes(4).toString("hex").toUpperCase();
    codes.push(`${code.slice(0, 4)}-${code.slice(4, 8)}`);
  }

  return codes;
}

Step 4: Database Schema (Prisma)

model User {
  id                  String    @id @default(cuid())
  email               String    @unique
  twoFactorEnabled    Boolean   @default(false)
  twoFactorSecret     String?
  twoFactorBackupCodes String[]
}

Step 5: Enable 2FA API Route

// app/api/auth/two-factor/enable/route.ts
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { generateTwoFactorSecret } from "@/lib/auth/two-factor";
import { prisma } from "@/lib/prisma";

export async function POST() {
  const session = await getServerSession();

  if (!session?.user?.email) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { secret, qrCodeDataUrl } = await generateTwoFactorSecret(
    session.user.email
  );

  // Store the secret temporarily (not enabled yet)
  await prisma.user.update({
    where: { email: session.user.email },
    data: { twoFactorSecret: secret },
  });

  return NextResponse.json({ qrCodeDataUrl });
}

Step 6: Verify and Activate 2FA

// app/api/auth/two-factor/verify/route.ts
import { NextResponse } from "next/server";
import { verifyTOTP, generateBackupCodes } from "@/lib/auth/two-factor";

export async function POST(request: Request) {
  const { code } = await request.json();
  const session = await getServerSession();

  const user = await prisma.user.findUnique({
    where: { email: session?.user?.email! },
  });

  if (!user?.twoFactorSecret) {
    return NextResponse.json(
      { error: "No 2FA setup in progress" },
      { status: 400 }
    );
  }

  if (!verifyTOTP(user.twoFactorSecret, code)) {
    return NextResponse.json({ error: "Invalid code" }, { status: 400 });
  }

  const backupCodes = generateBackupCodes();

  await prisma.user.update({
    where: { id: user.id },
    data: {
      twoFactorEnabled: true,
      twoFactorBackupCodes: backupCodes,
    },
  });

  return NextResponse.json({ success: true, backupCodes });
}

Step 7: Login Flow with 2FA

// Modify your login callback
if (user.twoFactorEnabled) {
  // Don't complete login yet, require 2FA code
  return {
    requiresTwoFactor: true,
    userId: user.id,
  };
}

Security Best Practices

  1. Encrypt secrets at rest - Use encrypted database fields
  2. Hash backup codes - Store hashed versions, invalidate after use
  3. Rate limit verification - Prevent brute force attacks
  4. Provide recovery options - Email-based recovery as fallback

Conclusion

2FA significantly improves your application's security. FastSaaS includes a complete 2FA implementation out of the box, saving you development time.

Get FastSaaS with built-in 2FA support!