diff --git a/docs/memory/architecture.md b/docs/memory/architecture.md
index df826bee..939d038a 100644
--- a/docs/memory/architecture.md
+++ b/docs/memory/architecture.md
@@ -699,6 +699,7 @@ Open-core seam: enterprise backend code lives in the private `trinity-enterprise
| `user_management` | `enterprise/backend/user_management/` (#995) | Org lifecycle: invite (whitelist + email), deactivate/reactivate (over the OSS `users.suspended_at` primitive), per-user activity view (reads OSS `audit_log`). `/api/enterprise/user-management/*`; Settings → User Management UI |
| `siem` | `enterprise/backend/siem/` (#997) | SIEM log export — ships OSS `audit_log` to a customer SIEM over HTTP/JSON webhook. Private `enterprise_siem_config` (destination + AES-encrypted token + export cursor); Redis-lock-serialised background pusher; at-least-once (cursor advances only on successful POST). `/api/enterprise/siem/*`; no OSS/UI surface |
| `2fa` | `enterprise/backend/two_factor/` (#5) | Two-factor auth via **TOTP** (RFC 6238; Google Authenticator is one compatible app — chosen over Google-OIDC to avoid a runtime IdP dependency). Private `enterprise_user_2fa` (AES-256-GCM secret + monotonic `last_used_step` replay guard), `enterprise_2fa_recovery_codes` (single-use, SHA-256-hashed), `enterprise_2fa_config` (per-role policy). `/api/enterprise/2fa/*` — self-service enroll/confirm/disable/recovery + admin policy + the `login/*` challenge-completion endpoints. Login seam is edition-agnostic: `services/mfa_gate.py` (no provider in OSS → no-op, fail-open) + `create_mfa_challenge_token`/`decode_mfa_challenge` in `dependencies.py`; on a required second factor the OSS auth routers (`/token`, `/api/auth/email/verify`) return a short-lived challenge token instead of an access token. Settings → Security UI |
+| `sso` | `enterprise/backend/sso/` (trinity-enterprise#32) | Single Sign-On via **OIDC** (authorization-code + PKCE; SAML is a tracked follow-up). Private `enterprise_sso_providers` (per-IdP config; AES-256-GCM `client_secret`, `protocol`-tagged for future SAML rows) + single-row `enterprise_sso_config` (password-fallback / auto-provision / default role). `/api/enterprise/sso/*` — admin provider CRUD + connectivity test + policy (admin-gated), `GET /public-providers` (unauth, `{id,name}` of enabled IdPs for login buttons), and the unauth `login/{id}` → IdP → `callback` flow. Dependency-free (httpx + python-jose, already in-image). State/nonce/PKCE-verifier held single-use in Redis (`sso_state:*`, GETDEL, TTL); id_token verified against IdP JWKS (asymmetric algs only — no `none`/`HS*`), `iss`/`aud`/`exp`/`nonce` checked. Edition-agnostic mint: the callback exchanges the validated assertion for a **standard platform JWT** via OSS `create_access_token` (no second session mechanism) and runs `mfa_gate` so local 2FA still applies; JIT provisioning honors the OSS whitelist + `default_role` (#314, no silent elevation). Settings → SSO UI + login-page buttons |
---
diff --git a/docs/memory/requirements.md b/docs/memory/requirements.md
index 78d44834..681f8d07 100644
--- a/docs/memory/requirements.md
+++ b/docs/memory/requirements.md
@@ -2876,6 +2876,54 @@ Standalone mobile-friendly admin page for managing agents on the go. Designed as
---
+## 40. Enterprise Single Sign-On — OIDC (trinity-enterprise#32)
+
+> Section number provisional — this branch is based on the 2FA branch (behind
+> `dev`); the final number resolves on rebase to `dev`. Enterprise feature on
+> the #847 seam; most logic lives in the private `enterprise/backend/sso/`
+> submodule. This documents the OSS-visible contract + the security rules.
+
+**Description**: Enterprise customers sign in through their identity provider
+(Okta, Entra ID, Google Workspace) instead of (or alongside) the OSS
+email-code / admin-password flow. **OIDC-first**; SAML 2.0 is a tracked
+follow-up (needs a native xmlsec dependency). Ships as an enterprise module —
+`register_module("sso")`, endpoints gated by `requires_entitlement("sso")`,
+zero OSS behavior change when the submodule is absent.
+
+- **FR-1 — Provider config (admin)**: CRUD over multiple named OIDC providers
+ (issuer/discovery URL, client id, client secret, scopes, enabled) +
+ connectivity test. Client secret is **write-only** (AES-256-GCM at rest,
+ Invariant #12) — never returned. Settings → SSO UI, admin-gated.
+- **FR-2 — OIDC login**: authorization-code flow with **PKCE**, single-use
+ `state` + `nonce` held in Redis (`GETDEL`, TTL). `id_token` validated against
+ the IdP JWKS restricted to **asymmetric** algorithms (no `none`/`HS*`);
+ `iss`/`aud`/`exp` checked by python-jose, `nonce` matched. Dependency-free
+ (httpx + python-jose, already in-image).
+- **FR-3 — Single mint path**: the callback exchanges the validated assertion
+ for a **standard platform JWT** via the OSS `create_access_token` — no second
+ session mechanism (Invariant: OSS token issuance stays the single mint path).
+- **FR-4 — 2FA composition (#5)**: after IdP auth the OSS `mfa_gate` still
+ runs, so a local second factor can be demanded; the callback returns a 2FA
+ challenge instead of a token in that case.
+- **FR-5 — JIT provisioning (#314)**: an unknown email is rejected unless
+ `auto_provision` is enabled, then added to the whitelist at the configured
+ `default_role` — never silently elevated. Suspended accounts are refused.
+- **FR-6 — Coexistence + break-glass**: OSS email/admin login stays available
+ (configurable `allow_password_fallback`); admin password login always works.
+- **FR-7 — Audit**: `login_success` / `login_failure` / `mfa_challenge_issued`
+ written to the OSS `audit_log` (`method: "sso"`).
+
+**API** (enterprise, gated): `GET/POST/PUT/DELETE /api/enterprise/sso/providers`,
+`POST .../providers/{id}/test`, `GET/PUT .../config` (admin); `GET
+.../public-providers` (unauth, `{id,name}` of enabled IdPs); `GET
+.../login/{id}` → IdP and `GET .../callback` (unauth login flow).
+
+**Out of scope (follow-up)**: SAML 2.0 (separate issue — native xmlsec dep +
+signature-wrap review); SCIM auto-provisioning; per-provider role mapping from
+IdP groups.
+
+---
+
## Out of Scope
- Multi-tenant deployment (single org only)
diff --git a/src/backend/enterprise b/src/backend/enterprise
index 1e916f60..87c8f975 160000
--- a/src/backend/enterprise
+++ b/src/backend/enterprise
@@ -1 +1 @@
-Subproject commit 1e916f60483ad2a11121ce997a0b9efa609477c1
+Subproject commit 87c8f975d3ba5db87b40c63536fd0ac29cef1cdf
diff --git a/src/frontend/src/components/settings/SsoPanel.vue b/src/frontend/src/components/settings/SsoPanel.vue
new file mode 100644
index 00000000..34441266
--- /dev/null
+++ b/src/frontend/src/components/settings/SsoPanel.vue
@@ -0,0 +1,165 @@
+
+
+ Let users sign in through your identity provider (Okta, Entra ID, Google Workspace).
+ SAML support is coming separately.
+ {{ error }}
+ {{ p.name }}
+ (disabled)
+ {{ p.issuer }} {{ testResult }} Admin break-glass password login always remains available.Single Sign-On (OIDC)
+ Identity providers
+
+ Add provider
+
+ Policy
+
+
+
+
Or sign in with
+ + 🔑 {{ p.name }} + +