⚡ You're viewing a live demo of ChimerAI. Data resets daily at midnight UTC.Get the CLI →

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 flagsession.user.mfaVerified for 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 + PathDescription
POST /api/mfa/setupGenerate secret + QR code, store pending (mfaEnabled=false)
POST /api/mfa/verifyConfirm first code — sets mfaEnabled=true
POST /api/mfa/disableDisable MFA (requires valid current code)

UI flow (4 stages)

  1. idle — "Set up 2FA" button
  2. setup — QR code + manual key + code input
  3. done — confirms 2FA is active, option to disable
  4. 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

ChimerAI Docs · Back to Demo