diff --git a/assets/images/multifactorAuthentication/open-padlock-green.svg b/assets/images/multifactorAuthentication/open-padlock-green.svg
new file mode 100644
index 0000000000000..d8ba3ea37bbcd
--- /dev/null
+++ b/assets/images/multifactorAuthentication/open-padlock-green.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__encryption-passkeys.svg b/assets/images/simple-illustrations/simple-illustration__encryption-passkeys.svg
new file mode 100644
index 0000000000000..bd3094024ddcd
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__encryption-passkeys.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/cspell.json b/cspell.json
index 1fe605e9ec94f..5512d8a13f1e3 100644
--- a/cspell.json
+++ b/cspell.json
@@ -205,6 +205,7 @@
"ecash",
"ecconnrefused",
"econn",
+ "EDDSA",
"EDIFACT",
"Egencia",
"Electromedical",
diff --git a/jest/setup.ts b/jest/setup.ts
index e30e2b55d06cd..5320837d0cdf8 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -12,7 +12,7 @@ import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
import type Animated from 'react-native-reanimated';
import 'setimmediate';
import {TextDecoder, TextEncoder} from 'util';
-import * as MockedSecureStore from '@src/libs/MultifactorAuthentication/Biometrics/SecureStore/index.web';
+import * as MockedSecureStore from '@src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.web';
import '@src/polyfills/PromiseWithResolvers';
import mockFSLibrary from './setupMockFullstoryLib';
import setupMockImages from './setupMockImages';
@@ -126,7 +126,7 @@ jest.mock('react-native-share', () => ({
}));
// Jest has no access to the native secure store module, so we mock it with the web implementation.
-jest.mock('@src/libs/MultifactorAuthentication/Biometrics/SecureStore', () => MockedSecureStore);
+jest.mock('@src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore', () => MockedSecureStore);
jest.mock('react-native-reanimated', () => ({
...jest.requireActual('react-native-reanimated/mock'),
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 7aab7c8dcbae2..0ac1cbe58a7c5 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -7,7 +7,7 @@ import type {ValueOf} from 'type-fest';
import type {SearchFilterKey} from '@components/Search/types';
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
import type {MileageRate} from '@libs/DistanceRequestUtils';
-import MULTIFACTOR_AUTHENTICATION_VALUES from '@libs/MultifactorAuthentication/Biometrics/VALUES';
+import MULTIFACTOR_AUTHENTICATION_VALUES from '@libs/MultifactorAuthentication/VALUES';
import addTrailingForwardSlash from '@libs/UrlUtils';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -439,6 +439,19 @@ const CONST = {
MULTIFACTOR_AUTHENTICATION: MULTIFACTOR_AUTHENTICATION_VALUES,
+ /**
+ * COSE algorithm identifiers used in WebAuthn credential registration.
+ * @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms
+ */
+ COSE_ALGORITHM: {
+ /** EdDSA (ED25519) */
+ EDDSA: -8,
+ /** ES256 (ECDSA w/ SHA-256, P-256 curve) */
+ ES256: -7,
+ /** RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256) */
+ RS256: -257,
+ },
+
/** WebAuthn/Passkey credential type */
PASSKEY_CREDENTIAL_TYPE: 'public-key',
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index cdb18843e9a45..a1db182e00ea2 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -28,6 +28,7 @@ import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustrat
import CreditCardsNewGreen from '@assets/images/simple-illustrations/simple-illustration__creditcards--green.svg';
import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg';
import EmptyShelves from '@assets/images/simple-illustrations/simple-illustration__empty-shelves.svg';
+import EncryptionPasskeys from '@assets/images/simple-illustrations/simple-illustration__encryption-passkeys.svg';
import Encryption from '@assets/images/simple-illustrations/simple-illustration__encryption.svg';
import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg';
import FastMoney from '@assets/images/simple-illustrations/simple-illustration__fastmoney.svg';
@@ -74,6 +75,7 @@ export {
ExpensifyCardCoins,
EmptyStateTravel,
Encryption,
+ EncryptionPasskeys,
EnvelopeReceipt,
ExpensifyApprovedLogo,
ExpensifyCardImage,
diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts
index ee192f47177d3..bc0a8e0a784fd 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -50,6 +50,7 @@ import DeniedTransactionHand from '@assets/images/multifactorAuthentication/deni
import EncryptionMan from '@assets/images/multifactorAuthentication/encryption-man.svg';
import HumptyDumpty from '@assets/images/multifactorAuthentication/humpty-dumpty.svg';
import MagnifyingGlassSpyMouthClosed from '@assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed-cropped.svg';
+import OpenPadlockGreen from '@assets/images/multifactorAuthentication/open-padlock-green.svg';
import OpenPadlock from '@assets/images/multifactorAuthentication/open-padlock.svg';
import RunOutOfTime from '@assets/images/multifactorAuthentication/running-out-of-time.svg';
import PendingTravel from '@assets/images/pending-travel.svg';
@@ -393,6 +394,7 @@ const Illustrations = {
// Multifactor Authentication Illustrations
MagnifyingGlassSpyMouthClosed,
OpenPadlock,
+ OpenPadlockGreen,
ApprovedTransactionHand,
DeniedTransactionHand,
RunOutOfTime,
diff --git a/src/components/MultifactorAuthentication/Context/Main.tsx b/src/components/MultifactorAuthentication/Context/Main.tsx
index bad34d7c5ac7d..d8dda545c203b 100644
--- a/src/components/MultifactorAuthentication/Context/Main.tsx
+++ b/src/components/MultifactorAuthentication/Context/Main.tsx
@@ -2,13 +2,15 @@ import React, {createContext, useCallback, useContext, useEffect, useMemo} from
import type {ReactNode} from 'react';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
+import type {AuthorizeResult, RegisterResult} from '@components/MultifactorAuthentication/biometrics/shared/types';
+import useBiometrics from '@components/MultifactorAuthentication/biometrics/useBiometrics';
import type {MultifactorAuthenticationScenario, MultifactorAuthenticationScenarioParams} from '@components/MultifactorAuthentication/config/types';
import addMFABreadcrumb from '@components/MultifactorAuthentication/observability/breadcrumbs';
import trackMFAFlowOutcome from '@components/MultifactorAuthentication/observability/trackMFAFlowOutcome';
import useNetwork from '@hooks/useNetwork';
import {requestValidateCodeAction} from '@libs/actions/User';
import getPlatform from '@libs/getPlatform';
-import type {ChallengeType, MultifactorAuthenticationCallbackInput, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/Biometrics/types';
+import type {ChallengeType, MultifactorAuthenticationCallbackInput} from '@libs/MultifactorAuthentication/shared/types';
import Navigation from '@navigation/Navigation';
import {clearLocalMFAPublicKeyList, requestAuthorizationChallenge, requestRegistrationChallenge} from '@userActions/MultifactorAuthentication';
import {processRegistration, processScenarioAction} from '@userActions/MultifactorAuthentication/processing';
@@ -17,8 +19,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {DeviceBiometrics} from '@src/types/onyx';
import {useMultifactorAuthenticationActions, useMultifactorAuthenticationState} from './State';
-import useNativeBiometrics from './useNativeBiometrics';
-import type {AuthorizeResult, RegisterResult} from './useNativeBiometrics';
let deviceBiometricsState: OnyxEntry;
@@ -70,10 +70,10 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
const state = useMultifactorAuthenticationState();
const {dispatch} = useMultifactorAuthenticationActions();
- const biometrics = useNativeBiometrics();
+ const biometrics = useBiometrics();
const {isOffline} = useNetwork();
const platform = getPlatform();
- const isWeb = useMemo(() => platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.MOBILE_WEB, [platform]);
+ const promptType = CONST.MULTIFACTOR_AUTHENTICATION.PROMPT_TYPE_MAP[biometrics.deviceVerificationType];
/**
* Handles the completion of a multifactor authentication scenario.
@@ -184,25 +184,26 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
return;
}
- // 2. Check if device is compatible
- if (!biometrics.doesDeviceSupportBiometrics()) {
- const {allowedAuthenticationMethods = [] as string[]} = scenario;
-
- let reason: MultifactorAuthenticationReason = CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.UNSUPPORTED_DEVICE;
-
- // If the user is using mobile app and the scenario allows native biometrics as a form of authentication,
- // then they need to enable it in the system settings as well for doesDeviceSupportBiometrics to return true.
- if (!isWeb && allowedAuthenticationMethods.includes(CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS)) {
- reason = CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.NO_ELIGIBLE_METHODS;
- }
+ // 2a. Check if device supports the authentication method
+ if (!biometrics.doesDeviceSupportAuthenticationMethod()) {
+ const reason = CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.UNSUPPORTED_DEVICE;
+ const message = `Device does not support biometric authentication (deviceVerificationType: ${biometrics.deviceVerificationType})`;
+ addMFABreadcrumb('Device check failed', {reason, deviceVerificationType: biometrics.deviceVerificationType, message}, 'warning');
+ dispatch({type: 'SET_ERROR', payload: {reason, message}});
+ return;
+ }
- addMFABreadcrumb('Device check failed', {reason}, 'warning');
- dispatch({
- type: 'SET_ERROR',
- payload: {
- reason,
- },
- });
+ // 2b. Check if the scenario allows the current authentication method
+ const {allowedAuthenticationMethods = [] as string[]} = scenario;
+ if (!allowedAuthenticationMethods.includes(biometrics.deviceVerificationType)) {
+ const reason = CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.UNSUPPORTED_DEVICE;
+ const message = `Authentication method not allowed (deviceVerificationType: ${biometrics.deviceVerificationType}, allowedMethods: ${allowedAuthenticationMethods.join(', ')})`;
+ addMFABreadcrumb(
+ 'Authentication method not allowed',
+ {reason, deviceVerificationType: biometrics.deviceVerificationType, allowedAuthenticationMethods: allowedAuthenticationMethods.join(', '), message},
+ 'warning',
+ );
+ dispatch({type: 'SET_ERROR', payload: {reason, message}});
return;
}
@@ -252,7 +253,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// Check if a soft prompt is needed
if (!softPromptApproved) {
addMFABreadcrumb('Soft prompt shown', {context: 'registration'});
- Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(CONST.MULTIFACTOR_AUTHENTICATION.PROMPT.BIOMETRICS), {forceReplace: true});
+ Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(promptType), {forceReplace: true});
return;
}
@@ -277,11 +278,9 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
return;
}
- // Call backend to register the public key
const registrationResponse = await processRegistration({
- publicKey: result.publicKey,
+ keyInfo: result.keyInfo,
authenticationMethod: result.authenticationMethod.marqetaValue,
- challenge: registrationChallenge.challenge,
});
addMFABreadcrumb(
@@ -308,7 +307,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
}
dispatch({type: 'SET_REGISTRATION_COMPLETE', payload: true});
- });
+ }, registrationChallenge);
return;
}
@@ -317,14 +316,14 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// they'll be shown the "do you want to enable FaceID again" system prompt, so we want to show them the soft prompt
if (!deviceBiometricsState?.hasAcceptedSoftPrompt) {
addMFABreadcrumb('Soft prompt shown', {context: 'authorization-reinstall'});
- Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(CONST.MULTIFACTOR_AUTHENTICATION.PROMPT.BIOMETRICS), {forceReplace: true});
+ Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(promptType), {forceReplace: true});
return;
}
// 4. Authorize the user if that has not already been done
if (!isAuthorizationComplete) {
- if (!Navigation.isActiveRoute(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(CONST.MULTIFACTOR_AUTHENTICATION.PROMPT.BIOMETRICS))) {
- Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(CONST.MULTIFACTOR_AUTHENTICATION.PROMPT.BIOMETRICS), {forceReplace: true});
+ if (!Navigation.isActiveRoute(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(promptType))) {
+ Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(promptType), {forceReplace: true});
}
// Request authorization challenge if not already fetched
@@ -374,7 +373,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// - The server no longer accepts the local public key (not in allowCredentials)
if (result.reason === CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.REGISTRATION_REQUIRED) {
addMFABreadcrumb('Authorization key reset', {reason: result.reason}, 'warning');
- await biometrics.resetKeysForAccount();
+ await biometrics.deleteLocalKeysForAccount();
dispatch({type: 'SET_REGISTRATION_COMPLETE', payload: false});
dispatch({type: 'SET_AUTHORIZATION_CHALLENGE', payload: undefined});
} else {
@@ -437,7 +436,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// 5. All steps completed - invoke callback to determine whether to show the outcome screen
handleCallback(true);
- }, [biometrics, dispatch, handleCallback, isOffline, state, isWeb]);
+ }, [biometrics, dispatch, handleCallback, isOffline, state, promptType]);
/**
* Drives the MFA state machine forward whenever relevant state changes occur.
diff --git a/src/components/MultifactorAuthentication/Context/index.ts b/src/components/MultifactorAuthentication/Context/index.ts
index 0d17f180f2553..ba00581beac80 100644
--- a/src/components/MultifactorAuthentication/Context/index.ts
+++ b/src/components/MultifactorAuthentication/Context/index.ts
@@ -5,4 +5,7 @@ export type {MultifactorAuthenticationContextValue, ExecuteScenarioParams} from
export {useMultifactorAuthenticationState, useMultifactorAuthenticationActions} from './State';
export type {MultifactorAuthenticationState, MultifactorAuthenticationStateContextType, MultifactorAuthenticationActionsContextType, ErrorState, Action} from './State';
-export {default as usePromptContent, serverHasRegisteredCredentials} from './usePromptContent';
+export {default as usePromptContent} from './usePromptContent';
+
+export {default as useBiometrics} from '@components/MultifactorAuthentication/biometrics/useBiometrics';
+export type {UseBiometricsReturn, RegisterResult, AuthorizeResult, AuthorizeParams} from '@components/MultifactorAuthentication/biometrics/shared/types';
diff --git a/src/components/MultifactorAuthentication/Context/types.ts b/src/components/MultifactorAuthentication/Context/types.ts
index 80e6c53b434e6..a2c51af3f5f20 100644
--- a/src/components/MultifactorAuthentication/Context/types.ts
+++ b/src/components/MultifactorAuthentication/Context/types.ts
@@ -4,8 +4,8 @@ import type {
MultifactorAuthenticationScenarioConfig,
MultifactorAuthenticationScenarioResponse,
} from '@components/MultifactorAuthentication/config/types';
-import type {AuthenticationChallenge, RegistrationChallenge} from '@libs/MultifactorAuthentication/Biometrics/ED25519/types';
-import type {AuthTypeInfo, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/Biometrics/types';
+import type {AuthenticationChallenge, RegistrationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes';
+import type {AuthTypeInfo, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/shared/types';
type ErrorState = {
reason: MultifactorAuthenticationReason;
diff --git a/src/components/MultifactorAuthentication/Context/usePromptContent.ts b/src/components/MultifactorAuthentication/Context/usePromptContent.ts
index 38e1e8eeef019..8cbf9dbbdbf39 100644
--- a/src/components/MultifactorAuthentication/Context/usePromptContent.ts
+++ b/src/components/MultifactorAuthentication/Context/usePromptContent.ts
@@ -1,33 +1,23 @@
import {useEffect, useRef, useState} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
import type DotLottieAnimation from '@components/LottieAnimations/types';
+import useBiometrics from '@components/MultifactorAuthentication/biometrics/useBiometrics';
import {MULTIFACTOR_AUTHENTICATION_PROMPT_UI} from '@components/MultifactorAuthentication/config';
import type {MultifactorAuthenticationPromptType} from '@components/MultifactorAuthentication/config/types';
import useOnyx from '@hooks/useOnyx';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Account} from '@src/types/onyx';
+import type IconAsset from '@src/types/utils/IconAsset';
import {useMultifactorAuthenticationState} from './State';
-import useNativeBiometrics from './useNativeBiometrics';
type PromptContent = {
- animation: DotLottieAnimation;
+ illustration: DotLottieAnimation | IconAsset;
title: TranslationPaths;
subtitle: TranslationPaths | undefined;
shouldDisplayConfirmButton: boolean;
};
/**
- * Selector to check if server has any registered credentials for this account.
- * Note: This checks server state only, not device-local credentials.
- */
-function serverHasRegisteredCredentials(data: OnyxEntry) {
- const credentialIDs = data?.multifactorAuthenticationPublicKeyIDs;
- return credentialIDs && credentialIDs.length > 0;
-}
-
-/**
- * Hook to get the prompt content (animation, title, subtitle) for the MFA prompt page.
+ * Hook to get the prompt content (illustration, title, subtitle) for the MFA prompt page.
* Handles the logic for determining the correct title and subtitle based on:
* - Whether the user is a returning user (already has biometrics registered)
* - Whether registration has just been completed
@@ -38,7 +28,7 @@ function serverHasRegisteredCredentials(data: OnyxEntry) {
*/
function usePromptContent(promptType: MultifactorAuthenticationPromptType): PromptContent {
const state = useMultifactorAuthenticationState();
- const {areLocalCredentialsKnownToServer} = useNativeBiometrics();
+ const {areLocalCredentialsKnownToServer} = useBiometrics();
const [serverHasCredentials, setServerHasCredentials] = useState(false);
const [deviceBiometricsState] = useOnyx(ONYXKEYS.DEVICE_BIOMETRICS);
const hasEverAcceptedSoftPrompt = deviceBiometricsState?.hasAcceptedSoftPrompt ?? false;
@@ -107,7 +97,7 @@ function usePromptContent(promptType: MultifactorAuthenticationPromptType): Prom
!hasEverAcceptedSoftPrompt || (!state.softPromptApproved && !state.isRegistrationComplete && !serverHasCredentials && !wasPreviouslyRegisteredRef.current);
return {
- animation: contentData.animation,
+ illustration: contentData.illustration,
title,
subtitle,
shouldDisplayConfirmButton,
@@ -115,4 +105,3 @@ function usePromptContent(promptType: MultifactorAuthenticationPromptType): Prom
}
export default usePromptContent;
-export {serverHasRegisteredCredentials};
diff --git a/src/components/MultifactorAuthentication/PromptContent.tsx b/src/components/MultifactorAuthentication/PromptContent.tsx
index 455caa5f85ffa..916f411fe8bf1 100644
--- a/src/components/MultifactorAuthentication/PromptContent.tsx
+++ b/src/components/MultifactorAuthentication/PromptContent.tsx
@@ -1,19 +1,25 @@
import React from 'react';
import {View} from 'react-native';
+import ImageSVG from '@components/ImageSVG';
import Lottie from '@components/Lottie';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {TranslationPaths} from '@src/languages/types';
+import type IconAsset from '@src/types/utils/IconAsset';
type MultifactorAuthenticationPromptContentProps = {
- animation: DotLottieAnimation;
+ illustration: DotLottieAnimation | IconAsset;
title: TranslationPaths;
subtitle?: TranslationPaths;
};
-function MultifactorAuthenticationPromptContent({title, subtitle, animation}: MultifactorAuthenticationPromptContentProps) {
+function isLottieAnimation(source: DotLottieAnimation | IconAsset): source is DotLottieAnimation {
+ return typeof source === 'object' && 'file' in source;
+}
+
+function MultifactorAuthenticationPromptContent({title, subtitle, illustration}: MultifactorAuthenticationPromptContentProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -23,13 +29,21 @@ function MultifactorAuthenticationPromptContent({title, subtitle, animation}: Mu
testID="MultifactorAuthenticationPromptContent"
>
-
+ {isLottieAnimation(illustration) ? (
+
+ ) : (
+
+ )}
{translate(title)}
diff --git a/src/components/MultifactorAuthentication/biometrics/shared/types.ts b/src/components/MultifactorAuthentication/biometrics/shared/types.ts
new file mode 100644
index 0000000000000..31dc671dbb935
--- /dev/null
+++ b/src/components/MultifactorAuthentication/biometrics/shared/types.ts
@@ -0,0 +1,71 @@
+import type {ValueOf} from 'type-fest';
+import type {AuthenticationChallenge, RegistrationChallenge, SignedChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes';
+import type {AuthTypeInfo, MultifactorAuthenticationReason, RegistrationKeyInfo} from '@libs/MultifactorAuthentication/shared/types';
+import type CONST from '@src/CONST';
+
+type BaseRegisterResult = {
+ keyInfo: RegistrationKeyInfo;
+ authenticationMethod: AuthTypeInfo;
+};
+
+type RegisterResult =
+ | ({
+ success: true;
+ reason: MultifactorAuthenticationReason;
+ } & BaseRegisterResult)
+ | ({
+ success: false;
+ reason: MultifactorAuthenticationReason;
+ } & Partial);
+
+type AuthorizeParams = {
+ challenge: AuthenticationChallenge;
+};
+
+type AuthorizeResultSuccess = {
+ success: true;
+ reason: MultifactorAuthenticationReason;
+ signedChallenge: SignedChallenge;
+ authenticationMethod: AuthTypeInfo;
+};
+
+type AuthorizeResultFailure = {
+ success: false;
+ reason: MultifactorAuthenticationReason;
+};
+
+type AuthorizeResult = AuthorizeResultSuccess | AuthorizeResultFailure;
+
+type UseBiometricsReturn = {
+ /** The authentication method type provided by this hook (BIOMETRICS on native, PASSKEYS on web) */
+ deviceVerificationType: ValueOf;
+
+ /** List of credential IDs known to server (from Onyx) */
+ serverKnownCredentialIDs: string[];
+
+ /** Whether biometric credentials have ever been configured for this account */
+ haveCredentialsEverBeenConfigured: boolean;
+
+ /** Retrieve the credential ID stored locally on this device */
+ getLocalCredentialID: () => Promise;
+
+ /** Check if device supports the authentication method */
+ doesDeviceSupportAuthenticationMethod: () => boolean;
+
+ /** Check if device has biometric credentials stored locally */
+ hasLocalCredentials: () => Promise;
+
+ /** Check if local credentials are known to server (local credential exists in server's list) */
+ areLocalCredentialsKnownToServer: () => Promise;
+
+ /** Register current device for the chosen authentication method */
+ register: (onResult: (result: RegisterResult) => Promise | void, registrationChallenge: RegistrationChallenge) => Promise;
+
+ /** Authorize using chosen authentication method */
+ authorize: (params: AuthorizeParams, onResult: (result: AuthorizeResult) => Promise | void) => Promise;
+
+ /** Delete local keys for account */
+ deleteLocalKeysForAccount: () => Promise;
+};
+
+export type {BaseRegisterResult, RegisterResult, AuthorizeParams, AuthorizeResultSuccess, AuthorizeResultFailure, AuthorizeResult, UseBiometricsReturn};
diff --git a/src/components/MultifactorAuthentication/biometrics/shared/useServerCredentials.ts b/src/components/MultifactorAuthentication/biometrics/shared/useServerCredentials.ts
new file mode 100644
index 0000000000000..2d2fb01ee42d6
--- /dev/null
+++ b/src/components/MultifactorAuthentication/biometrics/shared/useServerCredentials.ts
@@ -0,0 +1,27 @@
+import {mfaCredentialIDsSelector} from '@selectors/Account';
+import useOnyx from '@hooks/useOnyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type UseServerCredentialsReturn = {
+ serverKnownCredentialIDs: string[];
+ haveCredentialsEverBeenConfigured: boolean;
+};
+
+/**
+ * Reads the server-known MFA credential IDs from Onyx.
+ * Shared between native biometrics and web passkeys hooks.
+ */
+function useServerCredentials(): UseServerCredentialsReturn {
+ const [multifactorAuthenticationPublicKeyIDs] = useOnyx(ONYXKEYS.ACCOUNT, {
+ selector: mfaCredentialIDsSelector,
+ });
+ const serverKnownCredentialIDs = multifactorAuthenticationPublicKeyIDs ?? [];
+ const haveCredentialsEverBeenConfigured = multifactorAuthenticationPublicKeyIDs !== undefined;
+
+ return {
+ serverKnownCredentialIDs,
+ haveCredentialsEverBeenConfigured,
+ };
+}
+
+export default useServerCredentials;
diff --git a/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.native.ts b/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.native.ts
new file mode 100644
index 0000000000000..cb40a9d657c45
--- /dev/null
+++ b/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.native.ts
@@ -0,0 +1,3 @@
+import useNativeBiometrics from '@components/MultifactorAuthentication/biometrics/useNativeBiometrics';
+
+export default useNativeBiometrics;
diff --git a/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.ts b/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.ts
new file mode 100644
index 0000000000000..993fa7618dea1
--- /dev/null
+++ b/src/components/MultifactorAuthentication/biometrics/useBiometrics/index.ts
@@ -0,0 +1,3 @@
+import usePasskeys from '@components/MultifactorAuthentication/biometrics/usePasskeys';
+
+export default usePasskeys;
diff --git a/src/components/MultifactorAuthentication/Context/useNativeBiometrics.ts b/src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts
similarity index 57%
rename from src/components/MultifactorAuthentication/Context/useNativeBiometrics.ts
rename to src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts
index 3c4ae5a3688c2..308ad087d2c10 100644
--- a/src/components/MultifactorAuthentication/Context/useNativeBiometrics.ts
+++ b/src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts
@@ -1,87 +1,16 @@
-import {useCallback, useMemo} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
+import {useCallback} from 'react';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
-import {generateKeyPair, signToken as signTokenED25519} from '@libs/MultifactorAuthentication/Biometrics/ED25519';
-import type {AuthenticationChallenge, SignedChallenge} from '@libs/MultifactorAuthentication/Biometrics/ED25519/types';
-import {PrivateKeyStore, PublicKeyStore} from '@libs/MultifactorAuthentication/Biometrics/KeyStore';
-import {SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/Biometrics/SecureStore';
-import type {AuthTypeInfo, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/Biometrics/types';
-import VALUES from '@libs/MultifactorAuthentication/Biometrics/VALUES';
+import {generateKeyPair, signToken as signTokenED25519} from '@libs/MultifactorAuthentication/NativeBiometrics/ED25519';
+import {PrivateKeyStore, PublicKeyStore} from '@libs/MultifactorAuthentication/NativeBiometrics/KeyStore';
+import {SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/NativeBiometrics/SecureStore';
+import type {NativeBiometricsKeyInfo} from '@libs/MultifactorAuthentication/NativeBiometrics/types';
+import type {RegistrationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes';
+import VALUES from '@libs/MultifactorAuthentication/VALUES';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Account} from '@src/types/onyx';
-
-type BaseRegisterResult = {
- privateKey: string;
- publicKey: string;
- authenticationMethod: AuthTypeInfo;
-};
-
-type RegisterResult =
- | ({
- success: true;
- reason: MultifactorAuthenticationReason;
- } & BaseRegisterResult)
- | ({
- success: false;
- reason: MultifactorAuthenticationReason;
- } & Partial);
-
-type AuthorizeParams = {
- challenge: AuthenticationChallenge;
-};
-
-type AuthorizeResultSuccess = {
- success: true;
- reason: MultifactorAuthenticationReason;
- signedChallenge: SignedChallenge;
- authenticationMethod: AuthTypeInfo;
-};
-
-type AuthorizeResultFailure = {
- success: false;
- reason: MultifactorAuthenticationReason;
-};
-
-type AuthorizeResult = AuthorizeResultSuccess | AuthorizeResultFailure;
-
-// In the 4th release of the Multifactor Authentication this interface will not focus on the Onyx/Auth values.
-// Instead, the providers abstraction will be added.
-// For context, see: https://github.com/Expensify/App/pull/79473#discussion_r2747993460
-type UseNativeBiometricsReturn = {
- /** List of credential IDs known to server (from Onyx) */
- serverKnownCredentialIDs: string[];
-
- /** Whether biometric credentials have ever been configured for this account */
- haveCredentialsEverBeenConfigured: boolean;
-
- /** Retrieve the public key stored locally on this device */
- getLocalPublicKey: () => Promise;
-
- /** Check if device supports biometrics */
- doesDeviceSupportBiometrics: () => boolean;
-
- /** Check if local credentials are known to server (local credential exists in server's list) */
- areLocalCredentialsKnownToServer: () => Promise;
-
- /** Register biometrics on device */
- register: (onResult: (result: RegisterResult) => Promise | void) => Promise;
-
- /** Authorize using biometrics */
- authorize: (params: AuthorizeParams, onResult: (result: AuthorizeResult) => Promise | void) => Promise;
-
- /** Reset keys for account */
- resetKeysForAccount: () => Promise;
-};
-
-/**
- * Selector to get multifactor authentication public key IDs from Account Onyx state.
- */
-function getMultifactorAuthenticationPublicKeyIDs(data: OnyxEntry) {
- return data?.multifactorAuthenticationPublicKeyIDs;
-}
+import Base64URL from '@src/utils/Base64URL';
+import type {AuthorizeParams, AuthorizeResult, RegisterResult, UseBiometricsReturn} from './shared/types';
+import useServerCredentials from './shared/useServerCredentials';
/**
* Clears local credentials to allow re-registration.
@@ -91,42 +20,39 @@ async function resetKeys(accountID: number) {
await Promise.all([PrivateKeyStore.delete(accountID), PublicKeyStore.delete(accountID)]);
}
-function useNativeBiometrics(): UseNativeBiometricsReturn {
+function useNativeBiometrics(): UseBiometricsReturn {
const {accountID} = useCurrentUserPersonalDetails();
const {translate} = useLocalize();
-
- const [multifactorAuthenticationPublicKeyIDs] = useOnyx(ONYXKEYS.ACCOUNT, {selector: getMultifactorAuthenticationPublicKeyIDs});
- const serverKnownCredentialIDs = useMemo(() => multifactorAuthenticationPublicKeyIDs ?? [], [multifactorAuthenticationPublicKeyIDs]);
- const haveCredentialsEverBeenConfigured = multifactorAuthenticationPublicKeyIDs !== undefined;
+ const {serverKnownCredentialIDs, haveCredentialsEverBeenConfigured} = useServerCredentials();
/**
* Checks if the device supports biometric authentication methods.
* Verifies both biometrics and credentials authentication capabilities.
* @returns True if biometrics or credentials authentication is supported on the device.
*/
- const doesDeviceSupportBiometrics = useCallback(() => {
+ const doesDeviceSupportAuthenticationMethod = useCallback(() => {
const {biometrics, credentials} = PublicKeyStore.supportedAuthentication;
return biometrics || credentials;
}, []);
- // Only the public key is checked here because reading the private key
+ // Only the credential ID is checked here because reading the private key
// requires biometric authentication. If the private key is missing, it
// will be detected during authorize() and trigger re-registration.
- const getLocalPublicKey = useCallback(async () => {
+ const getLocalCredentialID = useCallback(async () => {
const {value} = await PublicKeyStore.get(accountID);
return value ?? undefined;
}, [accountID]);
const areLocalCredentialsKnownToServer = useCallback(async () => {
- const key = await getLocalPublicKey();
+ const key = await getLocalCredentialID();
return !!key && serverKnownCredentialIDs.includes(key);
- }, [getLocalPublicKey, serverKnownCredentialIDs]);
+ }, [getLocalCredentialID, serverKnownCredentialIDs]);
- const resetKeysForAccount = useCallback(async () => {
+ const deleteLocalKeysForAccount = useCallback(async () => {
await resetKeys(accountID);
}, [accountID]);
- const register = async (onResult: (result: RegisterResult) => Promise | void) => {
+ const register = async (onResult: (result: RegisterResult) => Promise | void, registrationChallenge: RegistrationChallenge) => {
// Generate key pair
const {privateKey, publicKey} = generateKeyPair();
@@ -168,12 +94,23 @@ function useNativeBiometrics(): UseNativeBiometricsReturn {
return;
}
- // Return success with keys - challenge is passed from Main.tsx
+ const clientDataJSON = JSON.stringify({challenge: registrationChallenge.challenge});
+ const keyInfo: NativeBiometricsKeyInfo = {
+ rawId: publicKey,
+ type: CONST.MULTIFACTOR_AUTHENTICATION.ED25519_TYPE,
+ response: {
+ clientDataJSON: Base64URL.encode(clientDataJSON),
+ biometric: {
+ publicKey,
+ algorithm: CONST.COSE_ALGORITHM.EDDSA,
+ },
+ },
+ };
+
await onResult({
success: true,
reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.LOCAL_REGISTRATION_COMPLETE,
- privateKey,
- publicKey,
+ keyInfo,
authenticationMethod: authType,
});
};
@@ -182,7 +119,7 @@ function useNativeBiometrics(): UseNativeBiometricsReturn {
const {challenge} = params;
// Extract public keys from challenge.allowCredentials
- const authPublicKeys = challenge.allowCredentials?.map((cred: {id: string; type: string}) => cred.id) ?? [];
+ const allowedCredentialIDs = challenge.allowCredentials?.map((cred: {id: string; type: string}) => cred.id) ?? [];
// Get private key from SecureStore
const privateKeyData = await PrivateKeyStore.get(accountID, {nativePromptTitle: translate('multifactorAuthentication.letsVerifyItsYou')});
@@ -195,9 +132,9 @@ function useNativeBiometrics(): UseNativeBiometricsReturn {
return;
}
- const publicKey = await getLocalPublicKey();
+ const credentialID = await getLocalCredentialID();
- if (!publicKey || !authPublicKeys.includes(publicKey)) {
+ if (!credentialID || !allowedCredentialIDs.includes(credentialID)) {
await resetKeys(accountID);
onResult({
success: false,
@@ -207,7 +144,7 @@ function useNativeBiometrics(): UseNativeBiometricsReturn {
}
// Sign the challenge
- const signedChallenge = signTokenED25519(challenge, privateKeyData.value, publicKey);
+ const signedChallenge = signTokenED25519(challenge, privateKeyData.value, credentialID);
const authenticationMethodCode = privateKeyData.type;
const authTypeEntry = Object.values(SECURE_STORE_VALUES.AUTH_TYPE).find(({CODE}) => CODE === authenticationMethodCode);
@@ -236,17 +173,20 @@ function useNativeBiometrics(): UseNativeBiometricsReturn {
});
};
+ const hasLocalCredentials = async () => !!(await getLocalCredentialID());
+
return {
+ deviceVerificationType: CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS,
serverKnownCredentialIDs,
haveCredentialsEverBeenConfigured,
- getLocalPublicKey,
- doesDeviceSupportBiometrics,
+ getLocalCredentialID,
+ doesDeviceSupportAuthenticationMethod,
+ hasLocalCredentials,
areLocalCredentialsKnownToServer,
register,
authorize,
- resetKeysForAccount,
+ deleteLocalKeysForAccount,
};
}
export default useNativeBiometrics;
-export type {RegisterResult, AuthorizeParams, AuthorizeResult, UseNativeBiometricsReturn};
diff --git a/src/components/MultifactorAuthentication/biometrics/usePasskeys.ts b/src/components/MultifactorAuthentication/biometrics/usePasskeys.ts
new file mode 100644
index 0000000000000..371e8a7fbd883
--- /dev/null
+++ b/src/components/MultifactorAuthentication/biometrics/usePasskeys.ts
@@ -0,0 +1,185 @@
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useOnyx from '@hooks/useOnyx';
+import {
+ arrayBufferToBase64URL,
+ authenticateWithPasskey,
+ buildAllowedCredentialDescriptors,
+ buildPublicKeyCredentialCreationOptions,
+ buildPublicKeyCredentialRequestOptions,
+ createPasskeyCredential,
+ decodeWebAuthnError,
+ isSupportedTransport,
+ isWebAuthnSupported,
+ PASSKEY_AUTH_TYPE,
+} from '@libs/MultifactorAuthentication/Passkeys/WebAuthn';
+import type {RegistrationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes';
+import VALUES from '@libs/MultifactorAuthentication/VALUES';
+import {addLocalPasskeyCredential, deleteLocalPasskeyCredentials, getPasskeyOnyxKey, reconcileLocalPasskeysWithBackend} from '@userActions/Passkey';
+import CONST from '@src/CONST';
+import type {AuthorizeParams, AuthorizeResult, RegisterResult, UseBiometricsReturn} from './shared/types';
+import useServerCredentials from './shared/useServerCredentials';
+
+function usePasskeys(): UseBiometricsReturn {
+ const {accountID} = useCurrentUserPersonalDetails();
+ const userId = String(accountID);
+ const {serverKnownCredentialIDs, haveCredentialsEverBeenConfigured} = useServerCredentials();
+ const [localPasskeyCredentials] = useOnyx(getPasskeyOnyxKey(userId));
+
+ const doesDeviceSupportAuthenticationMethod = () => isWebAuthnSupported();
+
+ const getLocalCredentialID = async (): Promise => {
+ return (localPasskeyCredentials ?? []).at(0)?.id;
+ };
+
+ const hasLocalCredentials = async () => (localPasskeyCredentials?.length ?? 0) > 0;
+
+ const areLocalCredentialsKnownToServer = async () => {
+ const serverSet = new Set(serverKnownCredentialIDs);
+ return (localPasskeyCredentials ?? []).some((c) => serverSet.has(c.id));
+ };
+
+ const deleteLocalKeysForAccount = async () => {
+ deleteLocalPasskeyCredentials(userId);
+ };
+
+ const register = async (onResult: (result: RegisterResult) => Promise | void, registrationChallenge: RegistrationChallenge) => {
+ const backendCredentials = serverKnownCredentialIDs.map((id) => ({id, type: CONST.PASSKEY_CREDENTIAL_TYPE}));
+ const reconciledExisting = reconcileLocalPasskeysWithBackend({
+ userId,
+ backendCredentials,
+ localCredentials: localPasskeyCredentials ?? null,
+ });
+ const publicKeyOptions = buildPublicKeyCredentialCreationOptions(registrationChallenge, reconciledExisting);
+
+ let credential: PublicKeyCredential;
+ try {
+ credential = await createPasskeyCredential(publicKeyOptions);
+ } catch (error) {
+ await onResult({
+ success: false,
+ reason: decodeWebAuthnError(error),
+ });
+ return;
+ }
+
+ if (!(credential.response instanceof AuthenticatorAttestationResponse)) {
+ await onResult({
+ success: false,
+ reason: VALUES.REASON.WEBAUTHN.UNEXPECTED_RESPONSE,
+ });
+ return;
+ }
+ const attestationResponse = credential.response;
+ const credentialId = arrayBufferToBase64URL(credential.rawId);
+ const clientDataJSON = arrayBufferToBase64URL(attestationResponse.clientDataJSON);
+ const attestationObject = arrayBufferToBase64URL(attestationResponse.attestationObject);
+
+ const transports = attestationResponse.getTransports?.().filter(isSupportedTransport);
+
+ addLocalPasskeyCredential({
+ userId,
+ credential: {
+ id: credentialId,
+ type: CONST.PASSKEY_CREDENTIAL_TYPE,
+ transports,
+ },
+ existingCredentials: localPasskeyCredentials ?? null,
+ });
+
+ await onResult({
+ success: true,
+ reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.LOCAL_REGISTRATION_COMPLETE,
+ keyInfo: {
+ rawId: credentialId,
+ type: CONST.PASSKEY_CREDENTIAL_TYPE,
+ response: {
+ clientDataJSON,
+ attestationObject,
+ },
+ },
+ authenticationMethod: {
+ name: PASSKEY_AUTH_TYPE.NAME,
+ marqetaValue: PASSKEY_AUTH_TYPE.MARQETA_VALUE,
+ },
+ });
+ };
+
+ const authorize = async (params: AuthorizeParams, onResult: (result: AuthorizeResult) => Promise | void) => {
+ const {challenge} = params;
+
+ const backendCredentials = challenge.allowCredentials?.map((c) => ({id: c.id, type: CONST.PASSKEY_CREDENTIAL_TYPE})) ?? [];
+ const reconciled = reconcileLocalPasskeysWithBackend({
+ userId,
+ backendCredentials,
+ localCredentials: localPasskeyCredentials ?? null,
+ });
+
+ if (reconciled.length === 0) {
+ await onResult({
+ success: false,
+ reason: VALUES.REASON.WEBAUTHN.REGISTRATION_REQUIRED,
+ });
+ return;
+ }
+
+ const allowCredentials = buildAllowedCredentialDescriptors(reconciled);
+ const publicKeyOptions = buildPublicKeyCredentialRequestOptions(challenge, allowCredentials);
+
+ let assertion: PublicKeyCredential;
+ try {
+ assertion = await authenticateWithPasskey(publicKeyOptions);
+ } catch (error) {
+ await onResult({
+ success: false,
+ reason: decodeWebAuthnError(error),
+ });
+ return;
+ }
+
+ if (!(assertion.response instanceof AuthenticatorAssertionResponse)) {
+ await onResult({
+ success: false,
+ reason: VALUES.REASON.WEBAUTHN.UNEXPECTED_RESPONSE,
+ });
+ return;
+ }
+ const assertionResponse = assertion.response;
+ const rawId = arrayBufferToBase64URL(assertion.rawId);
+ const authenticatorData = arrayBufferToBase64URL(assertionResponse.authenticatorData);
+ const clientDataJSON = arrayBufferToBase64URL(assertionResponse.clientDataJSON);
+ const signature = arrayBufferToBase64URL(assertionResponse.signature);
+
+ await onResult({
+ success: true,
+ reason: VALUES.REASON.CHALLENGE.CHALLENGE_SIGNED,
+ signedChallenge: {
+ rawId,
+ type: CONST.PASSKEY_CREDENTIAL_TYPE,
+ response: {
+ authenticatorData,
+ clientDataJSON,
+ signature,
+ },
+ },
+ authenticationMethod: {
+ name: PASSKEY_AUTH_TYPE.NAME,
+ marqetaValue: PASSKEY_AUTH_TYPE.MARQETA_VALUE,
+ },
+ });
+ };
+
+ return {
+ deviceVerificationType: CONST.MULTIFACTOR_AUTHENTICATION.TYPE.PASSKEYS,
+ serverKnownCredentialIDs,
+ haveCredentialsEverBeenConfigured,
+ getLocalCredentialID,
+ doesDeviceSupportAuthenticationMethod,
+ hasLocalCredentials,
+ areLocalCredentialsKnownToServer,
+ register,
+ authorize,
+ deleteLocalKeysForAccount,
+ };
+}
+
+export default usePasskeys;
diff --git a/src/components/MultifactorAuthentication/components/AuthenticationMethodDescription.tsx b/src/components/MultifactorAuthentication/components/AuthenticationMethodDescription.tsx
index 05d0909d709d3..dc6536938acfe 100644
--- a/src/components/MultifactorAuthentication/components/AuthenticationMethodDescription.tsx
+++ b/src/components/MultifactorAuthentication/components/AuthenticationMethodDescription.tsx
@@ -3,8 +3,8 @@ import {useMultifactorAuthenticationState} from '@components/MultifactorAuthenti
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import {SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/Biometrics/SecureStore';
-import type {AuthTypeName} from '@libs/MultifactorAuthentication/Biometrics/types';
+import {SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/NativeBiometrics/SecureStore';
+import type {AuthTypeName} from '@libs/MultifactorAuthentication/shared/types';
import type {TranslationPaths} from '@src/languages/types';
/* eslint-disable @typescript-eslint/naming-convention */
@@ -16,6 +16,7 @@ const AUTH_TYPE_TRANSLATION_KEY = {
'Face ID': 'multifactorAuthentication.biometricsTest.authType.faceId',
'Touch ID': 'multifactorAuthentication.biometricsTest.authType.touchId',
'Optic ID': 'multifactorAuthentication.biometricsTest.authType.opticId',
+ Passkey: 'multifactorAuthentication.biometricsTest.authType.passkey',
} as const satisfies Record;
/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/components/MultifactorAuthentication/components/OutcomeScreen/SuccessScreen/defaultScreens.tsx b/src/components/MultifactorAuthentication/components/OutcomeScreen/SuccessScreen/defaultScreens.tsx
index dcba752438053..808ad844f27ce 100644
--- a/src/components/MultifactorAuthentication/components/OutcomeScreen/SuccessScreen/defaultScreens.tsx
+++ b/src/components/MultifactorAuthentication/components/OutcomeScreen/SuccessScreen/defaultScreens.tsx
@@ -6,7 +6,7 @@ const DefaultSuccessScreen = createScreenWithDefaults(
SuccessScreenBase,
{
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsAuthentication',
- illustration: 'OpenPadlock',
+ illustration: 'OpenPadlockGreen',
iconWidth: variables.openPadlockWidth,
iconHeight: variables.openPadlockHeight,
title: 'multifactorAuthentication.biometricsTest.authenticationSuccessful',
diff --git a/src/components/MultifactorAuthentication/config/scenarios/BiometricsTest.tsx b/src/components/MultifactorAuthentication/config/scenarios/BiometricsTest.tsx
index 62795d8b0f31b..bd97a4e7ed9ac 100644
--- a/src/components/MultifactorAuthentication/config/scenarios/BiometricsTest.tsx
+++ b/src/components/MultifactorAuthentication/config/scenarios/BiometricsTest.tsx
@@ -7,7 +7,7 @@ import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';
export default {
- allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS],
+ allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS, CONST.MULTIFACTOR_AUTHENTICATION.TYPE.PASSKEYS],
action: troubleshootMultifactorAuthentication,
screen: SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST,
pure: true,
diff --git a/src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx b/src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx
index 8a6535c0f1640..35e20f0a7e948 100644
--- a/src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx
+++ b/src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx
@@ -8,7 +8,7 @@ import {
UnsupportedDeviceFailureScreen,
} from '@components/MultifactorAuthentication/components/OutcomeScreen';
import type {MultifactorAuthenticationDefaultUIConfig, MultifactorAuthenticationScenarioCustomConfig} from '@components/MultifactorAuthentication/config/types';
-import type {MultifactorAuthenticationCallbackResponse} from '@libs/MultifactorAuthentication/Biometrics/types';
+import type {MultifactorAuthenticationCallbackResponse} from '@libs/MultifactorAuthentication/shared/types';
import CONST from '@src/CONST';
const DEFAULT_CONFIG = {
diff --git a/src/components/MultifactorAuthentication/config/scenarios/names.ts b/src/components/MultifactorAuthentication/config/scenarios/names.ts
index 27e44e9cd2bb2..4d5cb515cbfab 100644
--- a/src/components/MultifactorAuthentication/config/scenarios/names.ts
+++ b/src/components/MultifactorAuthentication/config/scenarios/names.ts
@@ -19,6 +19,7 @@ const SCENARIO_NAMES = {
*/
const PROMPT_NAMES = {
BIOMETRICS: 'biometrics',
-};
+ PASSKEYS: 'passkeys',
+} as const;
export {SCENARIO_NAMES, PROMPT_NAMES};
diff --git a/src/components/MultifactorAuthentication/config/scenarios/prompts.ts b/src/components/MultifactorAuthentication/config/scenarios/prompts.ts
index 5be7642bc7712..eda96d7b391e3 100644
--- a/src/components/MultifactorAuthentication/config/scenarios/prompts.ts
+++ b/src/components/MultifactorAuthentication/config/scenarios/prompts.ts
@@ -1,15 +1,21 @@
+import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import type {MultifactorAuthenticationPrompt} from '@components/MultifactorAuthentication/config/types';
-import VALUES from '@libs/MultifactorAuthentication/Biometrics/VALUES';
+import VALUES from '@libs/MultifactorAuthentication/VALUES';
/**
- * Configuration for multifactor authentication prompt UI with animations and translations.
+ * Configuration for multifactor authentication prompt UI with illustrations and translations.
* Exported to a separate file to avoid circular dependencies.
*/
export default {
[VALUES.PROMPT.BIOMETRICS]: {
- animation: LottieAnimations.Fingerprint,
+ illustration: LottieAnimations.Fingerprint,
title: 'multifactorAuthentication.verifyYourself.biometrics',
subtitle: 'multifactorAuthentication.enableQuickVerification.biometrics',
},
+ [VALUES.PROMPT.PASSKEYS]: {
+ illustration: Illustrations.EncryptionPasskeys,
+ title: 'multifactorAuthentication.verifyYourself.passkeys',
+ subtitle: 'multifactorAuthentication.enableQuickVerification.passkeys',
+ },
} as const satisfies MultifactorAuthenticationPrompt;
diff --git a/src/components/MultifactorAuthentication/config/types.ts b/src/components/MultifactorAuthentication/config/types.ts
index 38e5419dc89c6..bb3b9267ffaeb 100644
--- a/src/components/MultifactorAuthentication/config/types.ts
+++ b/src/components/MultifactorAuthentication/config/types.ts
@@ -7,13 +7,14 @@ import type {CancelConfirmModalProps} from '@components/MultifactorAuthenticatio
import type {
AllMultifactorAuthenticationBaseParameters,
MultifactorAuthenticationActionParams,
- MultifactorAuthenticationKeyInfo,
MultifactorAuthenticationReason,
MultifactorAuthenticationScenarioCallback,
-} from '@libs/MultifactorAuthentication/Biometrics/types';
+ RegistrationKeyInfo,
+} from '@libs/MultifactorAuthentication/shared/types';
import type CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type SCREENS from '@src/SCREENS';
+import type IconAsset from '@src/types/utils/IconAsset';
import type {MULTIFACTOR_AUTHENTICATION_PROMPT_UI, MultifactorAuthenticationScenarioPayload} from './index';
/**
@@ -27,10 +28,10 @@ type MultifactorAuthenticationCancelConfirm = {
};
/**
- * Configuration for multifactor authentication prompt display with animation and translations.
+ * Configuration for multifactor authentication prompt display with illustration and translations.
*/
type MultifactorAuthenticationPromptConfig = {
- animation: DotLottieAnimation;
+ illustration: DotLottieAnimation | IconAsset;
title: TranslationPaths;
subtitle: TranslationPaths;
};
@@ -172,7 +173,7 @@ type MultifactorAuthenticationPromptType = keyof typeof MULTIFACTOR_AUTHENTICATI
*/
type RegisterBiometricsParams = MultifactorAuthenticationActionParams<
{
- keyInfo: MultifactorAuthenticationKeyInfo;
+ keyInfo: RegistrationKeyInfo;
},
'validateCode'
>;
diff --git a/src/components/MultifactorAuthentication/observability/trackMFAFlowOutcome.ts b/src/components/MultifactorAuthentication/observability/trackMFAFlowOutcome.ts
index 8db8e96b03095..37585ee9e0e91 100644
--- a/src/components/MultifactorAuthentication/observability/trackMFAFlowOutcome.ts
+++ b/src/components/MultifactorAuthentication/observability/trackMFAFlowOutcome.ts
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/react-native';
import type {MultifactorAuthenticationScenarioResponse} from '@components/MultifactorAuthentication/config/types';
import type {ErrorState} from '@components/MultifactorAuthentication/Context/types';
import Log from '@libs/Log';
-import type {AuthTypeName, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/Biometrics/types';
+import type {AuthTypeName, MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/shared/types';
import CONST from '@src/CONST';
type FailureClassification = 'routine' | 'anomalous' | 'unclassified';
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index d7a12f3984997..0de2f7d43f6aa 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -33,7 +33,7 @@ function TestToolMenu() {
const {translate} = useLocalize();
const {clearLHNCache} = useSidebarOrderedReportsActions();
const [isMFARevokeLoading, setIsMFARevokeLoading] = useState(false);
- const {localPublicKey, isCurrentDeviceRegistered, otherDeviceCount, registrationStatus} = useBiometricRegistrationStatus();
+ const {localCredentialID, isCurrentDeviceRegistered, otherDeviceCount, registrationStatus} = useBiometricRegistrationStatus();
const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();
@@ -125,7 +125,7 @@ function TestToolMenu() {
text={translate('multifactorAuthentication.biometricsTest.test')}
onPress={() => navigateToBiometricsTestPage()}
/>
- {isCurrentDeviceRegistered && !!localPublicKey && (
+ {isCurrentDeviceRegistered && !!localCredentialID && (