kavachOS
Authentication

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 kavachos

Add the plugin

lib/kavach.ts
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.

EndpointDescription
POST /auth/2fa/enrollGenerate a TOTP secret and backup codes
POST /auth/2fa/verifyConfirm enrollment or complete login
POST /auth/2fa/disableDisable 2FA, requires current code
GET /auth/2fa/statusCheck whether 2FA is enabled
POST /auth/2fa/backup-codesRegenerate backup codes

Options

OptionTypeDefaultDescription
issuerstringrequiredApp name shown in the authenticator
enforcebooleanfalseRequire 2FA for all users
shouldEnforce(user: User) => Promise<boolean>Per-user enforcement logic
backupCodeCountnumber10Number of backup codes generated at enrollment
challengeTokenTtlnumber300Seconds to complete verification after primary auth

On this page