Skip to content

feat(sso): implement organization-level OIDC configuration#2736

Open
viktormarinho wants to merge 2 commits intomainfrom
viktormarinho/org-level-sso
Open

feat(sso): implement organization-level OIDC configuration#2736
viktormarinho wants to merge 2 commits intomainfrom
viktormarinho/org-level-sso

Conversation

@viktormarinho
Copy link
Contributor

@viktormarinho viktormarinho commented Mar 17, 2026

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

  1. Run database migrations: bun run --cwd=apps/mesh migrate
  2. Start dev environment: bun run dev
  3. Open org settings and navigate to SSO section
  4. Configure OIDC provider (issuer URL, client ID, secret, email domain)
  5. Enable SSO enforcement for the org
  6. Switch away and back to the org to see SSO required screen
  7. Click "Sign in with SSO" to complete OIDC flow
  8. Verify successful login and org access

Migration Notes

This PR includes a new database migration 042-org-sso.ts that creates two tables:

  • org_sso_config - stores OIDC provider configuration per organization
  • org_sso_sessions - tracks per-user SSO authentication per organization with 24-hour expiry

Run bun run --cwd=apps/mesh migrate to apply.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Database migration included
  • No breaking changes
  • Frontend and backend integration complete

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 /status and /authorize by using the current org and membership checks; also fixes hook ordering in the app shell.

  • New Features

    • OIDC auth code flow with PKCE, ID token verification via jose JWKS, and email-to-user match.
    • /api/org-sso routes: status and authorize scoped to the current org with membership checks, plus callback, config (CRUD), and config/enforce.
    • Org Settings page for issuer, client ID/secret, domain, scopes, optional discovery endpoint, an enforcement toggle, “Test SSO”, and remove config; adds SSO to the Settings sidebar and React Query keys.
    • SSO-required screen in the shell with “Sign in with SSO”; server middleware returns 403 on org-scoped requests without a valid org SSO session; preserves the enforced flag on config updates.
  • Migration

    • Run: bun run --cwd=apps/mesh migrate.
    • Adds org_sso_config (per-org provider settings) and org_sso_sessions (per-user org sessions, 24h).

Written for commit 8ab680f. Summary will update on new commits.

@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.189.2-alpha.1
🎉 Patch 2.189.2
❤️ Minor 2.190.0
🚀 Major 3.0.0

Current version: 2.189.1

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ||
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

if (!confirm("Are you sure you want to remove SSO configuration?")) return;
try {
await deleteMutation.mutateAsync();
toast.success("SSO configuration removed");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

viktormarinho and others added 2 commits March 23, 2026 11:22
…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>
@viktormarinho viktormarinho force-pushed the viktormarinho/org-level-sso branch from 5b27d54 to 8ab680f Compare March 23, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant