Two-factor auth
TOTP-based second factor with backup codes.
The twoFactor() plugin adds TOTP (time-based one-time password) support compatible with Google Authenticator, Authy, 1Password, and any RFC 6238 app. Users enroll once by scanning a QR code, then provide a 6-digit code on every sign-in.
Setup
Install
pnpm add kavachosAdd the plugin
import { createKavach } from 'kavachos';
import { emailPassword } from 'kavachos/plugins/email-password';
import { twoFactor } from 'kavachos/plugins/two-factor';
const kavach = await createKavach({
database: { provider: 'postgres', url: process.env.DATABASE_URL! },
secret: process.env.KAVACH_SECRET!,
baseUrl: 'https://auth.example.com',
plugins: [
emailPassword(),
twoFactor({
issuer: 'My App', // Shown in the authenticator app
}),
],
});twoFactor() works with any primary auth method. Pair it with OAuth providers or magic link as well as email/password.
Enrollment flow
All enrollment endpoints require an active session.
Generate a TOTP secret
POST /auth/2fa/enroll
const res = await fetch('/auth/2fa/enroll', {
method: 'POST',
credentials: 'include',
});
const { secret, qrCodeUrl, backupCodes } = await res.json(); Render qrCodeUrl as a QR code image in your UI. The user scans it with their authenticator app. Show backupCodes once at this point — they cannot be retrieved again.
Confirm with the first code
Ask the user to enter the code their authenticator app shows to confirm enrollment:
POST /auth/2fa/verify
await fetch('/auth/2fa/verify', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: '482910' }),
});A successful response activates 2FA on the account.
Login verification
When a user with 2FA enabled signs in, the primary auth endpoint returns a partial response instead of a session:
{ "status": "two_factor_required", "challengeToken": "..." }Submit the TOTP code along with the challenge token to complete sign-in:
POST /auth/2fa/verify
const res = await fetch('/auth/2fa/verify', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challengeToken, // From the primary auth response
code: '482910', // From the user's authenticator app
}),
});On success, a session cookie is set.
Backup codes
Ten single-use backup codes are generated at enrollment. Each is a 10-character alphanumeric string. A backup code can be submitted in place of a TOTP code at the verify endpoint — useful when the user has lost access to their authenticator app.
Regenerate backup codes
POST /auth/2fa/backup-codes
This invalidates all existing backup codes and issues a new set. Requires an active session.
const res = await fetch('/auth/2fa/backup-codes', {
method: 'POST',
credentials: 'include',
});
const { backupCodes } = await res.json();Disabling 2FA
POST /auth/2fa/disable
Requires the user's current TOTP code as confirmation:
await fetch('/auth/2fa/disable', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: '482910' }),
});Check enrollment status
GET /auth/2fa/status
const res = await fetch('/auth/2fa/status', { credentials: 'include' });
const { enabled, enrolledAt } = await res.json();Endpoints
All endpoints require an authenticated session unless noted.
| Endpoint | Description |
|---|---|
POST /auth/2fa/enroll | Generate a TOTP secret and backup codes |
POST /auth/2fa/verify | Confirm enrollment or complete login |
POST /auth/2fa/disable | Disable 2FA, requires current code |
GET /auth/2fa/status | Check whether 2FA is enabled |
POST /auth/2fa/backup-codes | Regenerate backup codes |
Options
| Option | Type | Default | Description |
|---|---|---|---|
issuer | string | required | App name shown in the authenticator |
enforce | boolean | false | Require 2FA for all users |
shouldEnforce | (user: User) => Promise<boolean> | — | Per-user enforcement logic |
backupCodeCount | number | 10 | Number of backup codes generated at enrollment |
challengeTokenTtl | number | 300 | Seconds to complete verification after primary auth |