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
| File | Purpose |
|---|---|
components/features/two-factor-auth/two-factor-setup.tsx | QR code display + 6-digit verification for enabling 2FA |
components/features/two-factor-auth/two-factor-verify.tsx | 6-digit code input for login verification |
Setup
1
Install dependencies
pnpm add otplib qrcode
pnpm add -D @types/qrcode2
Add environment variables
NEXT_PUBLIC_APP_NAME="YourSaaS" # Shown in authenticator apps3
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
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_APP_NAME | No | Name shown in authenticator apps |
Verification
- Go to security settings → click "Enable 2FA"
- Scan the QR code with Google Authenticator, Authy, or 1Password
- Enter the 6-digit code — should show success and enable 2FA
- Sign out and back in — should be prompted for TOTP code
- 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
twoFactorSecretat rest for production - Add recovery codes (8-10 backup codes) during setup for production use cases