0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(experience): implement webauthn experience flow (#4642)

This commit is contained in:
Xiao Yijun 2023-10-17 15:15:44 +08:00 committed by GitHub
parent 15ab4d587e
commit bc62370db5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 422 additions and 321 deletions

View file

@ -40,6 +40,8 @@
"@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0",
"@silverhand/ts-config-react": "4.0.0",
"@simplewebauthn/browser": "^8.3.1",
"@simplewebauthn/typescript-types": "^8.0.0",
"@swc/core": "^1.3.52",
"@swc/jest": "^0.2.26",
"@testing-library/react": "^14.0.0",

View file

@ -15,8 +15,10 @@ import ErrorPage from './pages/ErrorPage';
import ForgotPassword from './pages/ForgotPassword';
import MfaBinding from './pages/MfaBinding';
import TotpBinding from './pages/MfaBinding/TotpBinding';
import WebAuthnBinding from './pages/MfaBinding/WebAuthnBinding';
import MfaVerification from './pages/MfaVerification';
import TotpVerification from './pages/MfaVerification/TotpVerification';
import WebAuthnVerification from './pages/MfaVerification/WebAuthnVerification';
import Register from './pages/Register';
import RegisterPassword from './pages/RegisterPassword';
import ResetPassword from './pages/ResetPassword';
@ -80,6 +82,7 @@ const App = () => {
<Route path={UserMfaFlow.MfaBinding}>
<Route index element={<MfaBinding />} />
<Route path={MfaFactor.TOTP} element={<TotpBinding />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnBinding />} />
</Route>
{/* Mfa verification */}
@ -87,6 +90,7 @@ const App = () => {
<Route path={UserMfaFlow.MfaVerification}>
<Route index element={<MfaVerification />} />
<Route path={MfaFactor.TOTP} element={<TotpVerification />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnVerification />} />
</Route>
</>
)}

View file

@ -1,15 +1,17 @@
/* istanbul ignore file */
import { InteractionEvent } from '@logto/schemas';
import type {
SignInIdentifier,
EmailVerificationCodePayload,
PhoneVerificationCodePayload,
SocialConnectorPayload,
SocialEmailPayload,
SocialPhonePayload,
BindMfaPayload,
VerifyMfaPayload,
import {
InteractionEvent,
type SignInIdentifier,
type EmailVerificationCodePayload,
type PhoneVerificationCodePayload,
type SocialConnectorPayload,
type SocialEmailPayload,
type SocialPhonePayload,
type BindMfaPayload,
type VerifyMfaPayload,
type WebAuthnRegistrationOptions,
type WebAuthnAuthenticationOptions,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
@ -230,6 +232,16 @@ export const createTotpSecret = async () =>
.post(`${interactionPrefix}/${verificationPath}/totp`)
.json<{ secret: string; secretQrCode: string }>();
export const createWebAuthnRegistrationOptions = async () =>
api
.post(`${interactionPrefix}/${verificationPath}/webauthn-registration`)
.json<WebAuthnRegistrationOptions>();
export const generateWebAuthnAuthnOptions = async () =>
api
.post(`${interactionPrefix}/${verificationPath}/webauthn-authentication`)
.json<WebAuthnAuthenticationOptions>();
export const bindMfa = async (payload: BindMfaPayload) => {
await api.put(`${interactionPrefix}/bind-mfa`, { json: payload });

View file

@ -19,18 +19,15 @@ const MfaFactorList = ({ flow, factors }: Props) => {
const navigate = useNavigate();
const handleSelectFactor = useCallback(
async (factor: MfaFactor) => {
if (factor === MfaFactor.TOTP) {
if (flow === UserMfaFlow.MfaBinding) {
await startTotpBinding(factors);
}
if (flow === UserMfaFlow.MfaVerification) {
const state: MfaFactorsState = { availableFactors: factors };
navigate(`/${UserMfaFlow.MfaVerification}/${factor}`, { state });
}
(factor: MfaFactor) => {
if (factor === MfaFactor.TOTP && flow === UserMfaFlow.MfaBinding) {
void startTotpBinding(factors);
return;
}
// Todo @xiaoyijun implement other factors
navigate(`/${flow}/${factor}`, {
state: { availableFactors: factors } satisfies MfaFactorsState,
});
},
[factors, flow, navigate, startTotpBinding]
);
@ -43,7 +40,7 @@ const MfaFactorList = ({ flow, factors }: Props) => {
factor={factor}
isBinding={flow === UserMfaFlow.MfaBinding}
onClick={() => {
void handleSelectFactor(factor);
handleSelectFactor(factor);
}}
/>
))}

View file

@ -8,13 +8,13 @@ import useTotpCodeVerification from './use-totp-code-verification';
const totpCodeLength = 6;
type Options = {
type Props = {
flow: UserMfaFlow;
};
const TotpCodeVerification = ({ flow }: Options) => {
const TotpCodeVerification = ({ flow }: Props) => {
const [code, setCode] = useState<string[]>([]);
const { errorMessage, onSubmit } = useTotpCodeVerification({ flow });
const { errorMessage, onSubmit } = useTotpCodeVerification(flow);
return (
<VerificationCodeInput
@ -26,7 +26,7 @@ const TotpCodeVerification = ({ flow }: Options) => {
setCode(code);
if (code.length === totpCodeLength && code.every(Boolean)) {
void onSubmit(code.join(''));
onSubmit(code.join(''));
}
}}
/>

View file

@ -1,62 +1,31 @@
import { MfaFactor } from '@logto/schemas';
import { useCallback, useMemo, useState } from 'react';
import { bindMfa, verifyMfa } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import useErrorHandler, { type ErrorHandlers } from '@/hooks/use-error-handler';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { UserMfaFlow } from '@/types';
import { type ErrorHandlers } from '@/hooks/use-error-handler';
import useSendMfaPayload from '@/hooks/use-send-mfa-payload';
import { type UserMfaFlow } from '@/types';
type Options = {
flow: UserMfaFlow;
};
const useTotpCodeVerification = ({ flow }: Options) => {
const useTotpCodeVerification = (flow: UserMfaFlow) => {
const [errorMessage, setErrorMessage] = useState<string>();
const asyncBindMfa = useApi(bindMfa);
const asyncVerifyMfa = useApi(verifyMfa);
const sendMfaPayload = useSendMfaPayload();
const preSignInErrorHandler = usePreSignInErrorHandler({ replace: true });
const handleError = useErrorHandler();
const errorHandlers: ErrorHandlers = useMemo(
const invalidCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'session.mfa.invalid_totp_code': (error) => {
setErrorMessage(error.message);
},
...preSignInErrorHandler,
}),
[preSignInErrorHandler]
[]
);
const onSubmit = useCallback(
async (code: string) => {
// Todo @xiaoyijun refactor this logic
if (flow === UserMfaFlow.MfaBinding) {
const [error, result] = await asyncBindMfa({ type: MfaFactor.TOTP, code });
if (error) {
await handleError(error, errorHandlers);
return;
}
if (result) {
window.location.replace(result.redirectTo);
}
return;
}
// Verify TOTP
const [error, result] = await asyncVerifyMfa({ type: MfaFactor.TOTP, code });
if (error) {
await handleError(error, errorHandlers);
return;
}
if (result) {
window.location.replace(result.redirectTo);
}
(code: string) => {
void sendMfaPayload(
{ flow, payload: { type: MfaFactor.TOTP, code } },
invalidCodeErrorHandlers
);
},
[asyncBindMfa, asyncVerifyMfa, errorHandlers, flow, handleError]
[flow, invalidCodeErrorHandlers, sendMfaPayload]
);
return {

View file

@ -0,0 +1,77 @@
import { MfaFactor, type RequestErrorBody } from '@logto/schemas';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { validate } from 'superstruct';
import { UserMfaFlow } from '@/types';
import { type MfaFactorsState, mfaErrorDataGuard } from '@/types/guard';
import type { ErrorHandlers } from './use-error-handler';
import useStartTotpBinding from './use-start-totp-binding';
import useToast from './use-toast';
export type Options = {
replace?: boolean;
};
const useMfaErrorHandler = ({ replace }: Options = {}) => {
const navigate = useNavigate();
const { setToast } = useToast();
const startTotpBinding = useStartTotpBinding({ replace });
const handleMfaRedirect = useCallback(
(flow: UserMfaFlow, availableFactors: MfaFactor[]) => {
const mfaFactorsState: MfaFactorsState = {
availableFactors,
};
if (availableFactors.length > 1) {
navigate({ pathname: `/${flow}` }, { replace, state: mfaFactorsState });
return;
}
const factor = availableFactors[0];
if (!factor) {
return;
}
if (factor === MfaFactor.TOTP && flow === UserMfaFlow.MfaBinding) {
void startTotpBinding(availableFactors);
return;
}
navigate({ pathname: `/${flow}/${factor}` }, { replace, state: mfaFactorsState });
},
[navigate, replace, startTotpBinding]
);
const handleMfaError = useCallback(
(flow: UserMfaFlow) => {
return (error: RequestErrorBody) => {
const [_, data] = validate(error.data, mfaErrorDataGuard);
const availableFactors = data?.availableFactors ?? [];
if (availableFactors.length === 0) {
setToast(error.message);
return;
}
handleMfaRedirect(flow, availableFactors);
};
},
[handleMfaRedirect, setToast]
);
const mfaVerificationErrorHandler = useMemo<ErrorHandlers>(
() => ({
'user.missing_mfa': handleMfaError(UserMfaFlow.MfaBinding),
'session.mfa.require_mfa_verification': handleMfaError(UserMfaFlow.MfaVerification),
}),
[handleMfaError]
);
return mfaVerificationErrorHandler;
};
export default useMfaErrorHandler;

View file

@ -0,0 +1,13 @@
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
import { mfaFactorsStateGuard } from '@/types/guard';
const useMfaFactorsState = () => {
const { state } = useLocation();
const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard);
return mfaFactorsState;
};
export default useMfaFactorsState;

View file

@ -1,83 +0,0 @@
import { MfaFactor } from '@logto/schemas';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { validate } from 'superstruct';
import { UserMfaFlow } from '@/types';
import {
type MfaFactorsState,
missingMfaFactorsErrorDataGuard,
requireMfaFactorsErrorDataGuard,
} from '@/types/guard';
import type { ErrorHandlers } from './use-error-handler';
import useStartTotpBinding from './use-start-totp-binding';
import useToast from './use-toast';
export type Options = {
replace?: boolean;
};
const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => {
const navigate = useNavigate();
const { setToast } = useToast();
const startTotpBinding = useStartTotpBinding({ replace });
const mfaVerificationErrorHandler = useMemo<ErrorHandlers>(
() => ({
'user.missing_mfa': (error) => {
const [_, data] = validate(error.data, missingMfaFactorsErrorDataGuard);
const availableFactors = data?.availableFactors ?? [];
if (availableFactors.length === 0) {
setToast(error.message);
return;
}
if (availableFactors.length > 1) {
const state: MfaFactorsState = { availableFactors };
navigate({ pathname: `/${UserMfaFlow.MfaBinding}` }, { replace, state });
return;
}
const factor = availableFactors[0];
if (factor === MfaFactor.TOTP) {
void startTotpBinding(availableFactors);
}
// Todo: @xiaoyijun handle other factors
},
'session.mfa.require_mfa_verification': async (error) => {
const [_, data] = validate(error.data, requireMfaFactorsErrorDataGuard);
const availableFactors = data?.availableFactors ?? [];
if (availableFactors.length === 0) {
setToast(error.message);
return;
}
if (availableFactors.length > 1) {
const state: MfaFactorsState = { availableFactors };
navigate({ pathname: `/${UserMfaFlow.MfaVerification}` }, { replace, state });
return;
}
const factor = availableFactors[0];
if (!factor) {
setToast(error.message);
return;
}
if (factor === MfaFactor.TOTP) {
const state: MfaFactorsState = { availableFactors };
navigate({ pathname: `/${UserMfaFlow.MfaVerification}/${factor}` }, { replace, state });
}
// Todo: @xiaoyijun handle other factors
},
}),
[navigate, replace, setToast, startTotpBinding]
);
return mfaVerificationErrorHandler;
};
export default useMfaVerificationErrorHandler;

View file

@ -4,9 +4,9 @@ import { useMemo } from 'react';
import { isDevelopmentFeaturesEnabled } from '@/constants/env';
import { type ErrorHandlers } from './use-error-handler';
import useMfaVerificationErrorHandler, {
import useMfaErrorHandler, {
type Options as UseMfaVerificationErrorHandlerOptions,
} from './use-mfa-verification-error-handler';
} from './use-mfa-error-handler';
import useRequiredProfileErrorHandler, {
type Options as UseRequiredProfileErrorHandlerOptions,
} from './use-required-profile-error-handler';
@ -15,14 +15,14 @@ type Options = UseRequiredProfileErrorHandlerOptions & UseMfaVerificationErrorHa
const usePreSignInErrorHandler = ({ replace, linkSocial }: Options = {}): ErrorHandlers => {
const requiredProfileErrorHandler = useRequiredProfileErrorHandler({ replace, linkSocial });
const mfaVerificationErrorHandler = useMfaVerificationErrorHandler({ replace });
const mfaErrorHandler = useMfaErrorHandler({ replace });
return useMemo(
() => ({
...requiredProfileErrorHandler,
...conditional(isDevelopmentFeaturesEnabled && mfaVerificationErrorHandler),
...conditional(isDevelopmentFeaturesEnabled && mfaErrorHandler),
}),
[mfaVerificationErrorHandler, requiredProfileErrorHandler]
[mfaErrorHandler, requiredProfileErrorHandler]
);
};

View file

@ -0,0 +1,53 @@
import { type BindMfaPayload, type VerifyMfaPayload } from '@logto/schemas';
import { useCallback } from 'react';
import { bindMfa, verifyMfa } from '@/apis/interaction';
import { UserMfaFlow } from '@/types';
import useApi from './use-api';
import useErrorHandler, { type ErrorHandlers } from './use-error-handler';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
export type SendMfaPayloadApiOptions =
| {
flow: UserMfaFlow.MfaBinding;
payload: BindMfaPayload;
}
| {
flow: UserMfaFlow.MfaVerification;
payload: VerifyMfaPayload;
};
const sendMfaPayloadApi = async ({ flow, payload }: SendMfaPayloadApiOptions) => {
if (flow === UserMfaFlow.MfaBinding) {
return bindMfa(payload);
}
return verifyMfa(payload);
};
const useSendMfaPayload = () => {
const asyncSendMfaPayload = useApi(sendMfaPayloadApi);
const preSignInErrorHandler = usePreSignInErrorHandler({ replace: true });
const handleError = useErrorHandler();
return useCallback(
async (apiOptions: SendMfaPayloadApiOptions, errorHandlers?: ErrorHandlers) => {
const [error, result] = await asyncSendMfaPayload(apiOptions);
if (error) {
await handleError(error, {
...errorHandlers,
...preSignInErrorHandler,
});
return;
}
if (result) {
window.location.replace(result.redirectTo);
}
},
[asyncSendMfaPayload, handleError, preSignInErrorHandler]
);
};
export default useSendMfaPayload;

View file

@ -0,0 +1,109 @@
import {
MfaFactor,
webAuthnRegistrationOptionsGuard,
type WebAuthnAuthenticationOptions,
type WebAuthnRegistrationOptions,
} from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
import type {
RegistrationResponseJSON,
AuthenticationResponseJSON,
} from '@simplewebauthn/typescript-types';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
createWebAuthnRegistrationOptions,
generateWebAuthnAuthnOptions,
} from '@/apis/interaction';
import { UserMfaFlow } from '@/types';
import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import useSendMfaPayload from './use-send-mfa-payload';
import useToast from './use-toast';
const isAuthenticationResponseJSON = (
responseJson: RegistrationResponseJSON | AuthenticationResponseJSON
): responseJson is AuthenticationResponseJSON => {
return 'signature' in responseJson.response;
};
const useWebAuthnOperation = (flow: UserMfaFlow) => {
const { t } = useTranslation();
const { setToast } = useToast();
const asyncCreateRegistrationOptions = useApi(createWebAuthnRegistrationOptions);
const asyncGenerateAuthnOptions = useApi(generateWebAuthnAuthnOptions);
const sendMfaPayload = useSendMfaPayload();
const handleError = useErrorHandler();
const handleRawWebAuthnError = useCallback(
(error: unknown) => {
if (error instanceof Error) {
setToast(error.message);
return;
}
setToast(t('error.unknown'));
},
[setToast, t]
);
const handleWebAuthnProcess = useCallback(
async (options: WebAuthnRegistrationOptions | WebAuthnAuthenticationOptions) => {
const parsedOptions = webAuthnRegistrationOptionsGuard.safeParse(options);
return trySafe(
async () =>
parsedOptions.success
? startRegistration(parsedOptions.data)
: startAuthentication(options),
handleRawWebAuthnError
);
},
[handleRawWebAuthnError]
);
return useCallback(async () => {
const [error, options] =
flow === UserMfaFlow.MfaBinding
? await asyncCreateRegistrationOptions()
: await asyncGenerateAuthnOptions();
if (error) {
await handleError(error);
return;
}
if (!options) {
return;
}
const response = await handleWebAuthnProcess(options);
if (!response) {
return;
}
/**
* Assert type manually to get the correct type
*/
void sendMfaPayload(
isAuthenticationResponseJSON(response)
? { flow: UserMfaFlow.MfaVerification, payload: { ...response, type: MfaFactor.WebAuthn } }
: { flow: UserMfaFlow.MfaBinding, payload: { ...response, type: MfaFactor.WebAuthn } }
);
}, [
asyncCreateRegistrationOptions,
asyncGenerateAuthnOptions,
flow,
handleError,
handleWebAuthnProcess,
sendMfaPayload,
]);
};
export default useWebAuthnOperation;

View file

@ -0,0 +1,5 @@
@use '@/scss/underscore' as _;
.switchLink {
margin-top: _.unit(6);
}

View file

@ -0,0 +1,35 @@
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import Button from '@/components/Button';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useMfaFactorsState from '@/hooks/use-mfa-factors-state';
import useWebAuthnOperation from '@/hooks/use-webauthn-operation';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';
import * as styles from './index.module.scss';
const WebAuthnBinding = () => {
const mfaFactorsState = useMfaFactorsState();
const bindWebAuthn = useWebAuthnOperation(UserMfaFlow.MfaBinding);
if (!mfaFactorsState) {
return <ErrorPage title="error.invalid_session" />;
}
const { availableFactors } = mfaFactorsState;
return (
<SecondaryPageLayout title="mfa.create_a_passkey" description="mfa.create_passkey_description">
<Button title="mfa.create_a_passkey" onClick={bindWebAuthn} />
{availableFactors.length > 1 && (
<SwitchMfaFactorsLink
flow={UserMfaFlow.MfaBinding}
factors={availableFactors}
className={styles.switchLink}
/>
)}
</SecondaryPageLayout>
);
};
export default WebAuthnBinding;

View file

@ -1,17 +1,12 @@
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import MfaFactorList from '@/containers/MfaFactorList';
import useMfaFactorsState from '@/hooks/use-mfa-factors-state';
import { UserMfaFlow } from '@/types';
import { mfaFactorsStateGuard } from '@/types/guard';
import ErrorPage from '../ErrorPage';
const MfaBinding = () => {
const { state } = useLocation();
const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard);
const { availableFactors } = mfaFactorsState ?? {};
const { availableFactors } = useMfaFactorsState() ?? {};
if (!availableFactors || availableFactors.length === 0) {
return <ErrorPage title="error.invalid_session" />;

View file

@ -1,19 +1,15 @@
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import SectionLayout from '@/Layout/SectionLayout';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import TotpCodeVerification from '@/containers/TotpCodeVerification';
import useMfaFactorsState from '@/hooks/use-mfa-factors-state';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';
import { mfaFactorsStateGuard } from '@/types/guard';
import * as styles from './index.module.scss';
const TotpVerification = () => {
const { state } = useLocation();
const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard);
const mfaFactorsState = useMfaFactorsState();
if (!mfaFactorsState) {
return <ErrorPage title="error.invalid_session" />;

View file

@ -0,0 +1,5 @@
@use '@/scss/underscore' as _;
.verifyButton {
margin: _.unit(4) 0;
}

View file

@ -0,0 +1,41 @@
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import SectionLayout from '@/Layout/SectionLayout';
import Button from '@/components/Button';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useMfaFactorsState from '@/hooks/use-mfa-factors-state';
import useWebAuthnOperation from '@/hooks/use-webauthn-operation';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';
import * as styles from './index.module.scss';
const WebAuthnVerification = () => {
const mfaFactorsState = useMfaFactorsState();
const verifyWebAuthn = useWebAuthnOperation(UserMfaFlow.MfaVerification);
if (!mfaFactorsState) {
return <ErrorPage title="error.invalid_session" />;
}
const { availableFactors } = mfaFactorsState;
return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.verify_via_passkey"
description="mfa.verify_via_passkey_description"
>
<Button
title="action.verify_via_passkey"
className={styles.verifyButton}
onClick={verifyWebAuthn}
/>
</SectionLayout>
{availableFactors.length > 1 && (
<SwitchMfaFactorsLink flow={UserMfaFlow.MfaVerification} factors={availableFactors} />
)}
</SecondaryPageLayout>
);
};
export default WebAuthnVerification;

View file

@ -1,17 +1,12 @@
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import MfaFactorList from '@/containers/MfaFactorList';
import useMfaFactorsState from '@/hooks/use-mfa-factors-state';
import { UserMfaFlow } from '@/types';
import { mfaFactorsStateGuard } from '@/types/guard';
import ErrorPage from '../ErrorPage';
const MfaVerification = () => {
const { state } = useLocation();
const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard);
const { availableFactors } = mfaFactorsState ?? {};
const { availableFactors } = useMfaFactorsState() ?? {};
if (!availableFactors || availableFactors.length === 0) {
return <ErrorPage title="error.invalid_session" />;

View file

@ -7,7 +7,7 @@ import { setUserPassword } from '@/apis/interaction';
import SetPassword from '@/containers/SetPassword';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import { type ErrorHandlers } from '@/hooks/use-error-handler';
import useMfaVerificationErrorHandler from '@/hooks/use-mfa-verification-error-handler';
import useMfaErrorHandler from '@/hooks/use-mfa-error-handler';
import usePasswordAction, { type SuccessHandler } from '@/hooks/use-password-action';
import { usePasswordPolicy, useSieMethods } from '@/hooks/use-sie';
@ -23,7 +23,7 @@ const RegisterPassword = () => {
setErrorMessage(undefined);
}, []);
const mfaVerificationErrorHandler = useMfaVerificationErrorHandler({ replace: true });
const mfaErrorHandler = useMfaErrorHandler({ replace: true });
const errorHandlers: ErrorHandlers = useMemo(
() => ({
@ -32,9 +32,9 @@ const RegisterPassword = () => {
await show({ type: 'alert', ModalContent: error.message, cancelText: 'action.got_it' });
navigate(-1);
},
...mfaVerificationErrorHandler,
...mfaErrorHandler,
}),
[navigate, mfaVerificationErrorHandler, show]
[navigate, mfaErrorHandler, show]
);
const successHandler: SuccessHandler<typeof setUserPassword> = useCallback((result) => {

View file

@ -71,17 +71,11 @@ const mfaFactorsGuard = s.array(
])
);
export const missingMfaFactorsErrorDataGuard = s.object({
export const mfaErrorDataGuard = s.object({
availableFactors: mfaFactorsGuard,
});
export const requireMfaFactorsErrorDataGuard = s.object({
availableFactors: mfaFactorsGuard,
});
export const mfaFactorsStateGuard = s.object({
availableFactors: mfaFactorsGuard,
});
export const mfaFactorsStateGuard = mfaErrorDataGuard;
export type MfaFactorsState = s.Infer<typeof mfaFactorsStateGuard>;

View file

@ -3364,7 +3364,7 @@ importers:
version: 8.44.0
jest:
specifier: ^29.5.0
version: 29.5.0(@types/node@18.11.18)
version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1)
jest-matcher-specific-error:
specifier: ^1.0.0
version: 1.0.0
@ -3551,6 +3551,12 @@ importers:
'@silverhand/ts-config-react':
specifier: 4.0.0
version: 4.0.0(typescript@5.0.2)
'@simplewebauthn/browser':
specifier: ^8.3.1
version: 8.3.1
'@simplewebauthn/typescript-types':
specifier: ^8.0.0
version: 8.0.0
'@swc/core':
specifier: ^1.3.52
version: 1.3.52
@ -6957,48 +6963,6 @@ packages:
slash: 3.0.0
dev: true
/@jest/core@29.5.0:
resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/console': 29.5.0
'@jest/reporters': 29.5.0
'@jest/test-result': 29.5.0
'@jest/transform': 29.5.0
'@jest/types': 29.5.0
'@types/node': 18.11.18
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.5.0
jest-config: 29.5.0(@types/node@18.11.18)
jest-haste-map: 29.5.0
jest-message-util: 29.5.0
jest-regex-util: 29.4.3
jest-resolve: 29.5.0
jest-resolve-dependencies: 29.5.0
jest-runner: 29.5.0
jest-runtime: 29.5.0
jest-snapshot: 29.5.0
jest-util: 29.5.0
jest-validate: 29.5.0
jest-watcher: 29.5.0
micromatch: 4.0.5
pretty-format: 29.5.0
slash: 3.0.0
strip-ansi: 6.0.1
transitivePeerDependencies:
- supports-color
- ts-node
dev: true
/@jest/core@29.5.0(ts-node@10.9.1):
resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9177,6 +9141,12 @@ packages:
typescript: 5.0.2
dev: true
/@simplewebauthn/browser@8.3.1:
resolution: {integrity: sha512-bMW7oOkxX4ydRAkkPtJ1do2k9yOoIGc/hZYebcuEOVdJoC6wwVpu97mYY7Mz8B9hLlcaR5WFgBsLl5tSJVzm8A==}
dependencies:
'@simplewebauthn/typescript-types': 8.0.0
dev: true
/@simplewebauthn/server@8.2.0:
resolution: {integrity: sha512-nknf7kCa5V61Kk2zn1vTuKeAlyut9aWduIcbHNQWpMCEJqH/m8cXpb+9UV42MEQRIk8JVC1GSNeEx56QVTfJHw==}
engines: {node: '>=16.0.0'}
@ -9196,7 +9166,6 @@ packages:
/@simplewebauthn/typescript-types@8.0.0:
resolution: {integrity: sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ==}
dev: false
/@sinclair/typebox@0.24.46:
resolution: {integrity: sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==}
@ -14488,34 +14457,6 @@ packages:
- supports-color
dev: true
/jest-cli@29.5.0(@types/node@18.11.18):
resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 29.5.0
'@jest/test-result': 29.5.0
'@jest/types': 29.5.0
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
import-local: 3.1.0
jest-config: 29.5.0(@types/node@18.11.18)
jest-util: 29.5.0
jest-validate: 29.5.0
prompts: 2.4.2
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest-cli@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -14544,45 +14485,6 @@ packages:
- ts-node
dev: true
/jest-config@29.5.0(@types/node@18.11.18):
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@types/node': '*'
ts-node: '>=9.0.0'
peerDependenciesMeta:
'@types/node':
optional: true
ts-node:
optional: true
dependencies:
'@babel/core': 7.20.2
'@jest/test-sequencer': 29.5.0
'@jest/types': 29.5.0
'@types/node': 18.11.18
babel-jest: 29.5.0(@babel/core@7.20.2)
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.11
jest-circus: 29.5.0
jest-environment-node: 29.5.0
jest-get-type: 29.4.3
jest-regex-util: 29.4.3
jest-resolve: 29.5.0
jest-runner: 29.5.0
jest-util: 29.5.0
jest-validate: 29.5.0
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 29.5.0
slash: 3.0.0
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
dev: true
/jest-config@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -14994,26 +14896,6 @@ packages:
supports-color: 8.1.1
dev: true
/jest@29.5.0(@types/node@18.11.18):
resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 29.5.0
'@jest/types': 29.5.0
import-local: 3.1.0
jest-cli: 29.5.0(@types/node@18.11.18)
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}