-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(sso): re-grant provider trust when an already-verified domain is re-submitted #6320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
775b120
97161ec
f5a3de6
45ec9a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,8 +70,20 @@ export const POST = withRouteHandler( | |
| return NextResponse.json({ success: true, data: { domain: toDomainResponse(row) } }) | ||
| } | ||
|
|
||
| const recordPresent = await checkDomainTxtRecord(row.domain, row.verificationToken) | ||
| if (!recordPresent) { | ||
| const lookup = await checkDomainTxtRecord(row.domain, row.verificationToken) | ||
| // 503, not 422: we learned nothing about their record, so this must not read | ||
| // as a missing one. SERVFAIL can mean either a fault of ours or a broken zone | ||
| // of theirs, so the message states what we know rather than assigning blame. | ||
| if (lookup === 'unavailable') { | ||
| return NextResponse.json( | ||
| { | ||
| error: | ||
| "We couldn't complete the DNS lookup, so we can't tell yet whether your record is published. Try again in a few minutes — if it keeps failing, check that your domain's nameservers are responding.", | ||
| }, | ||
| { status: 503 } | ||
| ) | ||
| } | ||
| if (lookup === 'absent') { | ||
| return NextResponse.json( | ||
| { | ||
| error: | ||
|
|
@@ -101,6 +113,17 @@ export const POST = withRouteHandler( | |
| // instead of mapping an undefined row or trusting a superseded challenge. A | ||
| // concurrent cross-org verification trips the partial unique index; surface | ||
| // that as a 409 rather than an unhandled 500. | ||
| /** | ||
| * Providers this proof covers. Normalized the way migration 0268 stored these | ||
| * rows (lower, trimmed, leading `*.` dropped) and identical to the expression | ||
| * the deletion path revokes with, so granting and revoking can never diverge. | ||
| */ | ||
| const providersOnDomain = (verifiedDomain: string) => | ||
| and( | ||
| eq(ssoProvider.organizationId, organizationId), | ||
| sql`lower(regexp_replace(btrim(${ssoProvider.domain}), '^\\*\\.', '')) = ${verifiedDomain}` | ||
| ) | ||
|
|
||
| let updated: (typeof row)[] | ||
| try { | ||
| updated = await db.transaction(async (tx) => { | ||
|
|
@@ -118,18 +141,12 @@ export const POST = withRouteHandler( | |
|
|
||
| // Restore trust this proof covers, mirroring the revocation on delete. | ||
| // Without it a delete-then-reverify leaves the provider untrusted, and | ||
| // since that flag gates sign-in the org sits in a silent SSO outage. The | ||
| // comparison matches the revoking one exactly so the two stay symmetric. | ||
| // since that flag gates sign-in the org sits in a silent SSO outage. | ||
| if (flipped.length > 0) { | ||
| await tx | ||
| .update(ssoProvider) | ||
| .set({ domainVerified: true }) | ||
| .where( | ||
| and( | ||
| eq(ssoProvider.organizationId, organizationId), | ||
| sql`lower(regexp_replace(btrim(${ssoProvider.domain}), '^\\*\\.', '')) = ${flipped[0].domain}` | ||
| ) | ||
| ) | ||
| .where(providersOnDomain(flipped[0].domain)) | ||
| } | ||
|
|
||
| return flipped | ||
|
|
@@ -155,6 +172,13 @@ export const POST = withRouteHandler( | |
| .where(and(eq(ssoDomain.id, domainId), eq(ssoDomain.organizationId, organizationId))) | ||
| .limit(1) | ||
| if (current?.status === 'verified') { | ||
| // Re-grant rather than returning early: a provider can hold a verified | ||
| // domain with its own flag off, after an update whose grant was refused | ||
| // reverted the config. The proof is present, which authorizes this. | ||
| await db | ||
| .update(ssoProvider) | ||
| .set({ domainVerified: true }) | ||
| .where(providersOnDomain(current.domain)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-grant skips proof row lockMedium Severity The idempotent re-grant runs a standalone Reviewed by Cursor Bugbot for commit f5a3de6. Configure here. |
||
| return NextResponse.json({ success: true, data: { domain: toDomainResponse(current) } }) | ||
| } | ||
| return NextResponse.json( | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.