feat(sso): implement organization-level OIDC configuration#2736
feat(sso): implement organization-level OIDC configuration#2736viktormarinho wants to merge 2 commits intomainfrom
Conversation
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsShould a new version be published when this PR is merged? React with an emoji to vote on the release type:
Current version: Deployment
|
There was a problem hiding this comment.
6 issues found across 21 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/storage/org-sso-config.ts">
<violation number="1" location="apps/mesh/src/storage/org-sso-config.ts:63">
P1: Upsert silently resets `enforced` to `false` when the field is omitted. Since `enforced` is optional, `undefined ? 1 : 0` evaluates to `0`, disabling SSO enforcement whenever config fields are updated without explicitly re-passing `enforced: true`. The onConflict update path should preserve the existing value when `enforced` is not provided.</violation>
</file>
<file name="apps/mesh/src/web/layouts/shell-layout.tsx">
<violation number="1" location="apps/mesh/src/web/layouts/shell-layout.tsx:249">
P0: **Rules of Hooks violation**: `useOrgSsoStatus` is called after two early returns, so it won't execute on every render. React requires hooks to be called unconditionally and in the same order every time. This will throw a runtime error when the component re-renders after `projectContext` changes between null/non-null.
Since the hook already supports `enabled: !!orgId`, move it above the early returns and derive `orgId` inline:</violation>
</file>
<file name="apps/mesh/src/api/app.ts">
<violation number="1" location="apps/mesh/src/api/app.ts:455">
P1: Mounting `/api/org-sso` before the MeshContext middleware leaves every SSO handler without `meshContext`, causing 500s on the first `ctx.auth` access.</violation>
</file>
<file name="apps/mesh/src/web/components/settings-modal/pages/org-sso.tsx">
<violation number="1" location="apps/mesh/src/web/components/settings-modal/pages/org-sso.tsx:85">
P2: After deleting SSO config, the form re-appears with stale values instead of empty fields. `handleDelete` calls `setIsEditing(false)` but never resets `formState`, so the new-config form renders pre-filled with the just-deleted configuration.</violation>
</file>
<file name="apps/mesh/src/api/routes/org-sso.ts">
<violation number="1" location="apps/mesh/src/api/routes/org-sso.ts:237">
P1: The SSO callback verifies the ID token email matches the logged-in user but never checks that the email belongs to `ssoConfig.domain`. A user with any email domain can complete SSO as long as they can authenticate at the IdP, making the `domain` configuration field unenforced. Add a domain check after the email comparison, e.g. `if (!tokenEmail.endsWith('@' + ssoConfig.domain.toLowerCase()))`.</violation>
<violation number="2" location="apps/mesh/src/api/routes/org-sso.ts:460">
P1: Server-Side Request Forgery (SSRF): `discoverOIDC` fetches from user-controlled `issuer`/`discoveryEndpoint` URLs without any URL validation or allowlisting. In cloud environments, an org owner could set `discoveryEndpoint` to an internal metadata endpoint (e.g., `http://169.254.169.254/...`). Consider validating that the URL scheme is HTTPS and the host resolves to a public IP address before making the request.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| } | ||
|
|
||
| const url = | ||
| discoveryEndpoint || |
There was a problem hiding this comment.
P1: Server-Side Request Forgery (SSRF): discoverOIDC fetches from user-controlled issuer/discoveryEndpoint URLs without any URL validation or allowlisting. In cloud environments, an org owner could set discoveryEndpoint to an internal metadata endpoint (e.g., http://169.254.169.254/...). Consider validating that the URL scheme is HTTPS and the host resolves to a public IP address before making the request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/org-sso.ts, line 460:
<comment>Server-Side Request Forgery (SSRF): `discoverOIDC` fetches from user-controlled `issuer`/`discoveryEndpoint` URLs without any URL validation or allowlisting. In cloud environments, an org owner could set `discoveryEndpoint` to an internal metadata endpoint (e.g., `http://169.254.169.254/...`). Consider validating that the URL scheme is HTTPS and the host resolves to a public IP address before making the request.</comment>
<file context>
@@ -0,0 +1,514 @@
+ }
+
+ const url =
+ discoveryEndpoint ||
+ `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
+
</file context>
| if (!confirm("Are you sure you want to remove SSO configuration?")) return; | ||
| try { | ||
| await deleteMutation.mutateAsync(); | ||
| toast.success("SSO configuration removed"); |
There was a problem hiding this comment.
P2: After deleting SSO config, the form re-appears with stale values instead of empty fields. handleDelete calls setIsEditing(false) but never resets formState, so the new-config form renders pre-filled with the just-deleted configuration.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/settings-modal/pages/org-sso.tsx, line 85:
<comment>After deleting SSO config, the form re-appears with stale values instead of empty fields. `handleDelete` calls `setIsEditing(false)` but never resets `formState`, so the new-config form renders pre-filled with the just-deleted configuration.</comment>
<file context>
@@ -0,0 +1,337 @@
+ if (!confirm("Are you sure you want to remove SSO configuration?")) return;
+ try {
+ await deleteMutation.mutateAsync();
+ toast.success("SSO configuration removed");
+ setIsEditing(false);
+ } catch {
</file context>
…ement - Add database migration and storage layer for org SSO config and sessions - Implement OIDC authorization code flow with PKCE in org-sso routes - Add API endpoints for OIDC flow, config management, and status checks - Create frontend SSO settings page for org admins with config form - Add React Query hooks for SSO state management - Integrate SSO enforcement in shell-layout to block access without valid session - Add SsoRequiredScreen component to guide users through SSO login - Update storage types and context factory to include new storage classes - Update test mocks with new storage fields Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move useOrgSsoStatus above conditional early returns in ShellLayoutContent to comply with React Rules of Hooks. Replace query-string orgId with ctx.organization?.id in /status and /authorize endpoints to prevent unauthorized users from probing SSO config of arbitrary organizations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5b27d54 to
8ab680f
Compare
What is this contribution about?
This PR implements organization-level SSO (OIDC) configuration, allowing org admins to dynamically configure and enforce OIDC-based authentication per organization through the UI. Users must authenticate via SSO to access orgs where SSO is enforced, while non-enforced orgs remain accessible via standard authentication methods. The implementation uses custom OIDC flow with PKCE for security, storing provider configs and session state in the database.
How to Test
bun run --cwd=apps/mesh migratebun run devMigration Notes
This PR includes a new database migration
042-org-sso.tsthat creates two tables:org_sso_config- stores OIDC provider configuration per organizationorg_sso_sessions- tracks per-user SSO authentication per organization with 24-hour expiryRun
bun run --cwd=apps/mesh migrateto apply.Review Checklist
Summary by cubic
Adds organization-level OIDC SSO with per-org enforcement and a settings UI. Includes server-side middleware that blocks org-scoped API requests without a valid org SSO session, and secures
/statusand/authorizeby using the current org and membership checks; also fixes hook ordering in the app shell.New Features
joseJWKS, and email-to-user match./api/org-ssoroutes:statusandauthorizescoped to the current org with membership checks, pluscallback,config(CRUD), andconfig/enforce.enforcedflag on config updates.Migration
bun run --cwd=apps/mesh migrate.org_sso_config(per-org provider settings) andorg_sso_sessions(per-user org sessions, 24h).Written for commit 8ab680f. Summary will update on new commits.