Skip to content

Enterprise: two-factor authentication (2FA/TOTP) — mandatory for admins, optional for users (#847 seam) #388

Description

@vybe

Summary

Add two-factor authentication (2FA / TOTP) as an enterprise module on the #847 entitlement seam. 2FA is mandatory for admin accounts and optional for regular users. Entitlement id: two_factor.

For a platform managing autonomous agents with access to credentials and external services, a second authentication factor materially reduces account-compromise risk — and it's table-stakes for the SOC2/ISO posture the Enterprise edition targets.

Open-core split

Per the #847 directive that enterprise features live entirely in the private Abilityai/trinity-enterprise repo, this follows the #995 "OSS core-primitive + enterprise knob" shape (not the fully-private #997 shape) — because 2FA enforcement sits on the OSS login path, which can't be moved into the private module.

OSS ships (edition-agnostic, no ALTER of users):

  • A pluggable second-factor provider seam in the auth path. After primary auth succeeds (POST /api/token password login and POST /api/auth/email/verify), the router consults a registered SecondFactorProvider (if any) before issuing the JWT.
  • Default OSS build registers no provider → behaves exactly as today (no second factor, no new tables touched). This mirrors users.suspended_at + get_current_user enforcement from Enterprise: User & Organization Management (Org/Team + advanced RBAC) on the #847 seam #995: OSS owns the primitive + the gate; only the enterprise module supplies the implementation.
  • A short-lived mfa_pending challenge token (not a full JWT) returned when a provider reports the user requires a second factor.

Enterprise module owns (enterprise/backend/two_factor/, gated by requires_entitlement("two_factor")):

  • TOTP secret generation/verification (pyotp), QR provisioning URI, backup/recovery codes, admin-enforcement policy, per-user enable/disable, verification rate-limiting.
  • Registers the TOTP SecondFactorProvider into the OSS seam via register_module("two_factor") in register_enterprise.
  • Its own private enterprise_2fa_* tables (FK into OSS users.id, never ALTER users).

Phase 1 (this issue)

  • OSS: SecondFactorProvider interface + null default + the two auth call sites (password login, email verify) + mfa_pending challenge token. Behavior-preserving when no provider is registered (regression-tested).
  • Enterprise: TOTP enrollment — POST /api/enterprise/2fa/setup (generate secret, return otpauth:// provisioning URI / QR data), POST /api/enterprise/2fa/verify (confirm first code, enable), DELETE /api/enterprise/2fa (disable; requires a current code).
  • Enterprise: login completion — POST /api/enterprise/2fa/challenge exchanges a valid mfa_pending token + TOTP/backup code for the real JWT.
  • Private tables (two-track migration, enterprise_schema_migrations):
    • enterprise_2fa_secretsuser_id FK, totp_secret (AES-256-GCM via OSS CredentialEncryptionService), enabled, confirmed_at, timestamps.
    • enterprise_2fa_backup_codesuser_id FK, code_hash, consumed_at (10 single-use codes, stored hashed).
    • enterprise_2fa_policy — which roles are mandatory (default: admin), enrollment grace window.
  • Admin enforcement: admins with the module entitled must complete enrollment on first login after deployment; the challenge is required on every subsequent login, checked on the login path (not just at role assignment).
  • Recovery: backup codes for self-service; documented CLI/super-admin reset for a fully locked-out admin (clears the user's enterprise_2fa_secrets row).
  • Rate-limit 2FA verification via the shared sliding-window limiter (services/rate_limiter.py); lock after repeated failures (e.g. 5).
  • Frontend gating: two_factor appears in GET /api/settings/feature-flagsenterprise_features; OSS Vue bundle shows the account-settings 2FA panel + the admin 2FA-status column only when entitled.
  • Unit tests: provider seam (registered vs null), TOTP enroll/verify/disable, backup-code single-use, challenge-token exchange + expiry, admin enforcement, rate-limit lockout, secret encryption, entitlement gate (403 unentitled / 404 OSS-only).

Later phases

Architectural notes

  • Invariant Feature/vector log retention #3: enterprise migrations may FK into OSS users but must never ALTER it. All 2FA state lives in enterprise_2fa_* tables on the private two-track runner — not the original plan's users.totp_* columns.
  • Router → service → db inside the private module; entitlement gate on every enterprise endpoint; register_module("two_factor") in register_enterprise.
  • TRINITY_OSS_ONLY=1 and OSS-only builds (submodule absent) hide it entirely — no import errors, enterprise endpoints 404, the OSS auth path runs with the null provider.
  • TOTP secrets encrypted at rest via the existing CredentialEncryptionService (AES-256-GCM), consistent with Invariant fix: Add ARM64 Alpine compatibility for frontend Docker build #12.

Out of scope

  • Moving the OSS email-code / admin-password login itself into the enterprise module (stays OSS — the seam is additive).
  • License/entitlement issuance mechanism — tracked by the spike #1040.

Reframed from a core feature to an enterprise module on the #847 seam (2026-06-08). The TOTP storage moved off the OSS users table per Invariant #3; the original users.totp_* ALTER approach is superseded by the enterprise_2fa_* tables above.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions