Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const UserInteractionContextProvider = ({ children }: Props) => {
remove(StorageKeys.IdentifierInputValue);
remove(StorageKeys.ForgotPasswordIdentifierInputValue);
remove(StorageKeys.verificationIds);
remove(StorageKeys.OneTimeTokenSignIn);
Comment thread
wangsijie marked this conversation as resolved.
Outdated
}, [remove]);

const setVerificationId = useCallback((type: VerificationType, id: string) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/experience/src/hooks/use-mfa-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { validate } from 'superstruct';

import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
import { UserMfaFlow } from '@/types';
import { type MfaFlowState, mfaErrorDataGuard } from '@/types/guard';
import { isNativeWebview } from '@/utils/native-sdk';
Expand All @@ -22,6 +23,7 @@ export type Options = {

const useMfaErrorHandler = ({ replace }: Options = {}) => {
const navigate = useNavigateWithPreservedSearchParams();
const { get } = useSessionStorage();
const { t } = useTranslation();
const { setToast } = useToast();
const startTotpBinding = useStartTotpBinding();
Expand Down Expand Up @@ -136,16 +138,19 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
? factors.filter((factor) => factor !== MfaFactor.WebAuthn)
: factors;

const hideBack = Boolean(get(StorageKeys.OneTimeTokenSignIn));
Comment thread
wangsijie marked this conversation as resolved.
Outdated

await handleMfaRedirect(flow, {
availableFactors,
skippable,
maskedIdentifiers,
suggestion,
isWebAuthnUsedAsSignInPasskey,
hideBack,
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
});
};
},
[handleMfaRedirect, navigate, replace, setToast]
[get, handleMfaRedirect, navigate, replace, setToast]
);

const mfaVerificationErrorHandler = useMemo<ErrorHandlers>(
Expand Down
2 changes: 2 additions & 0 deletions packages/experience/src/hooks/use-session-storages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum StorageKeys {
IdentifierInputValue = 'identifier-input-value',
ForgotPasswordIdentifierInputValue = 'forgot-password-identifier-input-value',
verificationIds = 'verification-ids',
OneTimeTokenSignIn = 'one-time-token-sign-in',
}

const valueGuard = Object.freeze({
Expand All @@ -26,6 +27,7 @@ const valueGuard = Object.freeze({
[StorageKeys.IdentifierInputValue]: identifierInputValueGuard,
[StorageKeys.ForgotPasswordIdentifierInputValue]: identifierInputValueGuard,
[StorageKeys.verificationIds]: verificationIdsMapGuard,
[StorageKeys.OneTimeTokenSignIn]: s.literal(true),
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about the superstruct details
} satisfies { [key in StorageKeys]: s.Struct<any> });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';

/**
* Hide the MFA setup back button when the user signed in via a one-time token.
* Going back would re-run token verification and fail with `one_time_token.token_consumed`.
*/
const useShouldHideMfaBackNavigation = () => {
const flowState = useMfaFlowState();
const { get } = useSessionStorage();

return Boolean(flowState?.hideBack || get(StorageKeys.OneTimeTokenSignIn));
Comment thread
wangsijie marked this conversation as resolved.
Outdated
};
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

export default useShouldHideMfaBackNavigation;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import Divider from '@/components/Divider';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import ErrorPage from '@/pages/ErrorPage';
Expand All @@ -26,6 +27,7 @@ const TotpBinding = () => {

const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();

if (!totpBindingState || !verificationId) {
return <ErrorPage title="error.invalid_session" />;
Expand All @@ -35,6 +37,7 @@ const TotpBinding = () => {

return (
<SecondaryPageLayout
isNavBarHidden={shouldHideBack}
title="mfa.add_authenticator_app"
onSkip={conditional(skippable && (suggestion ? skipOptionalMfa : skipMfa))}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { sendVerificationCode } from '@/apis/experience';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useErrorHandler from '@/hooks/use-error-handler';
import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import IdentifierProfileForm from '@/pages/Continue/IdentifierProfileForm';
Expand Down Expand Up @@ -42,6 +43,7 @@ const VerificationCodeMfaBinding = ({

const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();
const handleError = useErrorHandler();

const clearErrorMessage = useCallback(() => {
Expand Down Expand Up @@ -91,6 +93,7 @@ const VerificationCodeMfaBinding = ({

return (
<SecondaryPageLayout
isNavBarHidden={shouldHideBack}
title={titleKey}
description={descriptionKey}
onSkip={conditional(skippable && (suggestion ? skipOptionalMfa : skipMfa))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { validate } from 'superstruct';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useWebAuthnOperation from '@/hooks/use-webauthn-operation';
import ErrorPage from '@/pages/ErrorPage';
Expand All @@ -25,6 +26,7 @@ const WebAuthnBinding = () => {

const handleWebAuthn = useWebAuthnOperation();
const skipMfa = useSkipMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();
const [isCreatingPasskey, setIsCreatingPasskey] = useState(false);

if (!webAuthnState || !verificationId) {
Expand All @@ -39,6 +41,7 @@ const WebAuthnBinding = () => {

return (
<SecondaryPageLayout
isNavBarHidden={shouldHideBack}
title="mfa.create_a_passkey"
description="mfa.create_passkey_description"
onSkip={conditional(skippable && skipMfa)}
Expand Down
3 changes: 3 additions & 0 deletions packages/experience/src/pages/MfaBinding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { conditional } from '@silverhand/essentials';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import MfaFactorList from '@/containers/MfaFactorList';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import { UserMfaFlow } from '@/types';
Expand All @@ -11,6 +12,7 @@ import ErrorPage from '../ErrorPage';

const MfaBinding = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();
Comment thread
wangsijie marked this conversation as resolved.
Outdated
const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();

Expand All @@ -20,6 +22,7 @@ const MfaBinding = () => {

return (
<SecondaryPageLayout
isNavBarHidden={shouldHideBack}
title={flowState.suggestion ? 'mfa.add_another_mfa_factor' : 'mfa.add_mfa_factors'}
description={
flowState.suggestion ? 'mfa.add_another_mfa_description' : 'mfa.add_mfa_description'
Expand Down
3 changes: 3 additions & 0 deletions packages/experience/src/pages/MfaOnboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import useEnableMfa from '@/hooks/use-enable-mfa';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import Button from '@/shared/components/Button';

const MfaOnboarding = () => {
const skipMfa = useSkipMfa();
const enableMfa = useEnableMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();

return (
<SecondaryPageLayout
isNavBarHidden={shouldHideBack}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
title="mfa.onboarding"
description="mfa.onboarding_description"
onSkip={skipMfa}
Expand Down
5 changes: 5 additions & 0 deletions packages/experience/src/pages/OneTimeToken/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
signInWithOneTimeToken,
} from '@/apis/experience';
import useApi from '@/hooks/use-api';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
Comment thread
wangsijie marked this conversation as resolved.
Outdated
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
Expand All @@ -30,6 +31,7 @@ const OneTimeToken = () => {
const hasTermsAgreed = useRef(false);
const isSubmitted = useRef(false);

const { set: setSessionStorage } = useSessionStorage();
const asyncIdentifyUserAndSubmit = useApi(identifyAndSubmitInteraction);
const asyncSignInWithOneTimeToken = useApi(signInWithOneTimeToken);
const asyncRegisterWithVerifiedIdentifier = useApi(registerWithVerifiedIdentifier);
Expand Down Expand Up @@ -184,6 +186,8 @@ const OneTimeToken = () => {
return;
}

setSessionStorage(StorageKeys.OneTimeTokenSignIn, true);
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated

// Set email identifier to the <HiddenIdentifierInput />, so that when being asked for fulfilling
// the password later, the browser password manager can pick up both the email and the password.
setIdentifierInputValue({ type: SignInIdentifier.Email, value: email });
Expand All @@ -198,6 +202,7 @@ const OneTimeToken = () => {
handleError,
navigate,
setIdentifierInputValue,
setSessionStorage,
submit,
termsValidation,
]);
Expand Down
12 changes: 12 additions & 0 deletions packages/experience/src/types/guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,16 @@ describe('guard', () => {
);
}).not.toThrow();
});

it('mfaErrorDataGuard should accept hideBack for one-time token sign-in', () => {
expect(() => {
s.assert(
{
availableFactors: [MfaFactor.TOTP],
hideBack: true,
},
mfaErrorDataGuard
);
}).not.toThrow();
});
});
2 changes: 2 additions & 0 deletions packages/experience/src/types/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export const mfaErrorDataGuard = s.object({
suggestion: s.optional(s.boolean()),
// Whether the current WebAuthn factor is used as a sign-in passkey.
isWebAuthnUsedAsSignInPasskey: s.optional(s.boolean()),
// Hide back navigation (e.g. one-time token sign-in where back re-consumes the token).
hideBack: s.optional(s.boolean()),
});

export const mfaFlowStateGuard = mfaErrorDataGuard;
Expand Down
Loading