From d7b60e67e3465ac9f04f2582ab445b9f827c06fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Oct 2026 14:42:40 +0000 Subject: [PATCH] fix(js): use custom signInUrl for factor-one and factor-two URLs in OAuth callbacks --- .../fix-factor-one-custom-signin-url.md | 5 ++ .../clerk-js/src/core/__tests__/clerk.test.ts | 76 +++++++++++++++++++ packages/clerk-js/src/core/clerk.ts | 4 +- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-factor-one-custom-signin-url.md diff --git a/.changeset/fix-factor-one-custom-signin-url.md b/.changeset/fix-factor-one-custom-signin-url.md new file mode 100644 index 00000000000..b8d8e9f555b --- /dev/null +++ b/.changeset/fix-factor-one-custom-signin-url.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-js": patch +--- + +fix(clerk-js): OAuth callbacks now respect custom sign-in URLs when routing to factor-one or factor-two. When a custom `signInUrl` is provided via `handleRedirectCallback`, the factor selection pages will be built from that URL instead of the dashboard-configured default, ensuring a consistent sign-in experience for custom integrations. diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 314c28a7ccf..a6a580c8718 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2515,6 +2515,46 @@ describe('Clerk singleton', () => { }); }); + it('builds the factor-two URL from the signInUrl param when provided', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + onWindowLocationHost: () => false, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [], + signIn: new SignIn({ + status: 'needs_second_factor', + first_factor_verification: { + status: 'verified', + strategy: 'oauth_google', + external_verification_redirect_url: null, + error: null, + }, + second_factor_verification: null, + } as any as SignInJSON), + signUp: new SignUp(null), + }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(mockedLoadOptions); + + await sut.handleRedirectCallback({ signInUrl: '/custom-sign-in' }); + + await waitFor(() => { + expect(mockNavigate.mock.calls[0][0]).toBe('/custom-sign-in#/factor-two'); + }); + }); + it('redirects an existing user to a custom 2fa page if 2fa is enabled and secondFactorUrl is passed during sign in', async () => { mockEnvironmentFetch.mockReturnValue( Promise.resolve({ @@ -2858,6 +2898,42 @@ describe('Clerk singleton', () => { }); }); + it('builds the factor-one URL from the signInUrl param when provided', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [], + signIn: new SignIn({ + status: 'needs_first_factor', + supported_first_factors: [ + { strategy: 'enterprise_sso', enterprise_connection_id: 'ec_1', enterprise_connection_name: 'A' }, + { strategy: 'enterprise_sso', enterprise_connection_id: 'ec_2', enterprise_connection_name: 'B' }, + ], + } as unknown as SignInJSON), + signUp: new SignUp(null), + }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(mockedLoadOptions); + + await sut.handleRedirectCallback({ signInUrl: '/custom-sign-in' }); + + await waitFor(() => { + expect(mockNavigate.mock.calls[0][0]).toBe('/custom-sign-in#/factor-one'); + }); + }); + it('redirects user to sign-in when a single bare enterprise_sso factor needs the first factor', async () => { mockEnvironmentFetch.mockReturnValue( Promise.resolve({ diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d124caf535e..d01bea45459 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2688,12 +2688,12 @@ export class Clerk implements ClerkInterface { const navigateToFactorOne = makeNavigate( params.firstFactorUrl || - buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-one' }, { stringify: true }), + buildURL({ base: params.signInUrl || displayConfig.signInUrl, hashPath: '/factor-one' }, { stringify: true }), ); const navigateToFactorTwo = makeNavigate( params.secondFactorUrl || - buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), + buildURL({ base: params.signInUrl || displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), ); const navigateToResetPassword = makeNavigate(