You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 andPOST /api/auth/email/verify), the router consults a registered SecondFactorProvider (if any) before issuing the JWT.
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.
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-flags → enterprise_features; OSS Vue bundle shows the account-settings 2FA panel + the admin 2FA-status column only when entitled.
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.
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.
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-enterpriserepo, 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):POST /api/tokenpassword login andPOST /api/auth/email/verify), the router consults a registeredSecondFactorProvider(if any) before issuing the JWT.users.suspended_at+get_current_userenforcement 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.mfa_pendingchallenge token (not a full JWT) returned when a provider reports the user requires a second factor.Enterprise module owns (
enterprise/backend/two_factor/, gated byrequires_entitlement("two_factor")):pyotp), QR provisioning URI, backup/recovery codes, admin-enforcement policy, per-user enable/disable, verification rate-limiting.SecondFactorProviderinto the OSS seam viaregister_module("two_factor")inregister_enterprise.enterprise_2fa_*tables (FK into OSSusers.id, never ALTERusers).Phase 1 (this issue)
SecondFactorProviderinterface + null default + the two auth call sites (password login, email verify) +mfa_pendingchallenge token. Behavior-preserving when no provider is registered (regression-tested).POST /api/enterprise/2fa/setup(generate secret, returnotpauth://provisioning URI / QR data),POST /api/enterprise/2fa/verify(confirm first code, enable),DELETE /api/enterprise/2fa(disable; requires a current code).POST /api/enterprise/2fa/challengeexchanges a validmfa_pendingtoken + TOTP/backup code for the real JWT.enterprise_schema_migrations):enterprise_2fa_secrets—user_idFK,totp_secret(AES-256-GCM via OSSCredentialEncryptionService),enabled,confirmed_at, timestamps.enterprise_2fa_backup_codes—user_idFK,code_hash,consumed_at(10 single-use codes, stored hashed).enterprise_2fa_policy— which roles are mandatory (default:admin), enrollment grace window.enterprise_2fa_secretsrow).services/rate_limiter.py); lock after repeated failures (e.g. 5).two_factorappears inGET /api/settings/feature-flags→enterprise_features; OSS Vue bundle shows the account-settings 2FA panel + the admin 2FA-status column only when entitled.Later phases
SecondFactorProvideron the same seam.Architectural notes
usersbut must never ALTER it. All 2FA state lives inenterprise_2fa_*tables on the private two-track runner — not the original plan'susers.totp_*columns.register_module("two_factor")inregister_enterprise.TRINITY_OSS_ONLY=1and OSS-only builds (submodule absent) hide it entirely — no import errors, enterprise endpoints 404, the OSS auth path runs with the null provider.CredentialEncryptionService(AES-256-GCM), consistent with Invariant fix: Add ARM64 Alpine compatibility for frontend Docker build #12.Out of scope
Reframed from a core feature to an enterprise module on the #847 seam (2026-06-08). The TOTP storage moved off the OSS
userstable per Invariant #3; the originalusers.totp_*ALTER approach is superseded by theenterprise_2fa_*tables above.