Multi-Factor Authentication (MFA / TOTP)
ChimerAI ships a complete TOTP-based MFA implementation compatible with Google Authenticator, Authy, and any RFC 6238–compliant app.
What you get
- TOTP setup flow — QR code generation, secret enrollment
- Verification endpoint — validates 6-digit codes
- Recovery codes — one-time backup codes
- MFA enforcement — force MFA for specific roles or orgs
- Session flag —
session.user.mfaVerifiedfor conditional access
Quick setup
npx chimerai add mfa
Scaffolds:
app/(app)/settings/mfa/page.tsx ← Setup & management UI
lib/mfa.ts ← TOTP helper functions
app/api/mfa/setup/route.ts ← Generate secret + QR code
app/api/mfa/verify/route.ts ← Confirm setup with first code
app/api/mfa/disable/route.ts ← Remove MFA from account
Dependencies installed automatically:
otpauth ^9.0.0
qrcode ^1.5.3
@types/qrcode ^1.5.5
Prisma schema changes
Two fields are added to your User model and a new MfaBackupCode model is created:
// Added to User model:
mfaSecret String?
mfaEnabled Boolean @default(false)
mfaBackupCodes MfaBackupCode[]
model MfaBackupCode {
id String @id @default(cuid())
userId String
codeHash String
usedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
After installation run:
npx prisma migrate dev --name add-mfa
lib/mfa.ts functions
import { generateMfaSetup, verifyMfaCode } from '@/lib/mfa';
// 1. Generate a new secret + QR code (show to user during setup)
const { secret, qrCodeDataUrl } = await generateMfaSetup(user.email, 'MyApp');
// Store secret as mfaSecret (with mfaEnabled = false) until user confirms
// 2. Verify a 6-digit code — accepts ±1 period drift (30 s) for clock skew
const valid = verifyMfaCode(user.mfaSecret, '123456');
if (!valid) return NextResponse.json({ error: 'Invalid code' }, { status: 400 });
Both functions are synchronous except generateMfaSetup (async QR generation). verifyMfaCode has no network calls — safe to use in middleware.
API routes
| Method + Path | Description |
|---|---|
POST /api/mfa/setup | Generate secret + QR code, store pending (mfaEnabled=false) |
POST /api/mfa/verify | Confirm first code — sets mfaEnabled=true |
POST /api/mfa/disable | Disable MFA (requires valid current code) |
UI flow (4 stages)
- idle — "Set up 2FA" button
- setup — QR code + manual key + code input
- done — confirms 2FA is active, option to disable
- disable — asks for a valid code before disabling
Enforcing MFA at login
// lib/auth.ts — inside CredentialsProvider.authorize()
const user = await prisma.user.findUnique({ where: { email } });
if (user.mfaEnabled) {
const mfaCode = credentials.mfaCode; // extra field in sign-in form
if (!mfaCode || !verifyMfaCode(user.mfaSecret, mfaCode)) {
throw new Error('Invalid MFA code');
}
}
Further reading
- MFA Guide — complete reference with backup codes and full API spec
- otplib docs
- RFC 6238 — TOTP