Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions .changeset/sign-in-different-account.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions integration/tests/session-tasks-setup-mfa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Comment thread
alexcarpenter marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
);
1 change: 1 addition & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components/SignIn/SignInFactorTwoCodeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -134,6 +136,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) =>
profileImageUrl={signIn.userData.imageUrl}
identityPreviewEditButtonAriaLabel={localizationKeys('identityPreviewEditButton__identifier')}
onShowAlternativeMethodsClicked={props.onShowAlternativeMethodsClicked}
onDifferentAccountClicked={signInAsDifferentUser}
>
{isResettingPassword(signIn) && (
<Text
Expand Down
12 changes: 11 additions & 1 deletion packages/ui/src/elements/VerificationCodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type VerificationCodeCardProps = {
onShowAlternativeMethodsClicked?: React.MouseEventHandler;
onIdentityPreviewEditClicked?: React.MouseEventHandler;
onBackLinkClicked?: React.MouseEventHandler;
onDifferentAccountClicked?: React.MouseEventHandler;
};

export const VerificationCodeCard = (props: PropsWithChildren<VerificationCodeCardProps>) => {
Expand All @@ -41,7 +42,16 @@ export const VerificationCodeCard = (props: PropsWithChildren<VerificationCodeCa
<VerificationCodeContent {...props} />
</Card.Content>

<Card.Footer />
<Card.Footer>
{props.onDifferentAccountClicked && (
<Card.Action elementId='signIn'>
<Card.ActionLink
localizationKey={localizationKeys('signIn.differentAccountAction')}
onClick={props.onDifferentAccountClicked}
/>
</Card.Action>
)}
</Card.Footer>
</Card.Root>
);
};
Expand Down
Loading