Skip to content
Draft
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-factor-one-custom-signin-url.md
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading