Skip to content

auth-mfa

TOTP-based two-factor authentication: enable/disable flow with QR-code setup, 8 single-use recovery codes, and (once wired into a login flow) a second login step after password verification. Secrets are envelope-encrypted at rest via the same MasterKeyProvider as secrets/config.

From recipes-auth-mfa — the smallest working mount:

// Auth MFA (TOTP/2FA) Sample
//
// Shows the production-wiring shape: mount `createAuthMfaFeature()` next to
// `createAuthEmailPasswordFeature()`, thread its status-checker into the
// login handler, and (if `sessions` is mounted) bind its revoke-all-other-
// sessions callback. From there the framework handles the rest — login
// returns a challenge instead of a JWT when MFA is enabled, POST
// /auth/mfa/verify completes it, and every OTHER live session gets signed
// out whenever the account's MFA state changes (enable/disable/regenerate).
//
// What to copy into your own app:
//
// const mfaFeature = createAuthMfaFeature({
// setupTokenSecret: config.mfaSetupSecret,
// challengeTokenSecret: config.mfaChallengeSecret,
// issuer: "Your App",
// });
// const sessionCallbacks = createSessionCallbacks({ db });
// bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(
// sessionCallbacks.sessionRevokeAllOthers,
// );
//
// const server = buildServer({
// registry: buildRegistry([
// createAuthEmailPasswordFeature({
// mfaStatusChecker: mfaStatusCheckerFromFeature(mfaFeature),
// }),
// createSessionsFeature({
// autoRevokeOnPasswordChange: sessionCallbacks.sessionMassRevoker,
// }),
// mfaFeature,
// // ... other features
// ]),
// context: { db, ... },
// jwtSecret: config.jwtSecret,
// auth: {
// membershipQuery: "tenant:query:memberships",
// loginHandler: "auth-email-password:[REDACTED:API key param]",
// mfaVerifyHandler: AuthMfaHandlers.verify,
// sessionCreator: sessionCallbacks.sessionCreator,
// sessionRevoker: sessionCallbacks.sessionRevoker,
// sessionChecker: sessionCallbacks.sessionChecker,
// },
// });
//
// Apps built via runDevApp/runProdApp (the dev-server bootstrap) get all of
// this for free just by adding `createAuthMfaFeature(...)` to APP_FEATURES —
// composeFeatures threads the status-checker and the bootstrap wraps the
// session-revoke bind and the /auth/mfa/verify route automatically. This
// sample shows the raw wiring contract those helpers implement.
//
// Design rules the sample demonstrates:
//
// 1. Login becomes two-step once MFA is enabled. `POST /auth/login` returns
// `{ mfaRequired: true, challengeToken }` instead of a JWT; the client
// completes with `POST /auth/mfa/verify`.
// 2. Enrollment is stateless until confirmed. `enable-start` returns a
// signed setup token (QR + recovery codes) — nothing is persisted until
// `enable-confirm` proves the user scanned it with a valid TOTP code.
// 3. Recovery codes are single-use. Each of the 8 generated codes can
// complete exactly one login challenge, then it's burned.
// 4. Every MFA state change signs out every OTHER live session (stolen-
// session defense) — but never the session that made the change.
export {
AuthMfaHandlers,
base32Decode,
bindMfaRevokeAllOtherSessionsFromFeature,
createAuthMfaFeature,
currentTotpCode,
mfaStatusCheckerFromFeature,
userMfaEntity,
} from "@cosmicdrift/kumiko-bundled-features/auth-mfa";

📄 On GitHub: samples/recipes/auth-mfa/src/feature.ts

auth-mfa feature preview

What this feature needs to run (Requires, top) and the write commands it provides (Provides, bottom).

flowchart TB
  n_auth_mfa["auth-mfa"]
  subgraph how_reqs["Requires"]
    n_user["user"]
    n_config["config"]
  end
  subgraph how_provides["Provides"]
    n_cmd_auth_mfa_write_disable(["disable"])
    n_cmd_auth_mfa_write_enable_confirm(["enable-confirm"])
    n_cmd_auth_mfa_write_enable_start(["enable-start"])
    n_cmd_auth_mfa_write_regenerate_recovery(["regenerate-recovery"])
    n_cmd_auth_mfa_write_verify(["verify"])
  end
  n_user --> n_auth_mfa
  n_config --> n_auth_mfa
  n_auth_mfa --> n_cmd_auth_mfa_write_disable
  n_auth_mfa --> n_cmd_auth_mfa_write_enable_confirm
  n_auth_mfa --> n_cmd_auth_mfa_write_enable_start
  n_auth_mfa --> n_cmd_auth_mfa_write_regenerate_recovery
  n_auth_mfa --> n_cmd_auth_mfa_write_verify

Provides — write commands this feature registers (dispatch them through the command bus):

Start with recipes-auth-mfa for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: user, config
  • Activation: always on (not toggleable)

Per-tenant config keys, set via the tenant-admin UI or a seed. 🔒 = encrypted at rest.

KeyTypeDefaultScopeWho can writeWho can read
requiredselect (optional | admins | all)optionaltenantTenantAdmin, Admin, SystemAdminall