feat(sso): DNS domain verification gating org SSO registration#5909
feat(sso): DNS domain verification gating org SSO registration#5909waleedlatif1 wants to merge 18 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Data & APIs: New SSO register route: Org registrations now require a matching verified Product surface: Verified domains settings (nav + Reviewed by Cursor Bugbot for commit 085a962. Configure here. |
Greptile SummaryAdds DNS-based organization domain verification as a prerequisite for configuring organization SSO.
Confidence Score: 5/5The pull request appears safe to merge. No blocking failures remain within the scope of the previous review threads. Important Files Changed
Sequence DiagramsequenceDiagram
participant Admin
participant DomainAPI as Verified Domains API
participant DNS
participant DB
participant SSO as SSO Registration API
Admin->>DomainAPI: Add organization domain
DomainAPI->>DB: Insert pending domain and challenge
DomainAPI-->>Admin: Return TXT record
Admin->>DNS: Publish TXT challenge
Admin->>DomainAPI: Verify domain
DomainAPI->>DNS: Resolve TXT record
DNS-->>DomainAPI: Matching challenge
DomainAPI->>DB: Mark domain verified
Admin->>SSO: Configure organization SSO
SSO->>DB: Confirm verified domain ownership
DB-->>SSO: Verified
SSO->>DB: Create or update SSO provider
SSO-->>Admin: Configuration saved
Reviews (19): Last reviewed commit: "refactor(sso): share normalizeSSODomain ..." | Re-trigger Greptile |
b1f22a3 to
c5de3e7
Compare
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit dbae0ea. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b41fefb. Configure here.
|
@cursor review |
b41fefb to
6b9261c
Compare
|
Update after a comprehensive independent audit (5 tracks: Better Auth API alignment, regression/UX, adversarial correctness, cleanup, TOCTOU feasibility). Pushed 6b9261c:
All gates green (tsc 0, api-validation, migrations backward-compatible, db lint, 74 affected tests). @greptile @cursor please re-review. |
|
@cursor review |
…igration edge
Cosmetic cleanup from a final 4-track adversarial review (no bugs found in the
new logic):
- Self-host script: narrow the pre-transaction existence read to select({ id })
instead of SELECT * (it only feeds a log line now).
- Unify invalid-domain copy ("for example acme.com") and the verified-elsewhere
409 wording ("is already verified by another organization") across routes.
- p-3 shorthand on the domain row card.
- Document the migration's rare two-orgs-share-a-domain grandfather behavior
(login unaffected; validated no such duplicates in prod).
Two pre-existing SSO bugs the final review surfaced (prod has one SSO org, RVW, script-registered with the default mapping, so neither change affects it): - Attribute mapping was passed at the top level of the register payload, which Better Auth ignores — it reads oidcConfig.mapping / samlConfig.mapping. Nest it so custom mappings actually apply. (Default mapping is unchanged, so existing logins are unaffected.) - Editing an SSO provider was broken: registerSSOProvider is create-only and threw on the existing providerId → generic 500. Route now detects a provider the caller already owns and updates it via Better Auth's updateSSOProvider, and surfaces Better Auth's own error status/message instead of a blanket 500. Also drops the now-unnecessary providerExistedBefore guard (the rollback deletes by the created row's primary-key id and register is create-only) and the earlier final-review polish (script read, unified copy, migration edge note). Smoke-test SSO login + edit on staging before merge (auth-path change).
…pass) The personal branch of both provider-ownership lookups keyed on (providerId, userId) without requiring organizationId IS NULL. Because org providers store userId = their creator and providerId is globally unique, an org admin could send a personal-mode request (no orgId) — which skips the membership check and the domain-verification gate — yet still match, and then via the new update path move, their org's provider to an unverified domain. Add isNull(organizationId) to the personal branch of both clauses so it can only match a genuinely personal provider, matching the route's own isOwnedByCaller. Found by an adversarial review of the update path added in 394bda9.
Inside the registration transaction the script updated WHERE providerId — the logical key. If the observed provider was deregistered and a replacement created with the same providerId before the transaction ran, that update would clobber the replacement's config and ownership. Update the specific observed row by its primary-key id instead; if it's gone we insert, which fails cleanly on the providerId unique constraint rather than overwriting the replacement.
56ef036 to
464e7e0
Compare
|
@cursor review |
…onstraint) sso_provider.provider_id is a plain (non-unique) index and prod holds legitimate duplicates, so the previous "update by id, else insert" could create a duplicate provider when the observed row was deregistered and replaced before the transaction — the fallback insert would succeed. Delete every row for the providerId then insert exactly one, inside the transaction, so the providerId ends up as exactly this config atomically. Linked accounts key on the providerId string (not the row id), so existing logins are unaffected.
|
@cursor review |
…rification # Conflicts: # packages/db/migrations/meta/0266_snapshot.json # packages/db/migrations/meta/_journal.json # scripts/check-api-validation-contracts.ts
Staging landed migrations 0266/0267, colliding with our 0266. Removed our migration, merged staging, and regenerated cleanly with drizzle-kit as 0268_sso_domain_verification (identical sso_domain table + indexes), then re-appended the grandfather backfill. api-validation baseline reconciled to 973 (staging 970 + our 3 domain routes). Also make the register-route test's registerSSOProvider mock return an id so the guarded compensating delete runs.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit dce6d63. Configure here.
…hes gate The self-host script canonicalized SSO domains with a minimal inline transform (lower+trim+wildcard) that diverged from the app's full normalizeSSODomain (protocol, port, path, trailing dot, email local part) — equivalent spellings could store a different ownership key than the runtime gate looks up. Move normalizeSSODomain into @sim/utils/sso-domain (a pure function) so the register route, the domain-claim route, and the script all use the identical canonicalizer. The script now skips the verified-domain record when SSO_DOMAIN isn't a valid registrable domain instead of storing a malformed key.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 085a962. Configure here.
Summary
Existing-customer impact
Design note
Type of Change
Testing
Checklist