diff --git a/.changeset/sign-in-different-account.md b/.changeset/sign-in-different-account.md new file mode 100644 index 00000000000..3318b126a59 --- /dev/null +++ b/.changeset/sign-in-different-account.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Add a "Back" action to the sign-in two-step verification (second factor) step. This lets a user who reached the 2FA screen with the wrong account (for example, after signing in with the wrong social account) abandon the attempt and return to the sign-in start to sign in again, instead of being stuck with no way out. diff --git a/integration/tests/session-tasks-setup-mfa.test.ts b/integration/tests/session-tasks-setup-mfa.test.ts index 1bdb3c1e8f7..26a23e7335a 100644 --- a/integration/tests/session-tasks-setup-mfa.test.ts +++ b/integration/tests/session-tasks-setup-mfa.test.ts @@ -203,5 +203,55 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasksSetupMfa] })( await user.deleteIfExists(); }); + + test('can sign in as a different account from the two-step verification step', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const user = u.services.users.createFakeUser({ + fictionalEmail: true, + withPhoneNumber: true, + withPassword: true, + }); + await u.services.users.createBapiUser(user); + + // Enroll SMS as a second factor using the user's existing phone number. + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: user.email, password: user.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/page-protected'); + await u.page.getByText(/set up two-step verification/i).waitFor({ state: 'visible' }); + await u.page.getByRole('button', { name: /sms code/i }).click(); + const formattedPhoneNumber = stringPhoneNumber(user.phoneNumber); + await u.page.getByRole('button', { name: formattedPhoneNumber }).click(); + await u.page.getByText(/save these backup codes/i).waitFor({ state: 'visible', timeout: 10000 }); + await u.po.signIn.continue(); + await u.page.waitForAppUrl('/page-protected'); + await u.po.expect.toBeSignedIn(); + + // Sign out and back in so the sign-in flow now requires the second factor. + await u.page.signOut(); + await u.page.context().clearCookies(); + + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.getIdentifierInput().fill(user.email); + await u.po.signIn.setInstantPassword(user.password); + await u.po.signIn.continue(); + + // We are now on the two-step verification (SMS second factor) step, with no way to + // complete it if this is the wrong account. + await u.page.getByText(/check your phone/i).waitFor({ state: 'visible' }); + + // The "Back" action abandons the attempt and returns to the start. + const differentAccount = u.page.getByRole('link', { name: /^back$/i }); + await expect(differentAccount).toBeVisible(); + await differentAccount.click(); + + // Back on the sign-in start, where a different account can be used. + await expect(u.po.signIn.getIdentifierInput()).toBeVisible(); + + await user.deleteIfExists(); + }); }, ); diff --git a/packages/ui/src/components/SignIn/SignInFactorTwoCodeForm.tsx b/packages/ui/src/components/SignIn/SignInFactorTwoCodeForm.tsx index 69dd2a85dd0..75b2513288f 100644 --- a/packages/ui/src/components/SignIn/SignInFactorTwoCodeForm.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorTwoCodeForm.tsx @@ -46,6 +46,8 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => const supportEmail = useSupportEmail(); const clerk = useClerk(); + const signInAsDifferentUser = () => navigate('../'); + // Only show the new device verification notice if the user is new // and no attributes are explicitly used for second factor. // Retained for backwards compatibility. @@ -134,6 +136,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => profileImageUrl={signIn.userData.imageUrl} identityPreviewEditButtonAriaLabel={localizationKeys('identityPreviewEditButton__identifier')} onShowAlternativeMethodsClicked={props.onShowAlternativeMethodsClicked} + onDifferentAccountClicked={signInAsDifferentUser} > {isResettingPassword(signIn) && ( ) => { @@ -118,6 +119,14 @@ export const VerificationCodeContent = (props: PropsWithChildren )} + {props.onDifferentAccountClicked && ( + + + + )}