Skip to content

feat(sso): self-service SSO config with DNS-verified domains#13507

Merged
marksalpeter merged 21 commits intomainfrom
mark/LFE-9126-allow-sso-self-service-setup
May 7, 2026
Merged

feat(sso): self-service SSO config with DNS-verified domains#13507
marksalpeter merged 21 commits intomainfrom
mark/LFE-9126-allow-sso-self-service-setup

Conversation

@marksalpeter
Copy link
Copy Markdown
Contributor

@marksalpeter marksalpeter commented May 7, 2026

Summary

Org admins on Langfuse Cloud have no self-service path to configure SSO — the only path today is a Langfuse support engineer hitting /api/auth/add-sso-config with the ADMIN_API_KEY, and the Org Settings → SSO tab is a placeholder that opens the support drawer.

To achieve this we:

  • Added a VerifiedDomain model + tRPC router so orgs DNS-verify domain ownership (TXT record) before any SSO config can be created for that domain — closes the cross-tenant hijacking footgun.
  • Added an ssoConfig tRPC router (get / save / delete) that gates writes on a verified domain for the caller's org, encrypts clientSecret at rest, and emits before/after audit-log entries with the secret masked.
  • Pre-flight every save against the IdP's OIDC discovery doc — fetches <issuer>/.well-known/openid-configuration (or the equivalent for Azure AD / Google), validates the required fields and that the returned issuer matches what was configured. Catches mistyped issuer URLs and unreachable IdPs at save time instead of locking out users at first sign-in.
  • Replaced the placeholder SSO settings page with a per-domain list view + scoped setup form (provider picker, live callback URL panel, all 13 providers) and matching delete confirmation.
  • Tightened OIDC issuer validation to require https:// across every provider (also covers GitHub Enterprise's baseUrl) — catches a real footgun where Auth0/etc. issuers without a scheme broke at sign-in time instead of save time.
  • Query DNS for verification through Cloudflare / Google public resolvers so newly-added TXT records are visible within seconds instead of the system resolver's ~1-hour NXDOMAIN cache.

Note: GitHub and GitHub Enterprise are OAuth2-only with no .well-known endpoint, so the discovery pre-flight is skipped for those — misconfiguration there still surfaces at first sign-in. Cross-pod SSO config cache propagation lag (the "active within 1 hour" copy in the confirmation dialog) is tracked in LFE-9662.

Fixes LFE-9126

Verification

  • pnpm --filter web run typecheck
  • pnpm --filter web run lint
  • pnpm --filter web run test -- verifiedDomainRouter.servertest (15/15)
  • pnpm --filter web run test -- ssoConfigRouter.servertest (22/22, including 8 new discovery tests)
  • Browser pass — added domain, DNS-verified via Namecheap, configured Auth0 SSO, signed in via the new provider, updated credentials, deleted

Test plan

  • Verified Domains → Add domain → instructions show zone-relative host (_langfuse-verification) and copyable token → admin adds DNS record → Verify → row marks Verified.
  • Cross-tenant: a second org cannot add the same domain → CONFLICT error inline.
  • SSO tab → Configure SSO on a verified row → confirmation dialog warns about lockout → Save → row updates with provider summary.
  • Update flow pre-fills non-secret fields → clientSecret always required → Save replaces atomically.
  • Delete flow → confirmation → row reverts to "Not configured"; the other domain's config is untouched.
  • Form rejects an issuer like acme.us.auth0.com (missing https://) at validation time, not at sign-in.
  • Save with a mistyped issuer (e.g. wrong tenant on Azure AD, typo in Okta domain) is rejected with a clear discovery error at save time, not at sign-in.
  • GitHub provider: save succeeds without a discovery call (OAuth-only, skipped).
  • Member-role user (no organization:update) gets FORBIDDEN on every mutation.

Disclaimer: Experimental PR review

Greptile Summary

This PR introduces self-service SSO configuration for org admins on Langfuse Cloud, gated behind DNS-verified domain ownership. The implementation adds a VerifiedDomain model/router, a new ssoConfigRouter (get/save/delete) with clientSecret encryption and OIDC discovery pre-flight, and replaces the placeholder SSO settings page with a full domain-list + config form UI across 13 providers.

  • New VerifiedDomain table and tRPC router let org admins prove domain ownership via a DNS TXT record before any SSO config can be written; the Prisma migration adds appropriate unique and FK indexes.
  • ssoConfigRouter.save correctly requires a verified-domain claim, pre-flights the IdP's OIDC discovery document, encrypts clientSecret at rest, and emits masked audit logs; the delete handler's missing verifiedAt guard and the orphaned-SsoConfig problem on VerifiedDomain deletion were flagged in a prior review cycle.
  • The custom OIDC provider form path is silently broken: CustomProviderSchema requires authConfig.name but buildSsoPayload never includes it, so the Zod parse in handleConfirm fails and form.setError maps the error to a non-existent field — the user receives no feedback and the mutation is never called.

Confidence Score: 4/5

Safe to merge once the Custom OIDC form path is fixed — without it, custom provider saves silently fail in the UI with no user feedback.

The custom OIDC provider form never collects or sends the authConfig.name field that CustomProviderSchema requires; the resulting Zod validation failure in handleConfirm sets an error on a non-existent form field, silently swallowing the failure. This makes custom provider configuration completely non-functional through the UI. All other providers work correctly, the server-side security model is sound, encryption and audit logging are well-implemented, and the DNS-verification flow is solid.

web/src/ee/features/sso-settings/components/SSOSettings.tsx — the buildSsoPayload function and surrounding form code need a name field added for the custom provider case.

Security Review

  • SSRF via redirect following (validateSsoConfig.ts): fetch is called with redirect: \"follow\" against a user-supplied OIDC issuer URL. A redirect chain to an internal address (e.g. instance-metadata endpoints) will be silently followed; the issuer-equality check prevents body exfiltration but internal reachability is still leaked via error message differences. Switching to redirect: \"error\" closes this with no functional downside.

Important Files Changed

Filename Overview
web/src/ee/features/sso-settings/components/SSOSettings.tsx New SSO settings UI; buildSsoPayload omits required name field for the custom provider, causing silent save failures; allowDangerousEmailAccountLinking is hardcoded to true (flagged previously).
web/src/ee/features/multi-tenant-sso/server/ssoConfigRouter.ts New tRPC router for SSO config CRUD; save correctly gates on verifiedAt and encrypts clientSecret; delete only requires any VerifiedDomain claim (not verified) — already flagged in prior review; maskAuthConfig consistently strips secret before returning or logging.
web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts New tRPC router for domain-ownership verification via DNS TXT record; TOCTOU race in create and no entitlement guard on mutations flagged; delete leaves associated SsoConfig as an orphan (already flagged).
web/src/ee/features/multi-tenant-sso/validateSsoConfig.ts Server-side OIDC discovery preflight; correctly validates issuer match; redirect: "follow" allows the server to follow user-supplied redirects to internal endpoints — recommend redirect: "error".
web/src/ee/features/verified-domains/components/VerifiedDomainsSettings.tsx New Verified Domains UI component; RBAC and entitlement checks on client side; clean expandable row pattern for TXT record instructions; no logic issues found.
packages/shared/prisma/migrations/20260506144028_add_verified_domains/migration.sql Adds verified_domains table with appropriate unique index on domain, organization_id FK with CASCADE delete, and index on organization_id; no issues.
web/src/ee/features/multi-tenant-sso/types.ts Adds oidcIssuer Zod validator enforcing https:// prefix across all provider schemas; all 13 providers correctly defined; no issues.
web/src/ee/features/verified-domains/server/dnsLookup.ts Thin wrapper around Node.js dns.Resolver using Cloudflare/Google public resolvers; timeout and retry configured; no issues.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant UI
    participant verifiedDomainRouter
    participant ssoConfigRouter
    participant validateSsoConfig
    participant DNS
    participant IdP

    Admin->>UI: Add domain
    UI->>verifiedDomainRouter: "create({ orgId, domain })"
    verifiedDomainRouter-->>UI: "{ recordHost, recordValue }"

    Admin->>DNS: Add TXT record
    Admin->>UI: Click Verify
    UI->>verifiedDomainRouter: "verify({ orgId, id })"
    verifiedDomainRouter->>DNS: resolveTxtFresh(_langfuse-verification.domain)
    DNS-->>verifiedDomainRouter: TXT records
    verifiedDomainRouter-->>UI: "{ verifiedAt }"

    Admin->>UI: Configure SSO (provider + credentials)
    UI->>ssoConfigRouter: "save({ orgId, payload })"
    ssoConfigRouter->>ssoConfigRouter: "check verifiedAt != null"
    ssoConfigRouter->>validateSsoConfig: validateSsoConfig(payload)
    validateSsoConfig->>IdP: GET issuer/.well-known/openid-configuration
    IdP-->>validateSsoConfig: discovery doc
    validateSsoConfig-->>ssoConfigRouter: ok / throws PRECONDITION_FAILED
    ssoConfigRouter->>ssoConfigRouter: encrypt(clientSecret)
    ssoConfigRouter->>ssoConfigRouter: upsert SsoConfig + auditLog
    ssoConfigRouter-->>UI: saved row (secret stripped)
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
web/src/ee/features/sso-settings/components/SSOSettings.tsx:644-691
**Custom OIDC provider is silently broken — `name` field missing from form and payload**

`CustomProviderSchema` requires `authConfig.name: z.string()` (non-optional), but `buildSsoPayload` never includes `name` in the payload for the `"custom"` case. When the user clicks "Activate SSO" with a Custom OIDC config, `SsoProviderSchema.safeParse` fails with `{ path: ["authConfig", "name"], message: "Required" }`. The error-display path then calls `form.setError("authConfig.name", ...)`, but `"authConfig.name"` is not a field in `formSchema`, so no error is shown. The confirmation dialog closes silently, `saveMutation.mutate` is never called, and the user has no idea why nothing happened. Add a `name` field to the form (and `formSchema`) for the `"custom"` provider case, and include it in `buildSsoPayload` for that branch.

### Issue 2 of 3
web/src/ee/features/multi-tenant-sso/validateSsoConfig.ts:67-72
Using `redirect: "follow"` on a server-side fetch to a user-supplied URL means the server will silently follow redirects to internal endpoints (e.g. a 302 from the OIDC discovery URL to `http://169.254.169.254/latest/meta-data/` or similar). While the issuer-equality check prevents data exfiltration from the response body, the server still establishes a connection to the redirect target, which leaks whether internal addresses are reachable. Setting `redirect: "error"` eliminates this vector with no functional cost — a legitimate IdP's discovery endpoint doesn't redirect.

```suggestion
  let resp: Response;
  try {
    resp = await fetch(discoveryUrl, {
      signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
      redirect: "error",
    });
```

### Issue 3 of 3
web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts:59-96
**`create` / `verify` / `delete` have no entitlement check**

Unlike `ssoConfigRouter`, none of the `verifiedDomainRouter` mutations call `throwIfNoEntitlement`. Any org — including `cloud:hobby` plans — can claim, DNS-verify, and delete domain ownership records. If entitlement enforcement is intentionally deferred to the `ssoConfig.save` call, this should be documented; otherwise a `throwIfNoEntitlement` call matching the one in `ssoConfigRouter` should be added here.

Reviews (2): Last reviewed commit: "feat(sso): pre-flight OIDC discovery on ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

marksalpeter and others added 5 commits May 7, 2026 13:16
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fication

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label May 7, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

@claude review

@dosubot dosubot Bot added the feat-auth label May 7, 2026
Comment thread web/src/ee/features/multi-tenant-sso/server/ssoConfigRouter.ts
Comment thread web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts
Comment thread web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts Outdated
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
@marksalpeter marksalpeter marked this pull request as draft May 7, 2026 13:59
@marksalpeter marksalpeter marked this pull request as ready for review May 7, 2026 14:03
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx Outdated
Comment thread web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
Comment thread web/src/ee/features/multi-tenant-sso/types.ts Outdated
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
… support endpoint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marksalpeter marksalpeter force-pushed the mark/LFE-9126-allow-sso-self-service-setup branch from f660efd to 8846d48 Compare May 7, 2026 14:13
Comment thread web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts
Comment thread web/src/ee/features/multi-tenant-sso/server/ssoConfigRouter.ts Outdated
Comment thread web/src/ee/features/multi-tenant-sso/validateSsoConfig.ts
marksalpeter and others added 3 commits May 7, 2026 16:36
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
marksalpeter and others added 7 commits May 7, 2026 16:55
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rise baseUrl

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…form

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… provider

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marksalpeter
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/shared/prisma/schema.prisma
Comment thread web/src/ee/features/verified-domains/server/verifiedDomainRouter.ts Outdated
Comment thread web/src/ee/features/multi-tenant-sso/validateSsoConfig.ts
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
…etes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
marksalpeter and others added 3 commits May 7, 2026 18:03
… discovery

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…exists

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marksalpeter
Copy link
Copy Markdown
Contributor Author

@claude review

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marksalpeter
Copy link
Copy Markdown
Contributor Author

@claude review

@marksalpeter marksalpeter added this pull request to the merge queue May 7, 2026
@dosubot dosubot Bot added the auto-merge This PR is set to be merged label May 7, 2026
Merged via the queue into main with commit 871a8d5 May 7, 2026
34 checks passed
@marksalpeter marksalpeter deleted the mark/LFE-9126-allow-sso-self-service-setup branch May 7, 2026 16:35
@dosubot dosubot Bot removed the auto-merge This PR is set to be merged label May 7, 2026
Comment thread web/src/ee/features/sso-settings/components/SSOSettings.tsx
Comment thread web/src/ee/features/multi-tenant-sso/server/ssoConfigRouter.ts
Comment on lines +53 to +77
// Microsoft's documented multi-tenant tenantId values. Discovery for these
// returns `issuer` with the literal `{tenantid}` placeholder string instead
// of the configured tenantId — the actual tenant is bound at token-issuance
// time per user's home tenant. NextAuth's AzureADProvider handles this at
// sign-in, so save-time we compare against the placeholder rather than the
// configured value to avoid blocking legitimate multi-tenant configurations.
const AZURE_AD_MULTI_TENANT = new Set(["common", "organizations", "consumers"]);
const AZURE_AD_TENANT_PLACEHOLDER = "{tenantid}";

function expectedReturnedIssuer(
payload: SsoProviderSchema,
trimmedIssuer: string,
): string {
if (
payload.authProvider === "azure-ad" &&
payload.authConfig &&
AZURE_AD_MULTI_TENANT.has(payload.authConfig.tenantId)
) {
return trimmedIssuer.replace(
`/${payload.authConfig.tenantId}/`,
`/${AZURE_AD_TENANT_PLACEHOLDER}/`,
);
}
return trimmedIssuer;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new AZURE_AD_MULTI_TENANT set at validateSsoConfig.ts:59 includes "consumers" alongside "common" and "organizations", but Microsoft only emits the {tenantid} placeholder in the discovery doc for the two genuinely multi-tenant endpoints — https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration returns the literal MSA tenant GUID 9188040d-6c67-4c5b-b112-36a304b66dad, because consumers is a single-tenant alias for the fixed personal-accounts tenant. So a save with tenantId: "consumers" always fails the issuer-equality check at line 148 with PRECONDITION_FAILED. Niche case, but easy to fix: drop consumers from the set and special-case it to compare against the literal MSA GUID (or map consumers9188040d-6c67-4c5b-b112-36a304b66dad in expectedReturnedIssuer).

Extended reasoning...

What the bug is

The newly-added AZURE_AD_MULTI_TENANT set at web/src/ee/features/multi-tenant-sso/validateSsoConfig.ts:59 lumps three tenantId values together:

const AZURE_AD_MULTI_TENANT = new Set(["common", "organizations", "consumers"]);

The accompanying comment claims discovery for all three "returns issuer with the literal {tenantid} placeholder string instead of the configured tenantId." That assumption is correct for common and organizations, but not for consumers. Microsoft documents consumers as a single-tenant alias for the Microsoft Personal Account (MSA) tenant — it is not multi-tenant; the tenant is fixed and known. Its discovery doc therefore reports the actual MSA GUID, not the placeholder.

Empirical verification

The discovery endpoints behave as follows (each verifier independently confirmed against the live Microsoft endpoints):

  • GET https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" (placeholder)
  • GET https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration"issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" (placeholder)
  • GET https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration"issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" (literal MSA GUID)

Step-by-step proof

  1. Org admin opens Configure SSO for a verified domain, picks Azure AD / Entra ID, enters tenantId: "consumers" (a documented Microsoft value, intended to scope the app to personal Microsoft accounts only).
  2. SsoProviderSchema.safeParse succeeds — consumers passes .min(1).
  3. validateSsoConfig runs. discoveryIssuerFor builds https://login.microsoftonline.com/consumers/v2.0.
  4. fetch(".../consumers/v2.0/.well-known/openid-configuration") returns 200 with issuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0".
  5. expectedReturnedIssuer (lines 62-77) sees consumers is in AZURE_AD_MULTI_TENANT and substitutes it with the placeholder, producing https://login.microsoftonline.com/{tenantid}/v2.0.
  6. The equality check at line 148 (returnedIssuer !== expectedIssuer) compares https://login.microsoftonline.com/9188040d-.../v2.0 against https://login.microsoftonline.com/{tenantid}/v2.0 — mismatch — and throws PRECONDITION_FAILED with reported issuer "..." but we expected "...".
  7. The save is blocked. Since the legacy createNewSsoConfigHandler now also calls validateSsoConfig (per this PR), even the support-engineer backdoor is blocked for this configuration.

Why existing safeguards do not prevent it

  • The schema only enforces .min(1) on tenantId, so the string "consumers" passes.
  • expectedReturnedIssuer actively maps consumers to the placeholder, which is the wrong direction — Microsoft sends the GUID, not the placeholder, for this endpoint.
  • The new ssoConfigRouter.servertest.ts tests the multi-tenant placeholder path with tenantId: "common" only; the consumers variant is not exercised.

Impact

Bounded but real. Configuring an org-level SSO restricted to MSA personal accounts is unusual (per-domain SSO doesnt naturally pair with personal-only sign-in), and the admin can work around by entering the literal GUID 9188040d-6c67-4c5b-b112-36a304b66dad. But the codes claim about Microsofts behavior is empirically wrong for consumers, so the comment and the set are out of sync with reality, and the legacy support path regresses in the same spot.

Fix

Drop consumers from AZURE_AD_MULTI_TENANT and either (a) map consumers to the literal GUID before the equality check, or (b) special-case consumers to compare against https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0. Option (a) keeps the placeholder-substitution path for the genuinely multi-tenant case and adds a single mapping for consumers:

const AZURE_AD_MULTI_TENANT = new Set(["common", "organizations"]);
const MSA_TENANT_GUID = "9188040d-6c67-4c5b-b112-36a304b66dad";

function expectedReturnedIssuer(payload, trimmedIssuer) {
  if (payload.authProvider !== "azure-ad" || !payload.authConfig) return trimmedIssuer;
  const t = payload.authConfig.tenantId;
  if (AZURE_AD_MULTI_TENANT.has(t)) {
    return trimmedIssuer.replace(`/${t}/`, `/${AZURE_AD_TENANT_PLACEHOLDER}/`);
  }
  if (t === "consumers") {
    return trimmedIssuer.replace(`/consumers/`, `/${MSA_TENANT_GUID}/`);
  }
  return trimmedIssuer;
}

A short test mirroring the existing tenantId: "common" case but with consumers and the literal MSA-GUID issuer in the mocked discovery doc would lock the fix in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm. This is so specific. I will have to deep dive potentially.

// (enforced via a partial unique index added in the migration, since
// Prisma cannot express partial uniqueness in the schema DSL).
@@unique([organizationId, domain])
@@index([organizationId])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be redundant as it's already covered by the unique index above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I might change this if I have time. Its a small table, so not a huge deal, but you're right about the double index. My mistake.


// Domain ownership claim for an organization, verified via DNS TXT record.
// Required before an SsoConfig can be created for that domain.
model VerifiedDomain {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be possible to have a "loose" FK relationship to the ssoConfigs? That way, if an organization is deleted, we could delete the associated domain enforcement as well.

Copy link
Copy Markdown
Contributor Author

@marksalpeter marksalpeter May 8, 2026

Choose a reason for hiding this comment

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

Cascading deletes for SsoConfig would be nice, but its unclear how to implement, so I'm sidestepping the issue for now.


// Pending claims are shareable across orgs; only verified claims are
// exclusive. Block create only when another org has already verified
// the domain — otherwise a hobby-plan org could squat a global slot
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should only be available on cloud:team and cloud:enterprise given the entitlement. IMO still a valid check and assumption!

Comment on lines +128 to +131
// Race protection: two parallel creates from the same org or two orgs
// racing each other after a verified row was just created. The
// (organizationId, domain) unique index and the partial index on
// verified rows both surface as Prisma P2002.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could also consider a prisma transcation here with Serializable isolation. IMO both should be valid to protect against a race condition here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat-auth size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants