Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-allowed-redirect-origins-port.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions .changeset/fix-signup-future-sso-transfer-stale-ref.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion packages/clerk-js/src/core/resources/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/internal/clerk-js/__tests__/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
16 changes: 13 additions & 3 deletions packages/shared/src/internal/clerk-js/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down