diff --git a/.changeset/signup-challenge-before-sso.md b/.changeset/signup-challenge-before-sso.md new file mode 100644 index 00000000000..ecebccc9a6b --- /dev/null +++ b/.changeset/signup-challenge-before-sso.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Run a sign-up's verification challenge before handing off to an enterprise connection, matching the order used elsewhere in the flow. diff --git a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts index 28ed2bbcbf7..4abe15c1059 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts @@ -175,7 +175,7 @@ describe('completeSignUpFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('verify-email', { searchParams: new URLSearchParams() }); }); - it('prioritizes enterprise_sso over protect_check', async () => { + it('prioritizes protect_check over enterprise_sso', async () => { const mockSignUp = { status: 'missing_requirements', missingFields: ['enterprise_sso', 'protect_check'] as SignUpField[], @@ -191,6 +191,45 @@ describe('completeSignUpFlow', () => { redirectUrlComplete: 'https://example.com/done', }); + expect(mockNavigate).toHaveBeenCalledWith('protect-check', { searchParams: new URLSearchParams() }); + expect(mockAuthenticateWithRedirect).not.toHaveBeenCalled(); + }); + + it('hands off to the connection once no challenge is left', async () => { + const mockSignUp = { + status: 'missing_requirements', + missingFields: ['enterprise_sso'] as SignUpField[], + authenticateWithRedirect: mockAuthenticateWithRedirect, + } as unknown as SignUpResource; + + await completeSignUpFlow({ + signUp: mockSignUp, + protectCheckPath: 'protect-check', + handleComplete: mockHandleComplete, + navigate: mockNavigate, + redirectUrl: 'https://example.com/acs', + redirectUrlComplete: 'https://example.com/done', + }); + + expect(mockAuthenticateWithRedirect).toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('still hands off to the connection when the caller has no challenge route', async () => { + const mockSignUp = { + status: 'missing_requirements', + missingFields: ['enterprise_sso', 'protect_check'] as SignUpField[], + authenticateWithRedirect: mockAuthenticateWithRedirect, + } as unknown as SignUpResource; + + await completeSignUpFlow({ + signUp: mockSignUp, + handleComplete: mockHandleComplete, + navigate: mockNavigate, + redirectUrl: 'https://example.com/acs', + redirectUrlComplete: 'https://example.com/done', + }); + expect(mockAuthenticateWithRedirect).toHaveBeenCalled(); expect(mockNavigate).not.toHaveBeenCalled(); }); diff --git a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts index 69773089ca2..83fe66e92f2 100644 --- a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts +++ b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts @@ -31,6 +31,18 @@ export const completeSignUpFlow = ({ removeClerkQueryParam('__clerk_invitation_token'); return handleComplete && handleComplete(); } else if (signUp.status === 'missing_requirements') { + // The protect_check field is the authoritative gating signal. Sign-up also surfaces it + // via a missing_fields entry; treat either as equivalent. + // + // This runs before the enterprise SSO hand-off below, which is the order + // `navigateToNextStepSignUp` already uses: both fields can be missing at once, and handing + // the sign-up to the identity provider first defers the challenge until the round trip is + // over. Resolving it here returns to this function with only the hand-off left to do. + const isProtectGated = !!signUp.protectCheck || signUp.missingFields.some(mf => mf === 'protect_check'); + if (isProtectGated && protectCheckPath) { + return navigate(protectCheckPath, { searchParams: forwardClerkQueryParams() }); + } + if (signUp.missingFields.some(mf => mf === 'enterprise_sso')) { if (!redirectUrl || !redirectUrlComplete) { throw new Error( @@ -49,13 +61,6 @@ export const completeSignUpFlow = ({ const params = forwardClerkQueryParams(); - // The protect_check field is the authoritative gating signal. Sign-up also surfaces it - // via a missing_fields entry; treat either as equivalent. - const isProtectGated = !!signUp.protectCheck || signUp.missingFields.some(mf => mf === 'protect_check'); - if (isProtectGated && protectCheckPath) { - return navigate(protectCheckPath, { searchParams: params }); - } - if (signUp.unverifiedFields?.includes('email_address') && verifyEmailPath) { return navigate(verifyEmailPath, { searchParams: params }); }