Skip to content

Commit 0291f43

Browse files
committed
fix: Fix passkey mfa sign in flow
1 parent a0c706b commit 0291f43

File tree

8 files changed

+115
-38
lines changed

8 files changed

+115
-38
lines changed

lib/ts/recipe/webauthn/components/features/mfa/index.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const useFeatureReducer = (): [WebAuthnMFAState, React.Dispatch<WebAuthnM
7373
error: undefined,
7474
deviceSupported: false,
7575
canRegisterPasskey: false,
76+
hasRegisteredPassKey: false,
7677
loaded: false,
7778
showBackButton: true,
7879
email: undefined,
@@ -242,7 +243,8 @@ export const MFAFeature: React.FC<
242243
<ComponentOverrideContext.Provider value={recipeComponentOverrides}>
243244
<FeatureWrapper
244245
useShadowDom={SuperTokens.getInstanceOrThrow().useShadowDom}
245-
defaultStore={defaultTranslationsWebauthn}>
246+
defaultStore={defaultTranslationsWebauthn}
247+
>
246248
<MFAFeatureInner {...props} />
247249
</FeatureWrapper>
248250
</ComponentOverrideContext.Provider>
@@ -348,12 +350,6 @@ function useOnLoad(
348350
}
349351
}
350352

351-
const alreadySetup = mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
352-
if (alreadySetup) {
353-
dispatch({ type: "setError", accessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR_RELOAD" });
354-
return;
355-
}
356-
357353
// If the next array only has a single option, it means the we were redirected here
358354
// automatically during the sign in process. In that case, anywhere the back button
359355
// could go would redirect back here, making it useless.
@@ -365,21 +361,23 @@ function useOnLoad(
365361
const mfaInfoEmails = mfaInfo.emails[FactorIds.WEBAUTHN];
366362
const email = mfaInfoEmails ? mfaInfoEmails[0] : undefined;
367363

368-
const canRegisterPasskey = !mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
364+
const canRegisterPasskey = mfaInfo.factors.allowedToSetup.includes(FactorIds.WEBAUTHN);
365+
const hasRegisteredPassKey = mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
369366
const browserSupportsWebauthnResponse = await props.recipe.webJSRecipe.doesBrowserSupportWebAuthn({
370367
userContext: userContext,
371368
});
372-
const browserSupportsWebauthn =
369+
const deviceSupported =
373370
browserSupportsWebauthnResponse.status === "OK" &&
374371
browserSupportsWebauthnResponse?.browserSupportsWebauthn;
375372

376373
dispatch({
377374
type: "load",
378375
canRegisterPasskey,
376+
hasRegisteredPassKey,
379377
error,
380378
showBackButton,
381379
email,
382-
deviceSupported: browserSupportsWebauthn,
380+
deviceSupported,
383381
});
384382
},
385383
[dispatch, recipeImplementation, props.recipe, userContext]

lib/ts/recipe/webauthn/components/themes/mfa/index.tsx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,15 @@ export default MFAThemeWrapper;
3838

3939
export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
4040
const { onBackButtonClicked, onSignIn } = props;
41-
const [activeScreen, setActiveScreen] = React.useState<MFAScreens>(MFAScreens.SignIn);
41+
const [activeScreen, setActiveScreen] = React.useState<MFAScreens>(() => {
42+
if (!props.featureState.hasRegisteredPassKey) {
43+
return props.featureState.email ? MFAScreens.SignUpConfirmation : MFAScreens.SignUp;
44+
}
45+
return MFAScreens.SignIn;
46+
});
4247
const [signUpEmail, setSignUpEmail] = React.useState<string>("");
4348
const t = useTranslation();
4449

45-
const onRegisterPasskeyClick = React.useCallback(() => {
46-
if (!props.featureState.canRegisterPasskey) {
47-
return;
48-
}
49-
if (props.featureState.email) {
50-
setActiveScreen(MFAScreens.SignUpConfirmation);
51-
} else {
52-
setActiveScreen(MFAScreens.SignUp);
53-
}
54-
}, [props.featureState.email, props.featureState.canRegisterPasskey]);
55-
5650
const onSignUpContinue = React.useCallback(
5751
(email: string) => {
5852
if (!props.featureState.canRegisterPasskey) {
@@ -64,6 +58,17 @@ export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
6458
[props.featureState.canRegisterPasskey]
6559
);
6660

61+
const onRegisterPasskeyClick = React.useCallback(() => {
62+
if (!props.featureState.canRegisterPasskey) {
63+
return;
64+
}
65+
if (props.featureState.email) {
66+
setActiveScreen(MFAScreens.SignUpConfirmation);
67+
} else {
68+
setActiveScreen(MFAScreens.SignUp);
69+
}
70+
}, [props.featureState.email, props.featureState.canRegisterPasskey]);
71+
6772
const clearError = React.useCallback(() => {
6873
props.dispatch({ type: "setError", error: undefined });
6974
}, [props]);
@@ -75,25 +80,32 @@ export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
7580
[props]
7681
);
7782

78-
const onClickSignUpBackButton = React.useCallback(() => {
79-
if (!props.featureState.canRegisterPasskey) {
83+
const onClickSignUpConfirmationBackButton = React.useCallback(() => {
84+
if (!props.featureState.email) {
85+
setActiveScreen(MFAScreens.SignUp);
8086
return;
8187
}
82-
setActiveScreen(MFAScreens.SignIn);
83-
}, [props.featureState.canRegisterPasskey]);
8488

85-
const onClickSignUpConfirmationBackButton = React.useCallback(() => {
86-
if (props.featureState.email) {
87-
setActiveScreen(MFAScreens.SignIn);
88-
} else {
89-
setActiveScreen(MFAScreens.SignUp);
89+
if (!props.featureState.hasRegisteredPassKey) {
90+
onBackButtonClicked();
91+
return;
9092
}
93+
94+
setActiveScreen(MFAScreens.SignIn);
9195
}, [props.featureState.email]);
9296

9397
const onFetchError = React.useCallback(() => {
9498
onError("SOMETHING_WENT_WRONG_ERROR");
9599
}, [onError]);
96100

101+
const onClickSignUpBackButton = React.useCallback(() => {
102+
if (!props.featureState.hasRegisteredPassKey) {
103+
onBackButtonClicked();
104+
return;
105+
}
106+
setActiveScreen(MFAScreens.SignIn);
107+
}, [props.featureState.hasRegisteredPassKey, onBackButtonClicked()]);
108+
97109
if (!props.featureState.loaded) {
98110
return <WebauthnMFALoadingScreen />;
99111
}
@@ -116,8 +128,8 @@ export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
116128
canRegisterPasskey={props.featureState.canRegisterPasskey}
117129
onSignIn={onSignIn}
118130
error={props.featureState.error}
119-
onRegisterPasskeyClick={onRegisterPasskeyClick}
120131
deviceSupported={props.featureState.deviceSupported}
132+
onRegisterPasskeyClick={onRegisterPasskeyClick}
121133
/>
122134
) : activeScreen === MFAScreens.SignUp ? (
123135
<WebauthnMFASignUp

lib/ts/recipe/webauthn/components/themes/mfa/signIn.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { PasskeyNotSupportedError } from "../error/passkeyNotSupportedError";
1212
export type MFASignInProps = {
1313
onBackButtonClicked?: () => void;
1414
onSignIn: () => Promise<void>;
15-
onRegisterPasskeyClick: () => void;
16-
canRegisterPasskey: boolean;
1715
error: string | undefined;
1816
deviceSupported: boolean;
17+
canRegisterPasskey: boolean;
18+
onRegisterPasskeyClick: () => void;
1919
};
2020

2121
export const WebauthnMFASignIn = withOverride(
@@ -64,7 +64,7 @@ export const WebauthnMFASignIn = withOverride(
6464
</div>
6565
<div data-supertokens="headerSubtitle secondaryText">
6666
<span data-supertokens="link" onClick={props.onRegisterPasskeyClick}>
67-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
67+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
6868
</span>
6969
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_SUBTITLE")}
7070
</div>

lib/ts/recipe/webauthn/components/themes/mfa/signUp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const WebauthnMFASignUp = withOverride(
3737
<Fragment>
3838
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
3939
<BackButton onClick={props.onBackButtonClicked} />
40-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
40+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
4141
<span data-supertokens="backButtonPlaceholder backButtonCommon">
4242
{/* empty span for spacing the back button */}
4343
</span>

lib/ts/recipe/webauthn/components/themes/mfa/signUpConfirmation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const WebauthnMFASignUpConfirmation = withOverride(
3131
<Fragment>
3232
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
3333
<BackButton onClick={props.onBackButtonClicked} />
34-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
34+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
3535
<span data-supertokens="backButtonPlaceholder backButtonCommon">
3636
{/* empty span for spacing the back button */}
3737
</span>

lib/ts/recipe/webauthn/components/themes/translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ export const defaultTranslationsWebauthn = {
5858
"To finish signing in, click the button and follow the browser instructions.",
5959
WEBAUTHN_MFA_DIVIDER: "or",
6060
WEBAUTHN_MFA_REGISTER_PASSKEY_SUBTITLE: "Set up a new authentication method to use for future logins.",
61-
WEBAUTHN_MFA_REGISTER_PASSKEY_LINK: "Register a passkey",
61+
WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE: "Register a passkey",
6262
},
6363
};

lib/ts/recipe/webauthn/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export type WebAuthnMFAAction =
275275
email: string | undefined;
276276
showBackButton: boolean;
277277
canRegisterPasskey: boolean;
278+
hasRegisteredPassKey: boolean;
278279
};
279280

280281
type WebAuthnMFAInitialState = {
@@ -284,6 +285,7 @@ type WebAuthnMFAInitialState = {
284285
deviceSupported: boolean;
285286
showBackButton: boolean;
286287
canRegisterPasskey: false;
288+
hasRegisteredPassKey: false;
287289
email: undefined;
288290
};
289291

@@ -294,6 +296,7 @@ type WebAuthnMFALoadedState = {
294296
deviceSupported: boolean;
295297
showBackButton: boolean;
296298
canRegisterPasskey: boolean;
299+
hasRegisteredPassKey: boolean;
297300
email: string | undefined;
298301
};
299302

test/end-to-end/mfa.factorscreen.webauthn.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ import {
3131
setupBrowser,
3232
getGeneralError,
3333
backendHook,
34+
setInputValues,
35+
submitForm,
3436
} from "../helpers";
3537
import { MFA_INFO_API, SOMETHING_WENT_WRONG_ERROR, TEST_CLIENT_BASE_URL } from "../constants";
38+
import { getTestPhoneNumber } from "../exampleTestHelpers";
3639
import {
3740
tryEmailPasswordSignUp,
41+
tryPasswordlessSignInUp,
3842
waitForDashboard,
3943
tryEmailPasswordSignIn,
4044
chooseFactor,
@@ -441,5 +445,65 @@ describe("SuperTokens SignIn w/ MFA", function () {
441445
});
442446
});
443447
});
448+
449+
it("should show the sign up confirmation screen if the user does not have a registered passkey", async () => {
450+
await setupST({
451+
...appConfig,
452+
mfaInfo: {
453+
requirements: [factorId],
454+
alreadySetup: [],
455+
allowedToSetup: [factorId],
456+
},
457+
});
458+
459+
await tryEmailPasswordSignIn(page, email);
460+
await page.goto(`${TEST_CLIENT_BASE_URL}/auth/mfa/${factorId}`);
461+
await page.waitForNavigation({ waitUntil: "networkidle0" });
462+
463+
await waitForSTElement(page, "[data-supertokens~=passkeyConfirmationContainer]");
464+
465+
const backButton = await waitForSTElement(page, "[data-supertokens~=backButton]");
466+
backButton.click();
467+
await waitForSTElement(page, "[data-supertokens~=factorChooserList]");
468+
});
469+
470+
// it("should show the sign up screen if the user doesn not have a registered passkey and no linked email", async () => {
471+
// await setupST({
472+
// ...appConfig,
473+
// mfaInfo: {
474+
// requirements: [factorId],
475+
// alreadySetup: [],
476+
// allowedToSetup: [factorId],
477+
// },
478+
// });
479+
//
480+
// const phoneNumber = getTestPhoneNumber();
481+
// await tryPasswordlessSignInUp(page, phoneNumber, undefined, true);
482+
//
483+
// await page.goto(`${TEST_CLIENT_BASE_URL}/auth/mfa/${factorId}`);
484+
// await page.waitForNavigation({ waitUntil: "networkidle0" });
485+
//
486+
// await waitForSTElement(page, "[data-supertokens~=signUpFormInnerContainer]");
487+
// });
488+
489+
it("should show the sign in screen alongside the passkey registration button if the user is allowed to setup more passkeys", async () => {
490+
await setupST({
491+
...appConfig,
492+
mfaInfo: {
493+
requirements: [factorId],
494+
alreadySetup: [factorId],
495+
allowedToSetup: [factorId],
496+
},
497+
});
498+
499+
await tryEmailPasswordSignIn(page, email);
500+
501+
await page.goto(`${TEST_CLIENT_BASE_URL}/auth/mfa/${factorId}`);
502+
await page.waitForNavigation({ waitUntil: "networkidle0" });
503+
504+
await waitForSTElement(page, "[data-supertokens~=button]");
505+
await waitForSTElement(page, "[data-supertokens~=passkeyMfaSignInDivider]");
506+
await waitForSTElement(page, "[data-supertokens~=link]");
507+
});
444508
});
445509
});

0 commit comments

Comments
 (0)