From 7791359473e94a9769cae85a52e645487d40e6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20T=C3=B3th?= Date: Wed, 17 Jul 2024 14:30:25 +0200 Subject: [PATCH] NEVISACCESSAPP-5966: added password authenticator with custom policy --- src/App.tsx | 2 + src/error/AppError.ts | 10 + src/i18n/en/translation.json | 25 ++- src/model/AuthenticatorItem.ts | 5 +- src/model/OperationType.ts | 3 + src/model/PasswordMode.ts | 9 + .../AuthCloudApiRegistrationViewModel.ts | 2 + src/screens/HomeScreen.tsx | 2 + src/screens/HomeViewModel.ts | 63 +++++- src/screens/PasswordScreen.tsx | 183 ++++++++++++++++++ src/screens/PasswordViewModel.ts | 85 ++++++++ src/screens/RootStackParamList.ts | 14 ++ src/screens/SelectAccountViewModel.ts | 22 +++ src/screens/UsernamePasswordLoginViewModel.ts | 2 + .../OutOfBandOperationHandler.ts | 4 + src/userInteraction/PasswordChangerImpl.ts | 33 ++++ src/userInteraction/PasswordEnrollerImpl.ts | 36 ++++ src/userInteraction/PasswordPolicyImpl.ts | 46 +++++ .../PasswordUserVerifierImpl.ts | 37 ++++ src/userInteraction/PinUserVerifierImpl.ts | 8 +- src/utility/AuthenticatorUtils.ts | 2 + yarn.lock | 15 +- 22 files changed, 597 insertions(+), 11 deletions(-) create mode 100644 src/model/PasswordMode.ts create mode 100644 src/screens/PasswordScreen.tsx create mode 100644 src/screens/PasswordViewModel.ts create mode 100644 src/userInteraction/PasswordChangerImpl.ts create mode 100644 src/userInteraction/PasswordEnrollerImpl.ts create mode 100644 src/userInteraction/PasswordPolicyImpl.ts create mode 100644 src/userInteraction/PasswordUserVerifierImpl.ts diff --git a/src/App.tsx b/src/App.tsx index d241519..4570abd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import AuthCloudApiRegistrationScreen from './screens/AuthCloudApiRegistrationSc import ConfirmationScreen from './screens/ConfirmationScreen'; import DeviceInformationChangeScreen from './screens/DeviceInformationChangeScreen'; import HomeScreen from './screens/HomeScreen'; +import PasswordScreen from './screens/PasswordScreen'; import PinScreen from './screens/PinScreen'; import ReadQrCodeScreen from './screens/ReadQrCodeScreen'; import ResultScreen from './screens/ResultScreen'; @@ -49,6 +50,7 @@ export default function App() { component={SelectAuthenticatorScreen} /> + { .deviceInformation(DeviceInformationUtils.create()) .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) .pinEnroller(new PinEnrollerImpl()) + .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) .devicePasscodeUserVerifier(new DevicePasscodeUserVerifierImpl()) .fingerprintUserVerifier(new FingerprintUserVerifierImpl()) diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index d586278..00194db 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -29,6 +29,7 @@ const HomeScreen = () => { changeDeviceInformation, deleteLocalAuthenticators, changePin, + changePassword, } = useHomeViewModel(); const { t } = useTranslation(); @@ -99,6 +100,7 @@ const HomeScreen = () => { onPress={changeDeviceInformation} /> + { ); } - console.log(`Available device info: ${deviceInformation}`); + console.log( + `Available device info: ${JSON.stringify(deviceInformation, null, ' ')}` + ); navigation.navigate('DeviceInformationChange', { name: deviceInformation.name, }); @@ -284,16 +288,16 @@ const useHomeViewModel = () => { } else if (eligibleAccounts.length === 1) { // in case that there is only one account, then we can select it automatically console.log('Automatically selecting account for PIN Change'); - await startPinChange(eligibleAccounts); + await startPinChange(eligibleAccounts.at(0)!); } else { // in case that there are multiple eligible accounts then we have to show the account selection screen return selectAccount(OperationType.pinChange); } - async function startPinChange(accounts: Array) { + async function startPinChange(account: Account) { const client = ClientProvider.getInstance().client; client?.operations.pinChange - .username(accounts.at(0)!.username) + .username(account.username) .pinChanger(new PinChangerImpl()) .onSuccess(() => { console.log('PIN Change succeeded.'); @@ -307,6 +311,56 @@ const useHomeViewModel = () => { } } + async function changePassword() { + // we should only pass the accounts to the account selection that already have a password enrollment + const filteredAuthenticators = localAuthenticators.filter((authenticator) => { + return authenticator.aaid === Aaid.PASSWORD.rawValue(); + }); + const passwordAuthenticator = filteredAuthenticators.at(0); + if (!passwordAuthenticator) { + return ErrorHandler.handle( + OperationType.passwordChange, + new AppErrorPasswordAuthenticatorNotFound( + 'Password change failed, there are no registered password authenticators' + ) + ); + } + + const userEnrollment = passwordAuthenticator.userEnrollment; + const eligibleAccounts = localAccounts.filter((account) => { + return userEnrollment.isEnrolled(account.username); + }); + if (eligibleAccounts.length === 0) { + return ErrorHandler.handle( + OperationType.passwordChange, + new AppErrorAccountsNotFound(`Password change failed, no eligible accounts found`) + ); + } else if (eligibleAccounts.length === 1) { + // in case that there is only one account, then we can select it automatically + console.log('Automatically selecting account for password Change'); + await startPasswordChange(eligibleAccounts.at(0)!); + } else { + // in case that there are multiple eligible accounts then we have to show the account selection screen + return selectAccount(OperationType.passwordChange); + } + + async function startPasswordChange(account: Account) { + const client = ClientProvider.getInstance().client; + client?.operations.passwordChange + .username(account.username) + .passwordChanger(new PasswordChangerImpl()) + .onSuccess(() => { + console.log('Password Change succeeded.'); + RootNavigation.navigate('Result', { + operation: OperationType.passwordChange, + }); + }) + .onError(ErrorHandler.handle.bind(null, OperationType.passwordChange)) + .execute() + .catch(ErrorHandler.handle.bind(null, OperationType.passwordChange)); + } + } + return { numberOfAccounts, initClient, @@ -320,6 +374,7 @@ const useHomeViewModel = () => { changeDeviceInformation, deleteLocalAuthenticators, changePin, + changePassword, }; }; diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx new file mode 100644 index 0000000..acc829b --- /dev/null +++ b/src/screens/PasswordScreen.tsx @@ -0,0 +1,183 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { useCallback } from 'react'; +import { + BackHandler, + KeyboardAvoidingView, + Platform, + ScrollView, + Text, + useColorScheme, + View, +} from 'react-native'; + +import { + PasswordAuthenticatorProtectionStatus, + PasswordProtectionStatusLastAttemptFailed, + PasswordProtectionStatusLockedOut, + PasswordProtectionStatusUnlocked, + RecoverableError, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; +import { useFocusEffect } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { useTranslation } from 'react-i18next'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import usePasswordViewModel from './PasswordViewModel'; +import { RootStackParamList } from './RootStackParamList'; +import InputField from '../components/InputField'; +import OutlinedButton from '../components/OutlinedButton'; +import { PasswordMode } from '../model/PasswordMode'; +import { darkStyle, lightStyle } from '../Styles'; + +type Props = NativeStackScreenProps; + +const PasswordScreen = ({ route }: Props) => { + const { setOldPassword, setPassword, confirm, cancel } = usePasswordViewModel(); + + const { t } = useTranslation(); + const colorScheme = useColorScheme(); + const styles = colorScheme === 'dark' ? darkStyle : lightStyle; + const insets = useSafeAreaInsets(); + + useFocusEffect( + useCallback(() => { + const onBackPress = () => { + cancel(route.params.mode, route.params.handler); + return true; + }; + + const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress); + + return () => subscription.remove(); + }, [route.params.mode, route.params.handler]) + ); + + function title(passwordMode: PasswordMode): string { + switch (passwordMode) { + case PasswordMode.enrollment: + return t('password.enrollment.title'); + case PasswordMode.verification: + return t('password.verification.title'); + case PasswordMode.credentialChange: + return t('password.change.title'); + } + } + + function description(passwordMode: PasswordMode): string { + switch (passwordMode) { + case PasswordMode.enrollment: + return t('password.enrollment.description'); + case PasswordMode.verification: + return t('password.verification.description'); + case PasswordMode.credentialChange: + return t('password.change.description'); + } + } + + function errorText(recoverableError: RecoverableError): string { + let text = recoverableError.description; + if (recoverableError.cause) { + text += ` ${recoverableError.cause}`; + } + + return text; + } + + function authenticatorProtectionText(status?: PasswordAuthenticatorProtectionStatus): string { + if (status instanceof PasswordProtectionStatusLastAttemptFailed) { + const remainingRetries = status.remainingRetries; + const coolDownTimeInSec = status.coolDownTimeInSec; + // NOTE: if coolDownTimeInSec is not zero, a countdown timer should be started. + return t('password.passwordProtectionStatusDescriptionUnlocked', { + remainingRetries: remainingRetries, + coolDown: coolDownTimeInSec, + }); + } else if (status instanceof PasswordProtectionStatusLockedOut) { + return t('password.passwordProtectionStatusDescriptionLocked'); + } + return ''; + } + + const onConfirm = async () => { + await confirm(route.params.mode, route.params.handler); + }; + + const onCancel = useCallback(async () => { + await cancel(route.params.mode, route.params.handler); + }, [route.params.mode, route.params.handler]); + + const isChange = route.params.mode === PasswordMode.credentialChange; + const lastRecoverableError = route.params.lastRecoverableError; + const authenticatorProtectionStatus = route.params.authenticatorProtectionStatus; + return ( + + + + + + {title(route.params.mode)} + + + + + {description(route.params.mode)} + + {isChange && ( + + )} + + {lastRecoverableError && ( + + {errorText(lastRecoverableError)} + + )} + {authenticatorProtectionStatus && + !( + authenticatorProtectionStatus instanceof + PasswordProtectionStatusUnlocked + ) && ( + + {authenticatorProtectionText(authenticatorProtectionStatus)} + + )} + + + + + + + + + ); +}; + +export default PasswordScreen; diff --git a/src/screens/PasswordViewModel.ts b/src/screens/PasswordViewModel.ts new file mode 100644 index 0000000..abc1665 --- /dev/null +++ b/src/screens/PasswordViewModel.ts @@ -0,0 +1,85 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { useState } from 'react'; + +import { + PasswordChangeHandler, + PasswordEnrollmentHandler, + PasswordUserVerificationHandler, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { ErrorHandler } from '../error/ErrorHandler'; +import { OperationType } from '../model/OperationType'; +import { PasswordMode } from '../model/PasswordMode'; + +const usePasswordViewModel = () => { + const [oldPassword, setOldPassword] = useState(''); + const [password, setPassword] = useState(''); + + async function confirm( + mode: PasswordMode, + handler?: + | PasswordEnrollmentHandler + | PasswordUserVerificationHandler + | PasswordChangeHandler + ) { + console.log('Confirming entered credentials.'); + switch (mode) { + case PasswordMode.enrollment: + await (handler as PasswordEnrollmentHandler) + .password(password) + .catch(ErrorHandler.handle.bind(null, OperationType.registration)); + break; + case PasswordMode.verification: + await (handler as PasswordUserVerificationHandler) + .verifyPassword(password) + .catch(ErrorHandler.handle.bind(null, OperationType.unknown)); + break; + case PasswordMode.credentialChange: + await (handler as PasswordChangeHandler) + .passwords(oldPassword, password) + .catch(ErrorHandler.handle.bind(null, OperationType.passwordChange)); + break; + } + } + + async function cancel( + mode: PasswordMode, + handler?: + | PasswordEnrollmentHandler + | PasswordUserVerificationHandler + | PasswordChangeHandler + ) { + switch (mode) { + case PasswordMode.enrollment: + console.log('Cancelling password enrollment.'); + await (handler as PasswordEnrollmentHandler) + .cancel() + .catch(ErrorHandler.handle.bind(null, OperationType.registration)); + break; + case PasswordMode.verification: + console.log('Cancelling password verification.'); + await (handler as PasswordUserVerificationHandler) + .cancel() + .catch(ErrorHandler.handle.bind(null, OperationType.unknown)); + break; + case PasswordMode.credentialChange: + console.log('Cancelling password change.'); + await (handler as PasswordChangeHandler) + .cancel() + .catch(ErrorHandler.handle.bind(null, OperationType.passwordChange)); + break; + } + } + + return { + setOldPassword, + setPassword, + confirm, + cancel, + }; +}; + +export default usePasswordViewModel; diff --git a/src/screens/RootStackParamList.ts b/src/screens/RootStackParamList.ts index 2a98ce6..6a75737 100644 --- a/src/screens/RootStackParamList.ts +++ b/src/screens/RootStackParamList.ts @@ -8,6 +8,10 @@ import { BiometricUserVerificationHandler, DevicePasscodeUserVerificationHandler, FingerprintUserVerificationHandler, + PasswordAuthenticatorProtectionStatus, + PasswordChangeHandler, + PasswordEnrollmentHandler, + PasswordUserVerificationHandler, PinAuthenticatorProtectionStatus, PinChangeHandler, PinEnrollmentHandler, @@ -18,6 +22,7 @@ import { import type { AccountItem } from '../model/AccountItem'; import type { AuthenticatorItem } from '../model/AuthenticatorItem'; import type { OperationType } from '../model/OperationType'; +import { PasswordMode } from '../model/PasswordMode'; import type { PinMode } from '../model/PinMode'; export type RootStackParamList = { @@ -40,6 +45,15 @@ export type RootStackParamList = { lastRecoverableError?: RecoverableError; authenticatorProtectionStatus?: PinAuthenticatorProtectionStatus; }; + Password: { + mode: PasswordMode; + handler?: + | PasswordEnrollmentHandler + | PasswordUserVerificationHandler + | PasswordChangeHandler; + lastRecoverableError?: RecoverableError; + authenticatorProtectionStatus?: PasswordAuthenticatorProtectionStatus; + }; DeviceInformationChange: { name: string; }; diff --git a/src/screens/SelectAccountViewModel.ts b/src/screens/SelectAccountViewModel.ts index c858f6f..27a6f19 100644 --- a/src/screens/SelectAccountViewModel.ts +++ b/src/screens/SelectAccountViewModel.ts @@ -17,6 +17,8 @@ import { AuthenticationAuthenticatorSelectorImpl } from '../userInteraction/Auth import { BiometricUserVerifierImpl } from '../userInteraction/BiometricUserVerifierImpl'; import { DevicePasscodeUserVerifierImpl } from '../userInteraction/DevicePasscodeUserVerifierImpl'; import { FingerprintUserVerifierImpl } from '../userInteraction/FingerprintUserVerifierImpl'; +import { PasswordChangerImpl } from '../userInteraction/PasswordChangerImpl'; +import { PasswordUserVerifierImpl } from '../userInteraction/PasswordUserVerifierImpl'; import { PinChangerImpl } from '../userInteraction/PinChangerImpl'; import { PinUserVerifierImpl } from '../userInteraction/PinUserVerifierImpl'; import { AuthorizationUtils } from '../utility/AuthorizationUtils'; @@ -51,6 +53,9 @@ const useSelectAccountViewModel = () => { case OperationType.pinChange: await pinChange(username, operation); break; + case OperationType.passwordChange: + await passwordChange(username, operation); + break; default: await handler?.username(username).catch((error) => { ErrorHandler.handle.bind( @@ -68,6 +73,7 @@ const useSelectAccountViewModel = () => { .username(username) .authenticatorSelector(new AuthenticationAuthenticatorSelectorImpl()) .pinUserVerifier(new PinUserVerifierImpl()) + .passwordUserVerifier(new PasswordUserVerifierImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) .devicePasscodeUserVerifier(new DevicePasscodeUserVerifierImpl()) .fingerprintUserVerifier(new FingerprintUserVerifierImpl()) @@ -141,6 +147,22 @@ const useSelectAccountViewModel = () => { .catch(ErrorHandler.handle.bind(null, operation)); } + async function passwordChange(username: string, operation: OperationType) { + const client = ClientProvider.getInstance().client; + client?.operations.passwordChange + .username(username) + .passwordChanger(new PasswordChangerImpl()) + .onSuccess(() => { + console.log('Password Change succeeded.'); + navigation.navigate('Result', { + operation: operation, + }); + }) + .onError(ErrorHandler.handle.bind(null, OperationType.passwordChange)) + .execute() + .catch(ErrorHandler.handle.bind(null, operation)); + } + return { select, }; diff --git a/src/screens/UsernamePasswordLoginViewModel.ts b/src/screens/UsernamePasswordLoginViewModel.ts index 3df253b..30810ae 100644 --- a/src/screens/UsernamePasswordLoginViewModel.ts +++ b/src/screens/UsernamePasswordLoginViewModel.ts @@ -26,6 +26,7 @@ import { OperationType } from '../model/OperationType'; import { BiometricUserVerifierImpl } from '../userInteraction/BiometricUserVerifierImpl'; import { DevicePasscodeUserVerifierImpl } from '../userInteraction/DevicePasscodeUserVerifierImpl'; import { FingerprintUserVerifierImpl } from '../userInteraction/FingerprintUserVerifierImpl'; +import { PasswordEnrollerImpl } from '../userInteraction/PasswordEnrollerImpl'; import { PinEnrollerImpl } from '../userInteraction/PinEnrollerImpl'; import { RegistrationAuthenticatorSelectorImpl } from '../userInteraction/RegistrationAuthenticatorSelectorImpl'; import { ClientProvider } from '../utility/ClientProvider'; @@ -52,6 +53,7 @@ const useUsernamePasswordLoginViewModel = () => { .authorizationProvider(authorizationProvider) .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) .pinEnroller(new PinEnrollerImpl()) + .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) .devicePasscodeUserVerifier(new DevicePasscodeUserVerifierImpl()) .fingerprintUserVerifier(new FingerprintUserVerifierImpl()) diff --git a/src/userInteraction/OutOfBandOperationHandler.ts b/src/userInteraction/OutOfBandOperationHandler.ts index 0324506..2f20808 100644 --- a/src/userInteraction/OutOfBandOperationHandler.ts +++ b/src/userInteraction/OutOfBandOperationHandler.ts @@ -15,6 +15,8 @@ import { AuthenticationAuthenticatorSelectorImpl } from './AuthenticationAuthent import { BiometricUserVerifierImpl } from './BiometricUserVerifierImpl'; import { DevicePasscodeUserVerifierImpl } from './DevicePasscodeUserVerifierImpl'; import { FingerprintUserVerifierImpl } from './FingerprintUserVerifierImpl'; +import { PasswordEnrollerImpl } from './PasswordEnrollerImpl'; +import { PasswordUserVerifierImpl } from './PasswordUserVerifierImpl'; import { PinEnrollerImpl } from './PinEnrollerImpl'; import { PinUserVerifierImpl } from './PinUserVerifierImpl'; import { RegistrationAuthenticatorSelectorImpl } from './RegistrationAuthenticatorSelectorImpl'; @@ -31,6 +33,7 @@ async function handleRegistration(registration: OutOfBandRegistration) { .deviceInformation(DeviceInformationUtils.create()) .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) .pinEnroller(new PinEnrollerImpl()) + .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) .devicePasscodeUserVerifier(new DevicePasscodeUserVerifierImpl()) .fingerprintUserVerifier(new FingerprintUserVerifierImpl()) @@ -49,6 +52,7 @@ async function handleAuthentication(authentication: OutOfBandAuthentication) { .accountSelector(new AccountSelectorImpl()) .authenticatorSelector(new AuthenticationAuthenticatorSelectorImpl()) .pinUserVerifier(new PinUserVerifierImpl()) + .passwordUserVerifier(new PasswordUserVerifierImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) .devicePasscodeUserVerifier(new DevicePasscodeUserVerifierImpl()) .fingerprintUserVerifier(new FingerprintUserVerifierImpl()) diff --git a/src/userInteraction/PasswordChangerImpl.ts b/src/userInteraction/PasswordChangerImpl.ts new file mode 100644 index 0000000..2fca344 --- /dev/null +++ b/src/userInteraction/PasswordChangerImpl.ts @@ -0,0 +1,33 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { + PasswordChangeContext, + PasswordChangeHandler, + PasswordChanger, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { PasswordPolicyImpl } from './PasswordPolicyImpl'; +import { PasswordMode } from '../model/PasswordMode'; +import * as RootNavigation from '../utility/RootNavigation'; + +export class PasswordChangerImpl extends PasswordChanger { + changePassword(context: PasswordChangeContext, handler: PasswordChangeHandler): void { + console.log( + context.lastRecoverableError + ? 'Password change failed. Please try again.' + : 'Please start password change.' + ); + + RootNavigation.navigate('Password', { + mode: PasswordMode.credentialChange, + handler: handler, + lastRecoverableError: context.lastRecoverableError, + authenticatorProtectionStatus: context.authenticatorProtectionStatus, + }); + } + + // You can add a custom password policy by overriding the `passwordPolicy` getter. + passwordPolicy = new PasswordPolicyImpl(); +} diff --git a/src/userInteraction/PasswordEnrollerImpl.ts b/src/userInteraction/PasswordEnrollerImpl.ts new file mode 100644 index 0000000..5621a8e --- /dev/null +++ b/src/userInteraction/PasswordEnrollerImpl.ts @@ -0,0 +1,36 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { + PasswordEnroller, + PasswordEnrollmentContext, + PasswordEnrollmentHandler, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { PasswordPolicyImpl } from './PasswordPolicyImpl'; +import { PasswordMode } from '../model/PasswordMode'; +import * as RootNavigation from '../utility/RootNavigation'; + +export class PasswordEnrollerImpl extends PasswordEnroller { + enrollPassword(context: PasswordEnrollmentContext, handler: PasswordEnrollmentHandler): void { + console.log( + context.lastRecoverableError + ? 'Password enrollment failed. Please try again.' + : 'Please start password enrollment.' + ); + + RootNavigation.navigate('Password', { + mode: PasswordMode.enrollment, + handler: handler, + lastRecoverableError: context.lastRecoverableError, + }); + } + + onValidCredentialsProvided(): void { + console.log('Valid password credentials provided.'); + } + + // You can add a custom password policy by overriding the `passwordPolicy` getter. + passwordPolicy = new PasswordPolicyImpl(); +} diff --git a/src/userInteraction/PasswordPolicyImpl.ts b/src/userInteraction/PasswordPolicyImpl.ts new file mode 100644 index 0000000..c4558af --- /dev/null +++ b/src/userInteraction/PasswordPolicyImpl.ts @@ -0,0 +1,46 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { + PasswordChangeRecoverableCustomValidationError, + PasswordEnrollmentCustomValidationError, + PasswordPolicy, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +export class PasswordPolicyImpl extends PasswordPolicy { + errorMessage = 'The password must be more than 6 characters.'; + cause = 'The password is too short'; + + validatePasswordForEnrollment( + password: string, + onSuccess: () => void, + onError: (error: PasswordEnrollmentCustomValidationError) => void + ): void { + console.log(`Received password for enrollment is ${password}`); + + this._isValid(password) + ? onSuccess() + : onError(new PasswordEnrollmentCustomValidationError(this.errorMessage, this.cause)); + } + validatePasswordForPasswordChange( + password: string, + onSuccess: () => void, + onError: (error: PasswordChangeRecoverableCustomValidationError) => void + ): void { + console.log(`Received password for change is ${password}`); + + this._isValid(password) + ? onSuccess() + : onError( + new PasswordChangeRecoverableCustomValidationError( + this.errorMessage, + this.cause + ) + ); + } + + _isValid(password: string): boolean { + return password.trim() !== 'password'; + } +} diff --git a/src/userInteraction/PasswordUserVerifierImpl.ts b/src/userInteraction/PasswordUserVerifierImpl.ts new file mode 100644 index 0000000..a4a1b5a --- /dev/null +++ b/src/userInteraction/PasswordUserVerifierImpl.ts @@ -0,0 +1,37 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +import { + PasswordUserVerificationContext, + PasswordUserVerificationHandler, + PasswordUserVerifier, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { PasswordMode } from '../model/PasswordMode'; +import * as RootNavigation from '../utility/RootNavigation'; + +export class PasswordUserVerifierImpl extends PasswordUserVerifier { + verifyPassword( + context: PasswordUserVerificationContext, + handler: PasswordUserVerificationHandler + ): Promise { + console.log( + context.lastRecoverableError + ? 'Password user verification failed. Please try again.' + : 'Please start password user verification.' + ); + + RootNavigation.navigate('Password', { + mode: PasswordMode.verification, + handler: handler, + lastRecoverableError: context.lastRecoverableError, + authenticatorProtectionStatus: context.authenticatorProtectionStatus, + }); + return Promise.resolve(); + } + + onValidCredentialsProvided(): void { + console.log('Valid password credentials provided.'); + } +} diff --git a/src/userInteraction/PinUserVerifierImpl.ts b/src/userInteraction/PinUserVerifierImpl.ts index 68e4339..e9d6369 100644 --- a/src/userInteraction/PinUserVerifierImpl.ts +++ b/src/userInteraction/PinUserVerifierImpl.ts @@ -12,10 +12,6 @@ import { PinMode } from '../model/PinMode'; import * as RootNavigation from '../utility/RootNavigation'; export class PinUserVerifierImpl extends PinUserVerifier { - onValidCredentialsProvided(): void { - console.log('Valid pin credentials provided.'); - } - verifyPin( context: PinUserVerificationContext, handler: PinUserVerificationHandler @@ -34,4 +30,8 @@ export class PinUserVerifierImpl extends PinUserVerifier { }); return Promise.resolve(); } + + onValidCredentialsProvided(): void { + console.log('Valid pin credentials provided.'); + } } diff --git a/src/utility/AuthenticatorUtils.ts b/src/utility/AuthenticatorUtils.ts index 3cde01e..cb9c315 100644 --- a/src/utility/AuthenticatorUtils.ts +++ b/src/utility/AuthenticatorUtils.ts @@ -38,6 +38,8 @@ export class AuthenticatorUtils { return ''; }, })(); + case Aaid.PASSWORD.rawValue(): + return i18next.t('authenticator.title.password'); default: return `Unknown AAID: ${authenticator.aaid}`; } diff --git a/yarn.lock b/yarn.lock index a5210a7..016bf14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5040,7 +5040,7 @@ prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5122,6 +5122,19 @@ react-native-device-info@10.13.2: resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.13.2.tgz#7bfd4eb991c3434b8a2c38224cb1f51f8e799d54" integrity sha512-5EAls7uvGdZkVfp1KWHsR5BfJJHp/ux64+ZPj1865IcaUyrNQIWYFmrTHwTH8L/NGJUTBrzv+y6WODnN17LSbw== +react-native-iphone-x-helper@^1.0.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + +react-native-keyboard-aware-scroll-view@^0.9.5: + version "0.9.5" + resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz#e2e9665d320c188e6b1f22f151b94eb358bf9b71" + integrity sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA== + dependencies: + prop-types "^15.6.2" + react-native-iphone-x-helper "^1.0.3" + react-native-permissions@4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-4.1.5.tgz#db4d1ddbf076570043f4fd4168f54bb6020aec92"