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/signup-challenge-before-sso.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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();
});
Expand Down
19 changes: 12 additions & 7 deletions packages/shared/src/internal/clerk-js/completeSignUpFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 });
}
Expand Down
Loading