Multi-Factor Authentication (MFA / 2FA) Guide
This guide explains the TOTP-based two-factor authentication feature that ships with ChimerAI and how to install it via the CLI.
Installation
chimerai add mfa
This command installs the following files:
| File | Purpose |
|---|---|
app/(app)/settings/mfa/page.tsx | MFA setup and management UI |
lib/mfa.ts | TOTP helper functions |
app/api/mfa/setup/route.ts | Generate a new TOTP secret and QR code |
app/api/mfa/verify/route.ts | Confirm the setup by validating the first code |
app/api/mfa/disable/route.ts | Disable MFA (requires valid code) |
Dependencies installed automatically:
otpauth ^9.0.0
qrcode ^1.5.3
@types/qrcode ^1.5.5
Prisma schema changes:
Two fields are added to the 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
How It Works
ChimerAI uses TOTP (Time-based One-Time Password) as defined in RFC 6238. The user scans a QR code with any authenticator app and then enters 6-digit codes that rotate every 30 seconds.
Supported authenticator apps:
- Google Authenticator
- Authy
- Microsoft Authenticator
- 1Password
- Any TOTP-compatible app
lib/mfa.ts
generateMfaSetup(userEmail, appName?)
Generates a new TOTP secret and returns a QR code data URL ready to be displayed as an <img>. The secret is stored on the user record as mfaSecret with mfaEnabled = false until the user confirms with a valid code.
import { generateMfaSetup } from '@/lib/mfa';
const { secret, qrCodeDataUrl } = await generateMfaSetup('user@example.com', 'MyApp');
Returns:
{
secret: string; // Base32 TOTP secret (store in DB)
otpauthUrl: string; // otpauth:// URI
qrCodeDataUrl: string; // data:image/png;base64,...
}
verifyMfaCode(secret, code)
Validates a 6-digit TOTP code against a stored secret. Accepts a drift window of +-1 period (30 seconds) to handle clock skew.
import { verifyMfaCode } from '@/lib/mfa';
const valid = verifyMfaCode(user.mfaSecret, '123456');
if (!valid) return NextResponse.json({ error: 'Invalid code' }, { status: 400 });
API Routes
POST /api/mfa/setup
Generates a new TOTP secret for the authenticated user, stores it in the DB as pending (mfaEnabled = false), and returns the QR code.
Response:
{
"qrCodeDataUrl": "data:image/png;base64,...",
"secret": "JBSWY3DPEHPK3PXP"
}
POST /api/mfa/verify
Confirms MFA setup by validating the first code entered by the user. On success, sets mfaEnabled = true.
Request body:
{ "code": "123456" }
Response:
{ "success": true }
POST /api/mfa/disable
Disables MFA for the authenticated user. Requires a valid current TOTP code as confirmation.
Request body:
{ "code": "123456" }
Response:
{ "success": true }
On success, clears mfaSecret and sets mfaEnabled = false.
MFA Page
The page at /settings/mfa guides the user through the full setup flow:
- idle - shows a "Set up 2FA" button
- setup - shows the QR code, the manual key, and a code input field
- done - confirms that 2FA is enabled with an option to disable
- disable - asks for a valid code before disabling
The UI handles loading and error states automatically.
Enforcing MFA at Login
After installation, you need to integrate MFA into your NextAuth authorize callback or a middleware check. Example in lib/auth.ts:
// In your credentials provider authorize():
const user = await prisma.user.findUnique({ where: { email } });
if (user.mfaEnabled) {
// Require MFA code as an additional credentials field
const mfaCode = credentials.mfaCode;
if (!mfaCode || !verifyMfaCode(user.mfaSecret, mfaCode)) {
throw new Error('Invalid MFA code');
}
}
Notes
- The TOTP algorithm uses SHA1, 6 digits, 30-second periods - compatible with all major authenticator apps.
- The
secretis stored in plain text in the database. Consider encrypting it at rest for high-security applications. - Backup codes (
MfaBackupCodemodel) are generated as hashed values. The CLI creates the model but does not generate backup codes automatically - this is left as a customization point. - The
verifyMfaCodefunction is synchronous and has no network calls - it is safe to call in middleware.