From b90ea1534ad455544d00bf4756c2587438edb66c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 17 Jul 2026 11:34:56 -0400 Subject: [PATCH 1/2] feat(ui): add "Sign in as a different account" action to sign-in second factor Adds a footer action on the sign-in two-step verification (second factor) step that abandons the current sign-in attempt and returns to the sign-in start, so a user who reached 2FA with the wrong account (e.g. wrong social account) can sign in again instead of being stuck. --- .changeset/sign-in-different-account.md | 7 +++++++ packages/localizations/src/en-US.ts | 1 + packages/shared/src/types/localization.ts | 6 ++++++ .../components/SignIn/SignInFactorTwoCodeForm.tsx | 3 +++ packages/ui/src/elements/VerificationCodeCard.tsx | 12 +++++++++++- 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .changeset/sign-in-different-account.md diff --git a/.changeset/sign-in-different-account.md b/.changeset/sign-in-different-account.md new file mode 100644 index 00000000000..0ac88ec8b31 --- /dev/null +++ b/.changeset/sign-in-different-account.md @@ -0,0 +1,7 @@ +--- +'@clerk/ui': patch +'@clerk/localizations': patch +'@clerk/shared': patch +--- + +Add a "Sign in as a different account" 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/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index d766d4ce383..65de459f45f 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -1286,6 +1286,7 @@ export const enUS: LocalizationResource = { subtitle: 'Your backup code is the one you got when setting up two-step authentication.', title: 'Enter a backup code', }, + differentAccountAction: 'Sign in as a different account', emailCode: { formTitle: 'Verification code', resendButton: "Didn't receive a code? Resend", diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 9ca7ecee35e..d425d124d6b 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -551,6 +551,12 @@ export type __internal_LocalizationResource = { formSubtitle: LocalizationValue; resendButton: LocalizationValue; }; + /** + * Footer action on the second-factor step that abandons the current sign-in + * attempt and returns the user to the sign-in start so they can sign in with + * a different account. + */ + differentAccountAction: LocalizationValue; newDeviceVerificationNotice: LocalizationValue; phoneCodeMfa: { title: LocalizationValue; 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) && ( ) => { @@ -41,7 +42,16 @@ export const VerificationCodeCard = (props: PropsWithChildren - + + {props.onDifferentAccountClicked && ( + + + + )} + ); }; From 35f1b84a76aa4b3d2eed435d05c61321c0859489 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 17 Jul 2026 11:40:16 -0400 Subject: [PATCH 2/2] test(e2e): verify sign in as a different account escapes the second-factor step --- .../tests/session-tasks-setup-mfa.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/integration/tests/session-tasks-setup-mfa.test.ts b/integration/tests/session-tasks-setup-mfa.test.ts index 1bdb3c1e8f7..f0d340e0827 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 "Sign in as a different account" action abandons the attempt and returns to the start. + const differentAccount = u.page.getByRole('link', { name: /sign in as a different account/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(); + }); }, );