Skip to content
Merged
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/sign-in-different-account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Add a "Back" action to the sign-in second-factor and client-trust steps, letting a stuck user abandon the attempt and return to the sign-in start.
26 changes: 26 additions & 0 deletions integration/tests/client-trust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,31 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withNeedsClientTrust] })(
// Sign in again with a now "known" device
await u.po.expect.toBeSignedIn();
});

test('can navigate back to the sign-in start from the client trust step', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

// Sign in with email and password on a new device to reach the client-trust step.
await u.po.signIn.goTo();
await u.po.signIn.setIdentifier(fakeUser.email);
await u.po.signIn.continue();
await u.po.signIn.setPassword(fakeUser.password);
await u.po.signIn.continue();

await u.page.waitForURL(/\/sign-in\/client-trust/);
await expect(u.page.getByText("You're signing in from a new device.")).toBeVisible();

// The only verification method here is the same identifier, so a user who cannot
// complete it (e.g. wants social instead) needs a way out. "Back" abandons the
// attempt and returns to the sign-in start.
const back = u.page.getByRole('link', { name: /^back$/i });
await expect(back).toBeVisible();
await back.click();

// Back on the sign-in start, where another sign-in method can be chosen.
await u.page.waitForURL(url => !/\/sign-in\/client-trust/.test(url.href));
await expect(u.po.signIn.getIdentifierInput()).toBeVisible();
await u.po.expect.toBeSignedOut();
});
},
);
52 changes: 52 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,57 @@ 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(test, {
fictionalEmail: true,
withPhoneNumber: true,
withPassword: true,
});
await u.services.users.createBapiUser(user);

try {
// 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();
} finally {
await user.deleteIfExists();
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
);
2 changes: 1 addition & 1 deletion packages/ui/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{ "path": "./dist/ui.shared.browser.js", "maxSize": "42KB" },
{ "path": "./dist/framework*.js", "maxSize": "44KB" },
{ "path": "./dist/vendors*.js", "maxSize": "73KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "132KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "133KB" },
{ "path": "./dist/signin*.js", "maxSize": "17KB" },
{ "path": "./dist/signup*.js", "maxSize": "13KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
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
9 changes: 9 additions & 0 deletions 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 Down Expand Up @@ -118,6 +119,14 @@ export const VerificationCodeContent = (props: PropsWithChildren<VerificationCod
/>
</Card.Action>
)}
{props.onDifferentAccountClicked && (
<Card.Action elementId='signIn'>
<Card.ActionLink
localizationKey={localizationKeys('backButton')}
onClick={props.onDifferentAccountClicked}
/>
</Card.Action>
)}
</Col>
</Col>
</>
Expand Down
Loading