feat(sso): OSS gated surface for enterprise SSO (OIDC) (#32) - #1303
Conversation
OSS side of two-factor auth (enterprise issue #5). All IP (TOTP verify, secret store, recovery codes, policy) lives in the private trinity-enterprise submodule; this PR adds only the edition-agnostic seam + the entitlement-gated Vue surface, consistent with the open-core pattern (#847). Backend seam: - services/mfa_gate.py — single hook the enterprise provider registers into. No provider (OSS-only build) → gate_login() returns None → login unchanged. Fail-open on provider error (never lock everyone out of login). - dependencies.py — create_mfa_challenge_token / decode_mfa_challenge; a challenge-scoped token is rejected as a session token in get_current_user and decode_token. - routers/auth.py — /token and /api/auth/email/verify consult mfa_gate after the first factor; when a second factor is required they return a short-lived challenge token instead of an access token (audited as mfa_challenge_issued). - models.py — Token gains optional mfa_required / challenge_token fields. - Dockerfile — pyotp (used by the enterprise module running in this image). Frontend (gated by `2fa` in GET /api/settings/feature-flags): - Settings → Security tab (TwoFactorPanel): self-service enroll/confirm/ disable/recovery; admin-only org policy. - Login.vue: second-factor step (verify + forced-enroll) after the password/ email factor; QrCode.vue renders the otpauth URI (graceful manual-key fallback). auth store carries the challenge through to the real token. OSS-only builds: no Security tab, no login step, /api/enterprise/2fa/* → 404. Related to Abilityai/trinity-enterprise#5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
frontend-build npm ci failed — package.json added qrcode@^1.5.4 but package-lock.json was not regenerated, leaving it out of sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Companion to trinity-enterprise#36. OSS carries only the entitlement-gated
surface; all SSO logic lives in the private submodule.
- Login.vue: "Sign in with <IdP>" buttons (shown only when the `sso` feature is
entitled and a provider is enabled), plus OIDC callback-fragment handling
(`/login#sso=ok|mfa|error`) — reuses the existing 2FA challenge UI when the
IdP login still requires a local second factor.
- stores/auth.js: completeSsoLogin() (reuses _finalizeLogin / _setMfaChallenge)
+ fetchSsoProviders() (empty in OSS-only builds — endpoint 404s).
- Settings.vue: admin-gated "SSO" tab → SsoPanel.vue (provider CRUD + test +
policy). Gated by enterpriseStore.isEntitled('sso'), same as the 2FA tab.
- Bump enterprise submodule to the SSO module commit.
- docs: architecture enterprise-modules row + requirements §40 (SSO/OIDC).
No new backend dependency (python-jose + httpx already in the image) and no
OSS Python changes — the mint/whitelist/mfa seams already exist.
Stacked on feat/5-2fa-totp (reuses the OSS mfa_gate + 2FA challenge surface).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pulls in the email_verified / issuer-pinning / login-CSRF fixes (trinity-enterprise 87c8f97). OSS gated surface unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- enterprise gitlink: keep dev's pointer (f1c8caee) — the SSO module (ent#36) already reached enterprise main inside the 2FA squash; the branch's stale pointer (87c8f975, side branch) must not roll dev back - dependencies.py: parent-squash echo; PR doesn't touch it -> dev's version - Login.vue / Settings.vue / auth.js: dev's versions + the SSO-only delta - architecture.md / requirements.md: dev's versions; the PR's module-catalog row and §40 are dropped — enterprise module designs moved to the private repo (ent#45) and the enterprise-docs-guard now fails on enterprise_sso/SCIM tokens; design docs live in trinity-enterprise/docs
vybe
left a comment
There was a problem hiding this comment.
Validated via /validate-pr after resolving the dev merge: effective diff is now purely additive frontend (+235: SsoPanel.vue, Login.vue SSO buttons + #sso= callback, auth.js completeSsoLogin/fetchSsoProviders, Settings SSO tab). Key resolution decisions: (1) enterprise gitlink keeps dev's pointer f1c8caee — the SSO module (ent#36) already reached enterprise main via the 2FA squash, and dev's copy includes a newer JIT-provisioning fix (attribution actor); the branch's stale side-branch pointer would have rolled dev back. (2) The PR's architecture.md module-catalog row + requirements §40 are dropped — enterprise module designs relocated to the private repo (ent#45) and enterprise-docs-guard now fails on enterprise_sso/SCIM/two_factor tokens; the design doc belongs in trinity-enterprise/docs. All SFCs compile; security greps clean. Cross-tracker ent#32 needs a manual status-in-dev bump after merge.
…bilityai#1447) test_submodule_registers_audit_not_sso pinned the enterprise submodule's __init__.py to "no .sso import" — a stale assertion from the Abilityai#910/Abilityai#941 era when only the removed SSO PoC stub existed. Real SSO-OIDC (Abilityai#32) has since landed via Abilityai#1303, so the pin failed on every entitled clone with the submodule mounted (CI is unaffected — it skips when the submodule is absent). Update the pin to the current contract: - keep the direct register_module("audit") assertion (UI-gating module) - replace the two .sso-absence assertions with presence checks on `from .sso import register as register_sso` + `register_sso(app)` (SSO self-registers its feature_id inside .sso.register, so register_module("sso") is intentionally not called in __init__.py) - rename ..._not_sso -> ..._and_sso and refresh the docstring Verified against the mounted submodule: passes with it, skips cleanly without. Fixes Abilityai#1447 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
OSS-side, entitlement-gated surface for enterprise SSO via OIDC. All SSO logic lives in the private submodule (companion: trinity-enterprise#36); OSS carries only the gated UI + the submodule bump.
Related to trinity-enterprise#32.
Changes
<IdP>" buttons, shown only when thessofeature is entitled and a provider is enabled; handles the OIDC callback fragment (/login#sso=ok|mfa|error) and reuses the existing 2FA challenge UI when the IdP login still needs a local second factor.completeSsoLogin()(reuses_finalizeLogin/_setMfaChallenge) +fetchSsoProviders()(empty in OSS-only builds — endpoint 404s).SsoPanel.vue(provider CRUD + connectivity test + policy). Gated byenterpriseStore.isEntitled('sso'), same pattern as the 2FA tab.Why no backend/dep changes
python-jose+httpxare already in the backend image, and the mint (create_access_token), whitelist/default_role(#314), andmfa_gateseams already exist — so OSS needs no Python or Dockerfile change.Notes
feat/5-2fa-totp(reuses the OSSmfa_gate+ 2FA challenge surface) — base is the 2FA branch so the diff is SSO-only. Retarget todevonce 2FA lands; the submodule pointer also settles once both 2FA branches merge.uilabel).🤖 Generated with Claude Code