From 4bca5f8bf6367560c9c50a75962f298854914d76 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 28 Nov 2024 12:35:31 +0530 Subject: [PATCH] Use --- .../components/utils/second-factor-choice.ts | 92 +++++++++++++++++++ web/packages/accounts/pages/credentials.tsx | 1 - web/packages/accounts/pages/verify.tsx | 12 ++- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 web/packages/accounts/components/utils/second-factor-choice.ts diff --git a/web/packages/accounts/components/utils/second-factor-choice.ts b/web/packages/accounts/components/utils/second-factor-choice.ts new file mode 100644 index 00000000000..754691244dd --- /dev/null +++ b/web/packages/accounts/components/utils/second-factor-choice.ts @@ -0,0 +1,92 @@ +/** + * @file This code is conceputally related to `SecondFactorChoice.tsx`, but + * needs to be in a separate file to allow fast refresh. + */ + +import { useModalVisibility } from "@/base/components/utils/modal"; +import { useCallback, useMemo, useRef } from "react"; +import type { UserVerificationResponse } from "../../services/user"; +import type { SecondFactorType } from "../SecondFactorChoice"; + +/** + * A convenience hook for keeping track of the state and logic that is needed + * after password verification to determine which second factor (if any) we + * should be asking the user for. + * + * This is a rather ad-hoc abstraction meant to be used in a very specific way; + * the only intent is to reduce code duplication between the two pages that need + * this choice. + */ +export const useSecondFactorChoiceIfNeeded = () => { + const resolveSecondFactorChoice = useRef< + | ((value: SecondFactorType | PromiseLike) => void) + | undefined + >(); + const { + show: showSecondFactorChoice, + props: secondFactorChoiceVisibilityProps, + } = useModalVisibility(); + + const onSelect = useCallback((factor: SecondFactorType) => { + const resolve = resolveSecondFactorChoice.current!; + resolveSecondFactorChoice.current = undefined; + resolve(factor); + }, []); + + const secondFactorChoiceProps = useMemo( + () => ({ ...secondFactorChoiceVisibilityProps, onSelect }), + [secondFactorChoiceVisibilityProps, onSelect], + ); + + const userVerificationResultAfterResolvingSecondFactorChoice = useCallback( + async (response: UserVerificationResponse) => { + const { + twoFactorSessionID: _twoFactorSessionIDV1, + twoFactorSessionIDV2: _twoFactorSessionIDV2, + passkeySessionID: _passkeySessionID, + } = response; + + // When the user has both TOTP and pk set as the second factor, + // we'll get two session IDs. For backward compat, the TOTP session + // ID will be in a V2 attribute during a transient migration period. + // + // Note the use of || instead of ?? since _twoFactorSessionIDV1 will + // be an empty string, not undefined, if it is unset. We might need + // to add a `xxx-eslint-disable + // @typescript-eslint/prefer-nullish-coalescing` here too later. + const _twoFactorSessionID = + _twoFactorSessionIDV1 || _twoFactorSessionIDV2; + + let passkeySessionID: string | undefined; + let twoFactorSessionID: string | undefined; + // If both factors are set, ask the user which one they want to use. + if (_twoFactorSessionID && _passkeySessionID) { + const choice = await new Promise( + (resolve) => { + resolveSecondFactorChoice.current = resolve; + showSecondFactorChoice(); + }, + ); + switch (choice) { + case "passkey": + passkeySessionID = _passkeySessionID; + break; + case "totp": + twoFactorSessionID = _twoFactorSessionID; + break; + } + } else { + passkeySessionID = _passkeySessionID; + twoFactorSessionID = _twoFactorSessionID; + } + + return { ...response, passkeySessionID, twoFactorSessionID }; + }, + [showSecondFactorChoice], + ); + + return { + secondFactorChoiceProps, + userVerificationResultAfterResolvingSecondFactorChoice, + }; +}; diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index 1cf601317ef..89aa8d17708 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -217,7 +217,6 @@ const Page: React.FC = ({ appContext }) => { if (sessionValidityCheck) await sessionValidityCheck; const cryptoWorker = await sharedCryptoWorker(); - const { keyAttributes, encryptedToken, diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index 666a410b207..dfbeb387d46 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -30,6 +30,8 @@ import { LoginFlowFormFooter, VerifyingPasskey, } from "../components/LoginComponents"; +import { SecondFactorChoice } from "../components/SecondFactorChoice"; +import { useSecondFactorChoiceIfNeeded } from "../components/utils/second-factor-choice"; import { PAGES } from "../constants/pages"; import { openPasskeyVerificationURL, @@ -51,6 +53,10 @@ const Page: React.FC = ({ appContext }) => { const [passkeyVerificationData, setPasskeyVerificationData] = useState< { passkeySessionID: string; url: string } | undefined >(); + const { + secondFactorChoiceProps, + userVerificationResultAfterResolvingSecondFactorChoice, + } = useSecondFactorChoiceIfNeeded(); const router = useRouter(); @@ -83,7 +89,9 @@ const Page: React.FC = ({ appContext }) => { id, twoFactorSessionID, passkeySessionID, - } = resp.data as UserVerificationResponse; + } = await userVerificationResultAfterResolvingSecondFactorChoice( + resp.data as UserVerificationResponse, + ); if (passkeySessionID) { const user = getData(LS_KEYS.USER); await setLSUser({ @@ -243,6 +251,8 @@ const Page: React.FC = ({ appContext }) => { + + ); };