diff --git a/.changeset/fix-allowed-redirect-origins-port.md b/.changeset/fix-allowed-redirect-origins-port.md new file mode 100644 index 00000000000..f798c3583c8 --- /dev/null +++ b/.changeset/fix-allowed-redirect-origins-port.md @@ -0,0 +1,5 @@ +--- +"@clerk/shared": patch +--- + +Fix satellite domain redirect validation incorrectly rejecting URLs with non-default ports. Redirect URLs like `https://app.example.net:5173` (common in local development with Vite or similar dev servers) are now correctly matched against `allowedRedirectOrigins` patterns such as `https://*.example.net`. diff --git a/.changeset/fix-signup-future-sso-transfer-stale-ref.md b/.changeset/fix-signup-future-sso-transfer-stale-ref.md new file mode 100644 index 00000000000..1ac84a8a110 --- /dev/null +++ b/.changeset/fix-signup-future-sso-transfer-stale-ref.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-js": patch +--- + +Fix `signUp.update()` sending a PATCH request to `/client/sign_ups` instead of `/client/sign_ups/{id}` after an SSO sign-in transitions to a sign-up flow (e.g. when legal acceptance is required). The `SignUpFuture` reference held by hooks is now preserved across the transition and reflects the correct sign-up id. diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 6b690c7261c..b06ac0b80ae 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -143,7 +143,11 @@ export class Client extends BaseResource implements ClientResource { this.id = data.id; this.sessions = (data.sessions || []).map(s => new Session(s)); - if (data.sign_up && this.signUp instanceof SignUp && this.signUp.id === data.sign_up.id) { + if (data.sign_up && this.signUp instanceof SignUp && (this.signUp.id === data.sign_up.id || !this.signUp.id)) { + // Update in-place when ids match OR when the existing signUp has no id yet + // (e.g. an SSO sign-in that transitions to a sign-up flow). Reusing the same + // SignUp instance preserves any SignUpFuture references held by hooks so they + // remain valid and reflect the correct id after the transition. this.signUp.__internal_updateFromJSON(data.sign_up); } else { this.signUp = new SignUp(data.sign_up); diff --git a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts index 63927fae344..84f7abe09df 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts @@ -579,6 +579,17 @@ describe('isAllowedRedirect', () => { ['https:evil.com', [/https:\/\/www\.clerk\.com/], false], ['//evil.com', [/https:\/\/www\.clerk\.com/], false], ['..//evil.com', ['https://www.clerk.com'], false], + // non-default ports — satellite apps in local dev (e.g. Vite on :5173) + // wildcard glob entries accept any port on the matching host + ['https://app.example.net:5173', ['https://*.example.net'], true], + ['https://app.example.net:5176', ['https://*.example.net'], true], + ['https://www.clerk.com:3000', ['https://*.clerk.com'], true], + // exact string entries remain port-sensitive (port is part of the origin) + ['https://www.clerk.com:3000', ['https://www.clerk.com'], false], + ['https://www.clerk.com:3000/path?q=1', ['https://www.clerk.com'], false], + // non-default port must not bypass domain validation + ['https://evil.com:5173', ['https://*.clerk.com'], false], + ['https://evil.clerk.com.evil.com:5173', ['https://*.clerk.com'], false], ]; const warnMock = vi.spyOn(logger, 'warnOnce'); diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 6c524febf4a..15f11961dae 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -436,12 +436,22 @@ export const isAllowedRedirect = const isSameOrigin = currentOrigin === url.origin; + // When the redirect URL includes a non-default port (common in local dev, e.g. :5173), + // url.origin includes the port (https://app.example.net:5173) while glob entries like + // https://*.example.net have none. For wildcard/glob entries only, also test the + // port-stripped origin so satellite apps on custom ports are not rejected. + // Exact string entries (no wildcard) remain port-sensitive to preserve the security boundary. + const portlessOrigin = url.port !== '' ? `${url.protocol}//${url.hostname}` : null; + const isAllowed = !isProblematicUrl(url) && (isSameOrigin || - allowedRedirectOrigins - .map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin)) - .some(origin => origin.test(trimTrailingSlash(url.origin)))); + allowedRedirectOrigins.some(origin => { + const pattern = typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin; + if (pattern.test(trimTrailingSlash(url.origin))) return true; + const isGlobEntry = typeof origin === 'string' && origin.includes('*'); + return isGlobEntry && portlessOrigin !== null && pattern.test(portlessOrigin); + })); if (!isAllowed) { logger.warnOnce(