Skip to content

Commit 27ade42

Browse files
committed
chore(sso): trim verbose comments
1 parent c6b9d66 commit 27ade42

7 files changed

Lines changed: 89 additions & 145 deletions

File tree

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@ function selectTokenEndpointAuthMethod(
4141
}
4242

4343
/**
44-
* Proposes a free, tenant-scoped provider ID by suffixing the domain's first
45-
* label (`azure-ad` + `acme.com` -> `azure-ad-acme`), so a caller who hit the
46-
* global-uniqueness collision is handed something concrete to type rather than
47-
* being asked to invent a name. Callers pass a domain that already went through
48-
* `normalizeSSODomain`, whose `^[a-z0-9-]+(\.[a-z0-9-]+)+$` shape guarantees a
49-
* non-empty first label needing no further sanitizing.
44+
* Proposes a free provider ID by suffixing the domain's first label
45+
* (`azure-ad` + `acme.com` -> `azure-ad-acme`). Callers pass a domain already
46+
* through `normalizeSSODomain`, whose shape guarantees a non-empty first label.
5047
*/
5148
function suggestProviderId(providerId: string, domain: string): string {
5249
return `${providerId}-${domain.split('.')[0]}`
@@ -135,12 +132,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
135132
)
136133
}
137134

138-
// Security gate: configuring org SSO for a domain requires the org to have
139-
// proven ownership of it (DNS TXT verification). Without this, the old
140-
// first-come claim let any org wire another company's domain to their own
141-
// IdP — an account-takeover primitive. Existing domains were grandfathered
142-
// as verified by migration 0266, so live tenants are unaffected. Personal
143-
// (org-less) SSO is not gated.
135+
/**
136+
* Configuring org SSO for a domain requires DNS-proven ownership; without it
137+
* a first-come claim lets any org wire another company's domain to their own
138+
* IdP. Migration 0266 grandfathered existing domains. Org-less SSO is not gated.
139+
*/
144140
const isOrgDomainVerified = async (): Promise<boolean> => {
145141
if (!orgId) return true
146142
const [verified] = await db
@@ -166,9 +162,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
166162
{ status: 403 }
167163
)
168164

169-
// Fail fast before the expensive OIDC discovery. Re-checked immediately
170-
// before the provider write below to close the TOCTOU window (the verified
171-
// row could be removed while discovery is in flight).
165+
// Fail fast before OIDC discovery; re-checked before the write to close the
166+
// window where the proof is removed while discovery is in flight.
172167
if (!(await isOrgDomainVerified())) return domainNotVerifiedResponse()
173168

174169
const isOwnedByCaller = (provider: {
@@ -200,12 +195,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
200195
)
201196

202197
/**
203-
* Better Auth treats `providerId` as globally unique, not per-tenant:
204-
* `registerSSOProvider` rejects any id already present regardless of owner,
205-
* and `checkProviderAccess` resolves providers by that column alone. Catch
206-
* the cross-tenant collision here so the caller gets an actionable 409 that
207-
* names a free id, instead of Better Auth's opaque 422 ("SSO provider with
208-
* this providerId already exists") that gives no hint anything can be done.
198+
* Better Auth treats `providerId` as globally unique, not per-tenant, and
199+
* resolves providers by that column alone. Catching the cross-tenant
200+
* collision here turns its opaque 422 into a 409 naming a free id.
209201
*/
210202
const findProviderIdConflict = async () =>
211203
(
@@ -529,16 +521,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
529521
if (digestAlgorithm) samlConfig.digestAlgorithm = digestAlgorithm
530522

531523
/**
532-
* These two are always written, empty when unset, rather than omitted.
533-
* Better Auth merges SAML config with `??`, so an omitted key silently keeps
534-
* whatever was stored — clearing either field would never take effect. Both
535-
* are falsy-guarded downstream: `createIdP` falls back to
536-
* issuer/entryPoint/cert without metadata, and `createSP` omits nameIDFormat.
524+
* Always written, empty when unset: Better Auth merges SAML config with
525+
* `??`, so an omitted key keeps whatever was stored and clearing either
526+
* field would never take effect. Both are falsy-guarded downstream.
537527
*
538-
* Metadata in particular must not be generated here. Storing a document built
539-
* from cert + entryPoint made re-saving destructive, because the form loads it
540-
* back, resends it, and it then outranks the certificate — so rotating a SAML
541-
* cert appeared to succeed and changed nothing.
528+
* Metadata must not be generated here — a document built from cert +
529+
* entryPoint outranks the certificate on re-save, silently defeating
530+
* SAML cert rotation.
542531
*/
543532
samlConfig.idpMetadata = { metadata: idpMetadata ?? '' }
544533
samlConfig.identifierFormat = identifierFormat ?? ''
@@ -632,12 +621,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
632621
.limit(1)
633622

634623
/**
635-
* Unconditional write of Better Auth's `domainVerified` flag — the value Sim
636-
* mirrors from its own DNS proof, and what lets an SSO sign-in auto-link to an
637-
* existing same-email account. Used to withdraw trust, and to set the org-less
638-
* (personal) verdict, which {@link grantProviderDomainTrust} decides from the
639-
* deployment rather than from a domain. Granting on an org-scoped provider goes
640-
* through that same helper, which re-tests ownership in the write itself.
624+
* Unconditional write of Better Auth's `domainVerified` flag, which Sim
625+
* mirrors from its own DNS proof. Used to withdraw trust; granting on an
626+
* org-scoped provider goes through {@link grantProviderDomainTrust}, which
627+
* re-tests ownership in the write itself.
641628
*/
642629
const setProviderDomainVerified = async (verified: boolean) => {
643630
await db.update(ssoProvider).set({ domainVerified: verified }).where(ownerClause)
@@ -646,19 +633,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
646633
/**
647634
* Grants domain trust only while the proof is held under a row lock.
648635
*
649-
* Folding the ownership test into the UPDATE's WHERE clause is not sufficient:
650-
* under READ COMMITTED the EXISTS subquery is evaluated against the statement's
651-
* original snapshot, so a delete committing while the UPDATE waits on the
652-
* provider row can still leave the subquery seeing the removed sso_domain row —
653-
* granting trust after ownership is gone. Taking `FOR SHARE` on that row inside
654-
* a transaction makes the two operations order properly: the delete's removal of
655-
* sso_domain blocks until this commits, and if it committed first the SELECT
656-
* finds nothing and no trust is written.
636+
* A WHERE-clause EXISTS test is not enough: under READ COMMITTED the subquery
637+
* sees the statement's original snapshot, so a delete committing while the
638+
* UPDATE waits can still grant trust after ownership is gone. `FOR SHARE`
639+
* orders the two — the delete blocks until this commits, and if it committed
640+
* first the SELECT finds nothing.
657641
*
658-
* Org-less (personal) SSO is a self-host-only path — Sim's UI always registers
659-
* org-scoped. It has no verified domain behind it, so it is trusted only when
660-
* self-hosted, where the operator is the sole tenant. On the hosted deployment
661-
* that trust would let anyone claim a domain they do not own.
642+
* Org-less SSO is self-host-only (Sim's UI always registers org-scoped) and
643+
* has no proof behind it, so it is trusted only when self-hosted.
662644
*/
663645
const grantProviderDomainTrust = async (): Promise<boolean> => {
664646
if (!orgId) {
@@ -701,9 +683,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
701683
headers,
702684
})
703685

704-
// No newly-created row to roll back here, so clear the flag instead:
705-
// `updateSSOProvider` only resets it when the domain changes, so a
706-
// same-domain edit would otherwise leave stale trust standing.
686+
// Nothing to roll back on update, so clear the flag: `updateSSOProvider`
687+
// resets it only when the domain changes, leaving same-domain edits stale.
707688
if (!(await grantProviderDomainTrust())) {
708689
await setProviderDomainVerified(false)
709690
logger.warn('Revoked SSO domain trust: verification was removed mid-update', {
@@ -729,12 +710,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
729710
headers,
730711
})
731712

732-
// A refused grant means the verified sso_domain row was removed between the
733-
// pre-write check and Better Auth persisting the provider, leaving a provider
734-
// on a domain the org no longer proves — roll it back. registerSSOProvider is
735-
// create-only, so a successful call always created a brand-new row; we delete
736-
// by its primary-key `id`, not the logical providerId, which a concurrent
737-
// delete+recreate could point at a different row.
713+
// A refused grant means the proof vanished mid-write, leaving a provider on a
714+
// domain the org no longer proves — roll it back. Deleted by primary key, not
715+
// providerId, which a concurrent delete+recreate could point at another row.
738716
if (!(await grantProviderDomainTrust())) {
739717
// registerSSOProvider spreads the created row's `id` at runtime, but the
740718
// typed return omits it — read it defensively and only delete when it's a

apps/sim/app/api/organizations/[id]/domains/[domainId]/route.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,8 @@ export const DELETE = withRouteHandler(
6161
)
6262
}
6363

64-
// Removing the proof must also withdraw the trust it granted. `domainVerified`
65-
// on a provider is what authorizes auto-linking an SSO sign-in to an existing
66-
// same-email account, so leaving it set would keep that authorization alive
67-
// indefinitely after ownership was revoked. Both writes share a transaction so
68-
// a domain can never be gone while its provider still claims to be verified.
64+
// Removing the proof withdraws the trust it granted, in the same transaction
65+
// so a domain can never be gone while its provider still claims verification.
6966
const removed = await db.transaction(async (tx) => {
7067
const [deleted] = await tx
7168
.delete(ssoDomain)
@@ -74,10 +71,8 @@ export const DELETE = withRouteHandler(
7471

7572
if (!deleted) return null
7673

77-
// Match the provider domain the same way migration 0268 normalized it when it
78-
// grandfathered these rows: lower, trimmed, leading `*.` stripped. A provider
79-
// stored as `*.acme.com` against a verified row holding `acme.com` would
80-
// otherwise keep its trust after the proof was deleted.
74+
// Normalize as migration 0268 did when grandfathering these rows (lower,
75+
// trimmed, leading `*.` stripped), so `*.acme.com` matches proof `acme.com`.
8176
await tx
8277
.update(ssoProvider)
8378
.set({ domainVerified: false })

apps/sim/app/api/organizations/[id]/domains/[domainId]/verify/route.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,10 @@ export const POST = withRouteHandler(
116116
)
117117
.returning()
118118

119-
// Restore trust on any provider this proof covers, mirroring the revocation
120-
// performed when a verified domain is deleted. Without this, a delete followed
121-
// by a re-verification leaves the provider untrusted — and because that flag
122-
// gates sign-in, not just linking, the org would sit in a silent SSO outage
123-
// until someone re-saved the SSO config. Wildcard-tolerant, matching the
124-
// revoking comparison exactly so the two stay symmetric.
119+
// Restore trust this proof covers, mirroring the revocation on delete.
120+
// Without it a delete-then-reverify leaves the provider untrusted, and
121+
// since that flag gates sign-in the org sits in a silent SSO outage. The
122+
// comparison matches the revoking one exactly so the two stay symmetric.
125123
if (flipped.length > 0) {
126124
await tx
127125
.update(ssoProvider)

apps/sim/ee/sso/components/sso-settings.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,7 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
363363
let wantAssertionsSigned = true
364364
let idpMetadata = ''
365365
// Blank means "use the protocol default", so only carry over a stored value
366-
// that actually differs — otherwise editing would rewrite a default as an
367-
// explicit override, and a stored custom mapping must never silently reset.
366+
// that differs — otherwise editing rewrites a default as an explicit override.
368367
let mapping: { id?: string; email?: string; name?: string } = {}
369368
let identifierFormat = ''
370369
let authorizationEndpoint = ''
@@ -387,10 +386,9 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
387386
callbackUrl = config.callbackUrl || ''
388387
audience = config.audience || ''
389388
wantAssertionsSigned = config.wantAssertionsSigned ?? true
390-
// Two stored shapes: `{ metadata }` from the route, and a bare string from
391-
// older rows. Narrow on the type rather than truthiness — `{ metadata: '' }`
392-
// is falsy at `.metadata` but truthy as an object, which would put an object
393-
// into this string field and fail validation on the next save.
389+
// Two stored shapes: `{ metadata }` from the route, a bare string from older
390+
// rows. Narrow on type, not truthiness — `{ metadata: '' }` is falsy at
391+
// `.metadata` but truthy as an object, putting an object in a string field.
394392
idpMetadata =
395393
typeof config.idpMetadata === 'string'
396394
? config.idpMetadata
@@ -532,8 +530,7 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
532530
dirty: hasChanges,
533531
saving: configureSSOMutation.isPending,
534532
// Never disabled on validation errors: showErrors is only set by
535-
// handleSubmit, so a disabled Save left the admin with a greyed out
536-
// button and no message. Clicking now reveals what is wrong.
533+
// handleSubmit, so disabling Save left a greyed button and no message.
537534
saveLabel: isEditing ? 'Update' : 'Save',
538535
savingLabel: isEditing ? 'Updating...' : 'Saving...',
539536
onSave: () => void handleSubmit(),

apps/sim/lib/auth/auth.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,11 @@ const additionalTrustedOrigins = parseOriginList(env.TRUSTED_ORIGINS, (value) =>
109109

110110
/**
111111
* Extra provider IDs appended to `trustedProviders`, from `SSO_PROVIDER_ID` and
112-
* `SSO_TRUSTED_PROVIDER_IDS`. Empty when SSO is disabled, so `trustedProviders`
113-
* is unchanged for non-SSO deployments.
112+
* `SSO_TRUSTED_PROVIDER_IDS`. Empty when SSO is disabled.
114113
*
115-
* NOTE: these no longer affect SSO sign-in. Better Auth's SSO plugin calls the
116-
* account-linking handler with `trustProviderByName: false`, which disables the
117-
* `trustedProviders.includes(providerId)` branch entirely — SSO trust now comes
118-
* only from the provider's `domainVerified` flag (see the `sso()` config below).
119-
* They still apply to non-SSO providers that link by name, so the env vars are
120-
* kept rather than removed.
114+
* These no longer affect SSO sign-in: the plugin passes `trustProviderByName:
115+
* false`, disabling the name-based branch, so SSO trust comes only from
116+
* `domainVerified`. Kept because non-SSO providers still link by name.
121117
*/
122118
const additionalTrustedSsoProviders = isSsoEnabled
123119
? [env.SSO_PROVIDER_ID, ...(env.SSO_TRUSTED_PROVIDER_IDS?.split(',') ?? [])]
@@ -1092,35 +1088,27 @@ export const auth = betterAuth({
10921088
? [
10931089
sso({
10941090
/**
1095-
* Honor the IdP's verified-email claim, so an SSO sign-in from an IdP
1096-
* that asserts `email_verified` produces a verified local account
1097-
* rather than one forced to `emailVerified: false`.
1091+
* Honor the IdP's `email_verified` claim so the local account is
1092+
* verified rather than forced to false.
10981093
*
1099-
* This is NOT what enables account linking — many IdPs never send the
1100-
* claim at all (Microsoft Entra omits it, and Better Auth's own Entra
1101-
* provider defaults it to false), and the SAML path ignores it unless
1102-
* an explicit `mapping.emailVerified` is configured. `domainVerification`
1103-
* below is what actually establishes linking trust.
1094+
* This is not what enables linking — Entra omits the claim entirely,
1095+
* and SAML ignores it without an explicit `mapping.emailVerified`.
1096+
* `domainVerification` below establishes linking trust.
11041097
*/
11051098
trustEmailVerified: true,
11061099
/**
1107-
* Marks a provider as authoritative for its domain, which is what makes
1108-
* Better Auth auto-link an SSO sign-in to an existing same-email account.
1109-
* Without it `isTrustedProvider` is always false — the plugin passes
1110-
* `trustProviderByName: false`, so the `trustedProviders` allowlist never
1111-
* applies to SSO — and every user who already had a Sim account would be
1112-
* stranded on "account not linked".
1100+
* Marks a provider authoritative for its domain, which is what lets an
1101+
* SSO sign-in auto-link to an existing same-email account. Without it
1102+
* `isTrustedProvider` is always false and every user who already had a
1103+
* Sim account is stranded on "account not linked".
11131104
*
1114-
* Sim does not use Better Auth's own DNS challenge endpoints: ownership is
1115-
* proven by Sim's `sso_domain` flow before a provider can be registered,
1116-
* and the register route mirrors that decision onto this flag.
1105+
* Sim does not use Better Auth's DNS challenge endpoints: ownership is
1106+
* proven by the `sso_domain` flow before registration, and the register
1107+
* route mirrors that decision onto this flag.
11171108
*
1118-
* This path is constrained to emails whose domain matches the provider's
1119-
* (`validateEmailDomain`). It is NOT the only path: `link-account.mjs`
1120-
* blocks on `!isTrustedProvider && !userInfo.emailVerified`, so an IdP
1121-
* that asserts `email_verified` links regardless of domain — see the
1122-
* note on `trustEmailVerified` above. This flag narrows nothing on its
1123-
* own; it exists so linking survives IdPs that omit the claim.
1109+
* It narrows nothing on its own — an IdP asserting `email_verified`
1110+
* links regardless of domain (see `trustEmailVerified` above). It
1111+
* exists so linking survives IdPs that omit the claim.
11241112
*/
11251113
domainVerification: { enabled: true },
11261114
organizationProvisioning: {

0 commit comments

Comments
 (0)