Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

Why this exists

MFA enforcement protects against credential-leak risk on admin accounts. Even if an attacker steals a session cookie or API password, they cannot mutate decisioning configuration (offers, contact policies, decision flows, MCP playbooks, models, approvals, etc.) without the second factor. The enforcement runs in edge middleware for low latency — typically <5ms additional overhead per request — and applies to every state-changing HTTP method (POST / PUT / PATCH / DELETE) on /api/* routes.

How the flow works

  1. Admin signs in with email + password (or Google OAuth).
  2. If the user has MFA enabled, NextAuth issues a JWT with mfaPending = true and mfaVerifiedAt = null.
  3. Admin attempts a state-changing API call. Middleware sees mfaPending && !fresh(mfaVerifiedAt) and returns 403 MFA_REQUIRED with this body:
    {
      "error": {
        "code": "MFA_REQUIRED",
        "message": "Admin accounts require MFA verification within the last 15 minutes for write operations.",
        "status": 403,
        "hint": "POST /api/v1/auth/mfa with { action: 'verify', token: '<TOTP>' }, then call useSession().update({ mfaVerifiedAt: new Date().toISOString() })."
      }
    }
    
  4. Client calls POST /api/v1/auth/mfa with { action: "verify", token: "<6-digit TOTP>" }. Returns 200 { verified: true } on success or 400 on bad token.
  5. Client calls useSession().update({ mfaVerifiedAt: new Date().toISOString() }) to refresh the JWT — the auth.ts JWT callback handles the trigger="update" path and writes the timestamp.
  6. Subsequent admin writes pass for 15 minutes, after which the user must re-verify.

Step-up TTL

Default is 15 minutes. The TTL is intentionally short to limit blast radius if a session is hijacked. Sliding-window: every successful verify resets the 15-minute timer. The TTL is hardcoded in platform/src/middleware.ts as MFA_STEPUP_TTL_MS = 15 * 60 * 1000. To change it, modify the constant and redeploy. There is no per-tenant configuration today.

What’s bypassed

  • GET / HEAD / OPTIONS requests — no enforcement (read-only)
  • Non-admin users — no enforcement (the gate is admin-only)
  • Users with MFA not enabled — no enforcement (mfaPending is false)
  • /api/v1/auth/mfa itself — must be reachable to do the verify
  • /api/auth/* (NextAuth handlers) — needed for sign-in flow
  • API-key-authenticated server-to-server calls — these don’t carry a JWT and are gated separately by API-key scope

Kill switch (incident only)

Set MFA_ENFORCEMENT_DISABLED=true in the deployment environment to bypass enforcement for all requests. This is only for incident recovery — for example, if the TOTP server’s clock is drifting and legitimate codes are being rejected. When set, requests that would have been blocked are passed through with no logging change. Set the env var back to false (or unset) and redeploy to re-enable.

Operator runbook

SymptomLikely causeAction
All admin writes returning 403 MFA_REQUIREDAdmin has MFA enabled but never completed verifyHave admin do POST /api/v1/auth/mfa { action: "verify" } then update session
Admin verified successfully but next write returns 403JWT didn’t receive the timestamp from useSession().update()Check client-side: useSession().update({ mfaVerifiedAt: new Date().toISOString() }) must be called after the verify response
403 returns even though admin verified < 15 min agoClock drift between auth server and middleware hostCheck NTP on both hosts. Or temporarily disable via kill switch
All non-admin users returning 403Bug — non-admins shouldn’t be gatedFile issue immediately. Set kill switch as workaround
Production-grade incident: locked out of adminKill switch not yet set, need access NOWSet MFA_ENFORCEMENT_DISABLED=true in env, redeploy. Re-enable once admin can verify

What’s in scope (W2.2 baseline)

  • ✅ Edge middleware enforcement on all POST/PUT/PATCH/DELETE to /api/*
  • mfaVerifiedAt JWT timestamp + 15-minute sliding window
  • MFA_ENFORCEMENT_DISABLED env kill switch
  • ✅ Source-level regression tests at platform/src/__tests__/middleware-mfa.test.ts
  • ✅ MFA verify endpoint already existed at POST /api/v1/auth/mfa

Out of scope (later)

  • Per-tenant TTL configuration
  • WebAuthn / Passkey support (currently only TOTP + backup codes)
  • Audit log entry on every verify (currently logged at info via logger)
  • IP-based step-up (require fresh verify when source IP changes mid-session)

Source files

  • platform/src/middleware.ts — enforcement
  • platform/src/lib/auth.ts — JWT callback writes mfaVerifiedAt on trigger="update"
  • platform/src/app/api/v1/auth/mfa/route.ts — verify endpoint (TOTP validation, backup-code support, rate limiting)
  • platform/src/__tests__/middleware-mfa.test.ts — regression test