Two-Factor Auth

TOTP-based 2FA with QR code enrollment. Compatible with Google Authenticator, Authy, and 1Password.

Add TOTP two-factor authentication in ~15 minutes. Uses otplib for code generation/verification and qrcode for QR display.

Files

FilePurpose
components/features/two-factor-auth/two-factor-setup.tsxQR code display + 6-digit verification for enabling 2FA
components/features/two-factor-auth/two-factor-verify.tsx6-digit code input for login verification

Setup

1

Install dependencies

pnpm add otplib qrcode
pnpm add -D @types/qrcode
2

Add environment variables

NEXT_PUBLIC_APP_NAME="YourSaaS"   # Shown in authenticator apps
3

Add Prisma fields

model User {
  // ... existing fields
  twoFactorSecret  String?
  twoFactorEnabled Boolean @default(false)
}

Then run: pnpm prisma migrate dev --name add-two-factor

4

Create the setup route

Create app/api/auth/2fa/setup/route.ts:

import { getRequiredSession } from "@/lib/auth-utils"
import { authenticator } from "otplib"
import QRCode from "qrcode"
import { NextResponse } from "next/server"
import { db } from "@/lib/db"

export async function POST() {
  const session = await getRequiredSession()
  const secret = authenticator.generateSecret()
  const appName = process.env.NEXT_PUBLIC_APP_NAME ?? "YourSaaS"
  const otpauth = authenticator.keyuri(session.user.email, appName, secret)
  const qrCode = await QRCode.toDataURL(otpauth)

  await db.user.update({
    where: { id: session.user.id },
    data: { twoFactorSecret: secret },
    select: { id: true },
  })

  return NextResponse.json({ success: true, data: { qrCode, secret } })
}
5

Create the verify route

Create app/api/auth/2fa/verify/route.ts:

import { getRequiredSession } from "@/lib/auth-utils"
import { authenticator } from "otplib"
import { NextResponse } from "next/server"
import { z } from "zod"
import { db } from "@/lib/db"

const schema = z.object({ code: z.string().length(6) })

export async function POST(request: Request) {
  const session = await getRequiredSession()
  const { code } = schema.parse(await request.json())

  const user = await db.user.findUnique({
    where: { id: session.user.id },
    select: { twoFactorSecret: true },
  })

  if (!user?.twoFactorSecret) {
    return NextResponse.json({ success: false, error: "Start setup first." }, { status: 400 })
  }

  const isValid = authenticator.verify({ token: code, secret: user.twoFactorSecret })
  if (!isValid) {
    return NextResponse.json({ success: false, error: "Invalid code." }, { status: 400 })
  }

  await db.user.update({ where: { id: session.user.id }, data: { twoFactorEnabled: true }, select: { id: true } })
  return NextResponse.json({ success: true, data: { enabled: true } })
}
6

Use the components

// In account security settings
import { TwoFactorSetup } from "@/components/features/two-factor-auth/two-factor-setup"
<TwoFactorSetup />

// During login flow
import { TwoFactorVerify } from "@/components/features/two-factor-auth/two-factor-verify"
<TwoFactorVerify onVerified={() => { window.location.href = "/dashboard" }} />

Environment Variables

VariableRequiredDescription
NEXT_PUBLIC_APP_NAMENoName shown in authenticator apps

Verification

  1. Go to security settings → click "Enable 2FA"
  2. Scan the QR code with Google Authenticator, Authy, or 1Password
  3. Enter the 6-digit code — should show success and enable 2FA
  4. Sign out and back in — should be prompted for TOTP code
  5. Enter a wrong code — should show error and not proceed

Notes

  • Tokens use a 30-second window (otplib default) — compatible with all major authenticator apps
  • Secrets are stored unencrypted by default — consider encrypting twoFactorSecret at rest for production
  • Add recovery codes (8-10 backup codes) during setup for production use cases

Demo Mode — Explore freely. Some actions are restricted. demo@launchfst.dev / demo1234

Get LaunchFst →