-
Notifications
You must be signed in to change notification settings - Fork 477
fix(ui): continue to the identity provider after a challenge #9620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1354498
52d9439
233efaf
22c0327
85b8d57
f673f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/ui': patch | ||
| --- | ||
|
|
||
| Fix the verification challenge card spinning indefinitely when continuing the sign-in after a completed challenge fails. The error is now shown, and "Try again" retries the continuation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/ui': patch | ||
| --- | ||
|
|
||
| Fix sign-ins that use an enterprise connection stranding on "Use another method" after a verification challenge, instead of continuing to the identity provider. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| import { ClerkRuntimeError } from '@clerk/shared/error'; | ||
| import type { SignInResource } from '@clerk/shared/types'; | ||
| import { waitFor } from '@testing-library/react'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { bindCreateFixtures } from '@/test/create-fixtures'; | ||
| import { render, screen } from '@/test/utils'; | ||
|
|
||
| import { SignInFactorOne } from '../SignInFactorOne'; | ||
| import { SignInFactorOneEnterpriseConnections } from '../SignInFactorOneEnterpriseConnections'; | ||
|
|
||
| const { createFixtures } = bindCreateFixtures('SignIn'); | ||
|
|
||
|
|
@@ -58,3 +62,55 @@ describe('SignInFactorOneEnterpriseConnections', () => { | |
| expect(oktaButton.querySelector('.cl-socialButtonsProviderInitialIcon')).toHaveTextContent('O'); | ||
| }); | ||
| }); | ||
|
|
||
| /** Two connections is what puts the user on this card rather than a direct hand-off. */ | ||
| const TWO_CONNECTIONS = [ | ||
| { strategy: 'enterprise_sso', enterpriseConnectionId: 'ent_acme', enterpriseConnectionName: 'Acme SSO' }, | ||
| { strategy: 'enterprise_sso', enterpriseConnectionId: 'ent_globex', enterpriseConnectionName: 'Globex SSO' }, | ||
| ]; | ||
|
|
||
| describe('SignInFactorOneEnterpriseConnections with a challenge', () => { | ||
| it('routes to the challenge when preparing the hand-off raises one', async () => { | ||
| // GIVEN a user choosing between two enterprise connections | ||
| const { wrapper, fixtures } = await createFixtures(f => { | ||
| f.withEmailAddress(); | ||
| f.startSignInWithEmailAddress(); | ||
| }); | ||
| (fixtures.signIn as unknown as SignInResource).supportedFirstFactors = TWO_CONNECTIONS as never; | ||
| // WHEN preparing the hand-off comes back gated: no redirect is issued and the call throws. | ||
| fixtures.signIn.authenticateWithRedirect.mockImplementationOnce(() => { | ||
| (fixtures.signIn as any).protectCheck = { status: 'pending', token: 'challenge-token-abc' }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -t f -a 'SignIn.ts' packages | while IFS= read -r file; do
rg -n -C 3 'protectCheck' "$file"
doneRepository: clerk/javascript Length of output: 1891 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- SignInFactorOneEnterpriseConnections.test.tsx ---'
cat -n packages/ui/src/components/SignIn/__tests__/SignInFactorOneEnterpriseConnections.test.tsx | sed -n '1,90p'
printf '%s\n' '--- SignInProtectCheck.test.tsx ---'
cat -n packages/ui/src/components/SignIn/__tests__/SignInProtectCheck.test.tsx | sed -n '1,120p'
printf '%s\n' '--- ProtectCheckResource declarations and imports ---'
rg -n -C 5 'class ProtectCheckResource|interface ProtectCheckResource|type ProtectCheckResource|ProtectCheckResource' packages | head -240
printf '%s\n' '--- fixture declarations ---'
rg -n -C 6 'fixtures\.signIn|const fixtures|signIn:' packages/ui/src/components/SignIn/__tests__ | head -260Repository: clerk/javascript Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- createFixtures implementation and fixture type ---'
fd -t f -a -i 'create-fixtures*' packages
rg -n -C 8 'bindCreateFixtures|createFixtures|fixtures:|signIn:' packages/ui/src/test packages/ui/src | head -220
printf '%s\n' '--- SignInResource and runtime SignIn declarations ---'
cat -n packages/shared/src/types/signIn.ts | sed -n '35,78p'
cat -n packages/clerk-js/src/core/resources/SignIn.ts | sed -n '105,135p'
cat -n packages/clerk-js/src/core/resources/SignIn.ts | sed -n '850,875p'
printf '%s\n' '--- existing typed mutable fixture patterns ---'
rg -n -C 4 'as unknown as SignInResource|as SignInResource|protectCheck\s*=' packages/ui/src/components/SignIn/__tests__ packages/ui/src | head -220Repository: clerk/javascript Length of output: 47225 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ProtectCheckResource contract ---'
cat -n packages/shared/src/types/signUpCommon.ts | sed -n '30,65p'
printf '%s\n' '--- fixture mock binding ---'
sed -n '1,90p' packages/ui/src/test/create-fixtures.tsx
rg -n -C 5 'function mockClerkMethods|const mockClerkMethods|mockClerkMethods' packages | head -80Repository: clerk/javascript Length of output: 11368 Use a typed mutable
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| throw new ClerkRuntimeError('challenge required', { code: 'protect_check_required' }); | ||
| }); | ||
|
|
||
| const { userEvent } = render(<SignInFactorOneEnterpriseConnections />, { wrapper }); | ||
| await userEvent.click(await screen.findByText('Acme SSO')); | ||
|
|
||
| // THEN the challenge is shown, instead of the card sitting there looking inert. | ||
| await waitFor(() => { | ||
| expect(fixtures.router.navigate).toHaveBeenCalledWith('../protect-check'); | ||
| }); | ||
| expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalledWith( | ||
| expect.objectContaining({ strategy: 'enterprise_sso', enterpriseConnectionId: 'ent_acme' }), | ||
| ); | ||
| }); | ||
|
|
||
| it('does not route to the challenge when the hand-off is issued normally', async () => { | ||
| // GIVEN the same card, but nothing gates the hand-off | ||
| const { wrapper, fixtures } = await createFixtures(f => { | ||
| f.withEmailAddress(); | ||
| f.startSignInWithEmailAddress(); | ||
| }); | ||
| (fixtures.signIn as unknown as SignInResource).supportedFirstFactors = TWO_CONNECTIONS as never; | ||
| fixtures.signIn.authenticateWithRedirect.mockResolvedValueOnce(undefined as never); | ||
|
|
||
| const { userEvent } = render(<SignInFactorOneEnterpriseConnections />, { wrapper }); | ||
| await userEvent.click(await screen.findByText('Globex SSO')); | ||
|
|
||
| // THEN the redirect owns the navigation and we must not steal it. | ||
| await waitFor(() => { | ||
| expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalled(); | ||
| }); | ||
| expect(fixtures.router.navigate).not.toHaveBeenCalledWith('../protect-check'); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.