From 04240855df951616b8098709c9037778bb836cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20T=C3=B3th?= Date: Tue, 23 Jul 2024 11:33:18 +0200 Subject: [PATCH] NEVISACCESSAPP-6059: introduce authenticator allowlist - Added allowed list of authenticators in the configuration files. - Added fromJson functions for configuration related classes that are used for deserialization. - Added AuthenticatorValidator. - Added asyncFilter function. --- assets/config_authentication_cloud.json | 14 +- assets/config_identity_suite.json | 14 +- src/configuration/AppConfiguration.ts | 36 ++++- src/configuration/ConfigurationLoader.ts | 4 +- .../AuthCloudApiRegistrationViewModel.ts | 9 +- src/screens/SelectAccountViewModel.ts | 9 +- src/screens/UsernamePasswordLoginViewModel.ts | 9 +- ...AuthenticationAuthenticatorSelectorImpl.ts | 45 ------ .../AuthenticatorSelectorImpl.ts | 70 +++++++++ .../OutOfBandOperationHandler.ts | 14 +- .../RegistrationAuthenticatorSelectorImpl.ts | 102 ------------- src/utility/AsyncUtils.ts | 20 +++ src/utility/AuthenticatorUtils.ts | 15 ++ .../validation/AuthenticatorValidator.ts | 136 ++++++++++++++++++ 14 files changed, 334 insertions(+), 163 deletions(-) delete mode 100644 src/userInteraction/AuthenticationAuthenticatorSelectorImpl.ts create mode 100644 src/userInteraction/AuthenticatorSelectorImpl.ts delete mode 100644 src/userInteraction/RegistrationAuthenticatorSelectorImpl.ts create mode 100644 src/utility/AsyncUtils.ts create mode 100644 src/utility/validation/AuthenticatorValidator.ts diff --git a/assets/config_authentication_cloud.json b/assets/config_authentication_cloud.json index 3adbf56..8b0e8e3 100644 --- a/assets/config_authentication_cloud.json +++ b/assets/config_authentication_cloud.json @@ -2,5 +2,17 @@ "sdk": { "hostname": "myinstance.mauth.nevis.cloud", "facetId": "android:apk-key-hash:ch.nevis.mobile.authentication.sdk.react.example" - } + }, + "authenticatorAllowlist": [ + "F1D0#0001", + "F1D0#0002", + "F1D0#0003", + "F1D0#0004", + "F1D0#0005", + "F1D0#1001", + "F1D0#1002", + "F1D0#1003", + "F1D0#1004", + "F1D0#1005" + ] } diff --git a/assets/config_identity_suite.json b/assets/config_identity_suite.json index 18cf2aa..8ce8f6f 100644 --- a/assets/config_identity_suite.json +++ b/assets/config_identity_suite.json @@ -9,5 +9,17 @@ "authenticationResponsePath": "/auth/fidouaf/authenticationresponse/", "deregistrationRequestPath": "/nevisfido/uaf/1.1/request/deregistration/", "dispatchTargetResourcePath": "/nevisfido/token/dispatch/targets/" - } + }, + "authenticatorAllowlist": [ + "F1D0#1001", + "F1D0#1002", + "F1D0#1003", + "F1D0#1004", + "F1D0#1005", + "F1D0#1001", + "F1D0#1002", + "F1D0#1003", + "F1D0#1004", + "F1D0#1005" + ] } diff --git a/src/configuration/AppConfiguration.ts b/src/configuration/AppConfiguration.ts index d9a7ef6..ec733de 100644 --- a/src/configuration/AppConfiguration.ts +++ b/src/configuration/AppConfiguration.ts @@ -1,6 +1,9 @@ /** * Copyright © 2023 Nevis Security AG. All rights reserved. */ +import { Aaid } from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { AuthenticatorUtils } from '../utility/AuthenticatorUtils'; export class SdkConfiguration { baseUrl: string; @@ -13,7 +16,7 @@ export class SdkConfiguration { deregistrationRequestPath?: string; dispatchTargetResourcePath?: string; - constructor( + private constructor( baseUrl: string, hostname: string, facetId: string, @@ -34,13 +37,42 @@ export class SdkConfiguration { this.deregistrationRequestPath = deregistrationRequestPath; this.dispatchTargetResourcePath = dispatchTargetResourcePath; } + + static fromJson(json: any): SdkConfiguration { + return new SdkConfiguration( + json.baseUrl, + json.hostname, + json.facetId, + json.registrationRequestPath, + json.registrationResponsePath, + json.authenticationRequestPath, + json.authenticationResponsePath, + json.deregistrationRequestPath, + json.dispatchTargetResourcePath + ); + } } export class AppConfiguration { sdk: SdkConfiguration; loginRequestURL?: string; + authenticatorAllowlist: Array; - constructor(sdk: SdkConfiguration, loginRequestURL?: string) { + private constructor( + sdk: SdkConfiguration, + authenticatorAllowList: Array, + loginRequestURL?: string + ) { this.sdk = sdk; + this.authenticatorAllowlist = authenticatorAllowList; this.loginRequestURL = loginRequestURL; } + + static fromJson(json: any): AppConfiguration { + const sdk = SdkConfiguration.fromJson(json.sdk); + const data = json.authenticatorAllowlist; + const authenticatorAllowlist = data.flatMap((allowedAuthenticator: string) => { + return AuthenticatorUtils.getAaidFromRawValue(allowedAuthenticator) || []; + }); + return new AppConfiguration(sdk, authenticatorAllowlist, json.loginRequestURL); + } } diff --git a/src/configuration/ConfigurationLoader.ts b/src/configuration/ConfigurationLoader.ts index aa3f993..8c8e42e 100644 --- a/src/configuration/ConfigurationLoader.ts +++ b/src/configuration/ConfigurationLoader.ts @@ -43,8 +43,8 @@ export class ConfigurationLoader { return this._appConfiguration; } - const json = this.configJson(); - this._appConfiguration = Object.assign(AppConfiguration.prototype, json); + this._appConfiguration = AppConfiguration.fromJson(this.configJson()); + if (!this._appConfiguration) { ErrorHandler.handle( OperationType.unknown, diff --git a/src/screens/AuthCloudApiRegistrationViewModel.ts b/src/screens/AuthCloudApiRegistrationViewModel.ts index eecb0d3..c1e2b22 100644 --- a/src/screens/AuthCloudApiRegistrationViewModel.ts +++ b/src/screens/AuthCloudApiRegistrationViewModel.ts @@ -10,12 +10,15 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from './RootStackParamList'; import { ErrorHandler } from '../error/ErrorHandler'; import { OperationType } from '../model/OperationType'; +import { + AuthenticatorSelectorImpl, + AuthenticatorSelectorOperation, +} from '../userInteraction/AuthenticatorSelectorImpl'; 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'; import { DeviceInformationUtils } from '../utility/DeviceInformationUtils'; @@ -29,7 +32,9 @@ const useAuthCloudApiRegistrationViewModel = () => { const client = ClientProvider.getInstance().client; const authCloudApiRegistration = client?.operations.authCloudApiRegistration .deviceInformation(DeviceInformationUtils.create()) - .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) + .authenticatorSelector( + new AuthenticatorSelectorImpl(AuthenticatorSelectorOperation.registration) + ) .pinEnroller(new PinEnrollerImpl()) .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) diff --git a/src/screens/SelectAccountViewModel.ts b/src/screens/SelectAccountViewModel.ts index 27a6f19..829f5fc 100644 --- a/src/screens/SelectAccountViewModel.ts +++ b/src/screens/SelectAccountViewModel.ts @@ -13,7 +13,10 @@ import type { RootStackParamList } from './RootStackParamList'; import { AppErrorAuthorizationProviderNotFound, AppErrorUnknownError } from '../error/AppError'; import { ErrorHandler } from '../error/ErrorHandler'; import { OperationType } from '../model/OperationType'; -import { AuthenticationAuthenticatorSelectorImpl } from '../userInteraction/AuthenticationAuthenticatorSelectorImpl'; +import { + AuthenticatorSelectorImpl, + AuthenticatorSelectorOperation, +} from '../userInteraction/AuthenticatorSelectorImpl'; import { BiometricUserVerifierImpl } from '../userInteraction/BiometricUserVerifierImpl'; import { DevicePasscodeUserVerifierImpl } from '../userInteraction/DevicePasscodeUserVerifierImpl'; import { FingerprintUserVerifierImpl } from '../userInteraction/FingerprintUserVerifierImpl'; @@ -71,7 +74,9 @@ const useSelectAccountViewModel = () => { const client = ClientProvider.getInstance().client; client?.operations.authentication .username(username) - .authenticatorSelector(new AuthenticationAuthenticatorSelectorImpl()) + .authenticatorSelector( + new AuthenticatorSelectorImpl(AuthenticatorSelectorOperation.authentication) + ) .pinUserVerifier(new PinUserVerifierImpl()) .passwordUserVerifier(new PasswordUserVerifierImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) diff --git a/src/screens/UsernamePasswordLoginViewModel.ts b/src/screens/UsernamePasswordLoginViewModel.ts index 30810ae..8fff52d 100644 --- a/src/screens/UsernamePasswordLoginViewModel.ts +++ b/src/screens/UsernamePasswordLoginViewModel.ts @@ -23,12 +23,15 @@ import { } from '../error/AppError'; import { ErrorHandler } from '../error/ErrorHandler'; import { OperationType } from '../model/OperationType'; +import { + AuthenticatorSelectorImpl, + AuthenticatorSelectorOperation, +} from '../userInteraction/AuthenticatorSelectorImpl'; 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'; import { DeviceInformationUtils } from '../utility/DeviceInformationUtils'; @@ -51,7 +54,9 @@ const useUsernamePasswordLoginViewModel = () => { .username(usernameToRegister) .deviceInformation(DeviceInformationUtils.create()) .authorizationProvider(authorizationProvider) - .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) + .authenticatorSelector( + new AuthenticatorSelectorImpl(AuthenticatorSelectorOperation.registration) + ) .pinEnroller(new PinEnrollerImpl()) .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) diff --git a/src/userInteraction/AuthenticationAuthenticatorSelectorImpl.ts b/src/userInteraction/AuthenticationAuthenticatorSelectorImpl.ts deleted file mode 100644 index d3d9788..0000000 --- a/src/userInteraction/AuthenticationAuthenticatorSelectorImpl.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2023 Nevis Security AG. All rights reserved. - */ - -import { - AuthenticatorSelectionContext, - AuthenticatorSelectionHandler, - AuthenticatorSelector, -} from '@nevis-security/nevis-mobile-authentication-sdk-react'; - -import { AuthenticatorItem } from '../model/AuthenticatorItem'; -import * as RootNavigation from '../utility/RootNavigation'; - -export class AuthenticationAuthenticatorSelectorImpl extends AuthenticatorSelector { - constructor() { - super(); - } - - async selectAuthenticator( - context: AuthenticatorSelectionContext, - handler: AuthenticatorSelectionHandler - ): Promise { - console.log('Please select one of the received available authenticators!'); - const username = context.account.username; - const authenticators = context.authenticators.filter((a) => { - // Do not display: - // - non-registered authenticators - // - not hardware supported authenticators - return a.registration.isRegistered(username) && a.isSupportedByHardware; - }); - - const items: AuthenticatorItem[] = []; - for (const authenticator of authenticators) { - items.push( - new AuthenticatorItem( - authenticator, - await context.isPolicyCompliant(authenticator.aaid), - authenticator.userEnrollment.isEnrolled(username) - ) - ); - } - - RootNavigation.navigate('SelectAuthenticator', { items: items, handler: handler }); - } -} diff --git a/src/userInteraction/AuthenticatorSelectorImpl.ts b/src/userInteraction/AuthenticatorSelectorImpl.ts new file mode 100644 index 0000000..4304508 --- /dev/null +++ b/src/userInteraction/AuthenticatorSelectorImpl.ts @@ -0,0 +1,70 @@ +/** + * Copyright © 2023 Nevis Security AG. All rights reserved. + */ + +import { + Authenticator, + AuthenticatorSelectionContext, + AuthenticatorSelectionHandler, + AuthenticatorSelector, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import { ConfigurationLoader } from '../configuration/ConfigurationLoader'; +import { AuthenticatorItem } from '../model/AuthenticatorItem'; +import * as RootNavigation from '../utility/RootNavigation'; +import { AuthenticatorValidator } from '../utility/validation/AuthenticatorValidator'; + +export enum AuthenticatorSelectorOperation { + registration, + authentication, +} + +export class AuthenticatorSelectorImpl extends AuthenticatorSelector { + operation: AuthenticatorSelectorOperation; + constructor(operation: AuthenticatorSelectorOperation) { + super(); + this.operation = operation; + } + + async selectAuthenticator( + context: AuthenticatorSelectionContext, + handler: AuthenticatorSelectionHandler + ): Promise { + console.log('Please select one of the received available authenticators!'); + const configuration = ConfigurationLoader.getInstance().appConfiguration; + const username = context.account.username; + let authenticators: Array = []; + switch (this.operation) { + case AuthenticatorSelectorOperation.registration: + authenticators = await AuthenticatorValidator.validateForRegistration( + context, + configuration.authenticatorAllowlist + ); + break; + case AuthenticatorSelectorOperation.authentication: + authenticators = AuthenticatorValidator.validateForAuthentication( + context, + configuration.authenticatorAllowlist + ); + break; + } + + if (authenticators.length === 0) { + console.log('No available authenticators found. Cancelling authenticator selection.'); + return handler.cancel(); + } + + const items: AuthenticatorItem[] = []; + for (const authenticator of authenticators) { + items.push( + new AuthenticatorItem( + authenticator, + await context.isPolicyCompliant(authenticator.aaid), + authenticator.userEnrollment.isEnrolled(username) + ) + ); + } + + RootNavigation.navigate('SelectAuthenticator', { items: items, handler: handler }); + } +} diff --git a/src/userInteraction/OutOfBandOperationHandler.ts b/src/userInteraction/OutOfBandOperationHandler.ts index 2f20808..ff4d160 100644 --- a/src/userInteraction/OutOfBandOperationHandler.ts +++ b/src/userInteraction/OutOfBandOperationHandler.ts @@ -11,7 +11,10 @@ import { } from '@nevis-security/nevis-mobile-authentication-sdk-react'; import { AccountSelectorImpl } from './AccountSelectorImpl'; -import { AuthenticationAuthenticatorSelectorImpl } from './AuthenticationAuthenticatorSelectorImpl'; +import { + AuthenticatorSelectorImpl, + AuthenticatorSelectorOperation, +} from './AuthenticatorSelectorImpl'; import { BiometricUserVerifierImpl } from './BiometricUserVerifierImpl'; import { DevicePasscodeUserVerifierImpl } from './DevicePasscodeUserVerifierImpl'; import { FingerprintUserVerifierImpl } from './FingerprintUserVerifierImpl'; @@ -19,7 +22,6 @@ import { PasswordEnrollerImpl } from './PasswordEnrollerImpl'; import { PasswordUserVerifierImpl } from './PasswordUserVerifierImpl'; import { PinEnrollerImpl } from './PinEnrollerImpl'; import { PinUserVerifierImpl } from './PinUserVerifierImpl'; -import { RegistrationAuthenticatorSelectorImpl } from './RegistrationAuthenticatorSelectorImpl'; import { AppErrorPayloadDecodeError, AppErrorQrCodeError } from '../error/AppError'; import { ErrorHandler } from '../error/ErrorHandler'; import { OperationType } from '../model/OperationType'; @@ -31,7 +33,9 @@ import * as RootNavigation from '../utility/RootNavigation'; async function handleRegistration(registration: OutOfBandRegistration) { await registration .deviceInformation(DeviceInformationUtils.create()) - .authenticatorSelector(new RegistrationAuthenticatorSelectorImpl()) + .authenticatorSelector( + new AuthenticatorSelectorImpl(AuthenticatorSelectorOperation.registration) + ) .pinEnroller(new PinEnrollerImpl()) .passwordEnroller(new PasswordEnrollerImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) @@ -50,7 +54,9 @@ async function handleRegistration(registration: OutOfBandRegistration) { async function handleAuthentication(authentication: OutOfBandAuthentication) { await authentication .accountSelector(new AccountSelectorImpl()) - .authenticatorSelector(new AuthenticationAuthenticatorSelectorImpl()) + .authenticatorSelector( + new AuthenticatorSelectorImpl(AuthenticatorSelectorOperation.authentication) + ) .pinUserVerifier(new PinUserVerifierImpl()) .passwordUserVerifier(new PasswordUserVerifierImpl()) .biometricUserVerifier(new BiometricUserVerifierImpl()) diff --git a/src/userInteraction/RegistrationAuthenticatorSelectorImpl.ts b/src/userInteraction/RegistrationAuthenticatorSelectorImpl.ts deleted file mode 100644 index f3a18b9..0000000 --- a/src/userInteraction/RegistrationAuthenticatorSelectorImpl.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright © 2023 Nevis Security AG. All rights reserved. - */ - -import { Platform } from 'react-native'; - -import { - Aaid, - Authenticator, - AuthenticatorSelectionContext, - AuthenticatorSelectionHandler, - AuthenticatorSelector, -} from '@nevis-security/nevis-mobile-authentication-sdk-react'; - -import { AuthenticatorItem } from '../model/AuthenticatorItem'; -import * as RootNavigation from '../utility/RootNavigation'; - -export class RegistrationAuthenticatorSelectorImpl extends AuthenticatorSelector { - constructor() { - super(); - } - - async selectAuthenticator( - context: AuthenticatorSelectionContext, - handler: AuthenticatorSelectionHandler - ): Promise { - console.log('Please select one of the received available authenticators!'); - const authenticators: Authenticator[] = []; - for (const authenticator of context.authenticators) { - const mustDisplay = await this.mustDisplayForRegistration(authenticator, context); - if (mustDisplay) { - authenticators.push(authenticator); - } - } - - const items: AuthenticatorItem[] = []; - for (const authenticator of authenticators) { - items.push( - new AuthenticatorItem( - authenticator, - await context.isPolicyCompliant(authenticator.aaid), - authenticator.userEnrollment.isEnrolled(context.account.username) - ) - ); - } - - RootNavigation.navigate('SelectAuthenticator', { items: items, handler: handler }); - } - - async mustDisplayForRegistration( - authenticator: Authenticator, - context: AuthenticatorSelectionContext - ): Promise { - if (Platform.OS === 'android') { - const biometricsRegistered = context.authenticators.filter((contextAuthenticator) => { - return ( - contextAuthenticator.aaid === Aaid.BIOMETRIC.rawValue() && - contextAuthenticator.registration.isRegistered(context.account.username) - ); - }); - - const biometricsAvailable: Authenticator[] = []; - const fingerprintAvailable: Authenticator[] = []; - for (const contextAuthenticator of context.authenticators) { - const isBiometricAvailable = - contextAuthenticator.aaid === Aaid.BIOMETRIC.rawValue() && - contextAuthenticator.isSupportedByHardware && - (await context.isPolicyCompliant(contextAuthenticator.aaid)); - if (isBiometricAvailable) { - biometricsAvailable.push(contextAuthenticator); - } - - const isFingerprintAvailable = - contextAuthenticator.aaid === Aaid.FINGERPRINT.rawValue() && - contextAuthenticator.isSupportedByHardware && - (await context.isPolicyCompliant(contextAuthenticator.aaid)); - if (isFingerprintAvailable) { - fingerprintAvailable.push(contextAuthenticator); - } - } - - // If biometric can be registered (or is already registered), or if we - // cannot register fingerprint, do not propose to register fingerprint - // (we favor biometric over fingerprint). - if ( - (biometricsRegistered.length > 0 || - biometricsAvailable.length > 0 || - fingerprintAvailable.length === 0) && - authenticator.aaid === Aaid.FINGERPRINT.rawValue() - ) { - console.log(`Return false`); - return false; - } - } - - // Do not display: - // - policy non-compliant authenticators (this includes already registered authenticators) - // - not hardware supported authenticators - const isPolicyCompliant = await context.isPolicyCompliant(authenticator.aaid); - return authenticator.isSupportedByHardware && isPolicyCompliant; - } -} diff --git a/src/utility/AsyncUtils.ts b/src/utility/AsyncUtils.ts new file mode 100644 index 0000000..f229830 --- /dev/null +++ b/src/utility/AsyncUtils.ts @@ -0,0 +1,20 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ + +class AsyncUtils { + /** + * An async version of the filter function. + * @param arr The array that needs to be filtered. + * @param predicate The predicate which is an async function that is used to do the filtering. + */ + static readonly asyncFilter = async ( + arr: any[], + predicate: (authenticator: any) => Promise + ) => + Promise.all(arr.map(predicate)).then((results) => + arr.filter((_v: any, index: number) => results[index]) + ); +} + +export default AsyncUtils; diff --git a/src/utility/AuthenticatorUtils.ts b/src/utility/AuthenticatorUtils.ts index cb9c315..1fe5eb7 100644 --- a/src/utility/AuthenticatorUtils.ts +++ b/src/utility/AuthenticatorUtils.ts @@ -44,4 +44,19 @@ export class AuthenticatorUtils { return `Unknown AAID: ${authenticator.aaid}`; } } + + static getAaidFromRawValue(aaidString: string): Aaid | undefined { + switch (aaidString) { + case Aaid.PIN.rawValue(): + return Aaid.PIN; + case Aaid.FINGERPRINT.rawValue(): + return Aaid.FINGERPRINT; + case Aaid.BIOMETRIC.rawValue(): + return Aaid.BIOMETRIC; + case Aaid.DEVICE_PASSCODE.rawValue(): + return Aaid.DEVICE_PASSCODE; + case Aaid.PASSWORD.rawValue(): + return Aaid.PASSWORD; + } + } } diff --git a/src/utility/validation/AuthenticatorValidator.ts b/src/utility/validation/AuthenticatorValidator.ts new file mode 100644 index 0000000..f2b034a --- /dev/null +++ b/src/utility/validation/AuthenticatorValidator.ts @@ -0,0 +1,136 @@ +/** + * Copyright © 2024 Nevis Security AG. All rights reserved. + */ +import { Platform } from 'react-native'; + +import { + Aaid, + Authenticator, + AuthenticatorSelectionContext, +} from '@nevis-security/nevis-mobile-authentication-sdk-react'; + +import AsyncUtils from '../AsyncUtils'; +import { AuthenticatorUtils } from '../AuthenticatorUtils'; + +export abstract class AuthenticatorValidator { + /** + * Validates authenticators for registration. + * @param context The context holding the accounts to validate. + * @param allowlistedAuthenticators The array holding the allowlisted authenticators. + * @returns The array of allowed authenticators. + */ + static async validateForRegistration( + context: AuthenticatorSelectionContext, + allowlistedAuthenticators: Array + ): Promise> { + const allowedAuthenticators: Array = + AuthenticatorValidatorImpl.allowedAuthenticators(context, allowlistedAuthenticators); + + return AsyncUtils.asyncFilter( + allowedAuthenticators, + async (authenticator: Authenticator) => { + // Do not display: + // - policy non-compliant authenticators (this includes already registered authenticators) + // - not hardware supported authenticators + // - prefer Biometrics authenticator on Android + const isSupportedByHardware = authenticator.isSupportedByHardware; + const isPolicyCompliant = await context.isPolicyCompliant(authenticator.aaid); + const filterAndroidIfNecessary = + await AuthenticatorValidatorImpl.filterAndroidFingerprintIfNecessary( + context, + authenticator + ); + return isSupportedByHardware && isPolicyCompliant && filterAndroidIfNecessary; + } + ); + } + + /** + * Validates authenticators for authentication. + * @param context The context holding the accounts to validate. + * @param allowlistedAuthenticators The array holding the allowlisted authenticators. + * @returns The array of allowed authenticators. + */ + static validateForAuthentication( + context: AuthenticatorSelectionContext, + allowlistedAuthenticators: Array + ): Array { + return AuthenticatorValidatorImpl.allowedAuthenticators( + context, + allowlistedAuthenticators + ).filter((authenticator) => { + // Do not display: + // - non-registered authenticators + // - not hardware supported authenticators + return ( + authenticator.isSupportedByHardware && + authenticator.registration.isRegistered(context.account.username) + ); + }); + } +} + +export class AuthenticatorValidatorImpl extends AuthenticatorValidator { + /** + * Filters out non-allowlisted authenticators. + * @param context The context holding the accounts to validate. + * @param allowlistedAuthenticators The array holding the allowlisted authenticators. + * @returns The array of allowed authenticators. + */ + static allowedAuthenticators( + context: AuthenticatorSelectionContext, + allowlistedAuthenticators: Array + ): Array { + return context.authenticators.filter((authenticator) => { + const authenticatorAaid = AuthenticatorUtils.getAaidFromRawValue(authenticator.aaid); + if (authenticatorAaid === undefined) { + return false; + } + return allowlistedAuthenticators.some((allowlistedAuthenticator) => { + return allowlistedAuthenticator.rawValue() === authenticatorAaid.rawValue(); + }); + }); + } + + static async filterAndroidFingerprintIfNecessary( + context: AuthenticatorSelectionContext, + authenticator: Authenticator + ): Promise { + if (Platform.OS === 'ios' || authenticator.aaid !== Aaid.FINGERPRINT.rawValue()) { + return true; + } + + let isBiometricsRegistered: boolean = false; + let canRegisterBiometrics: boolean = false; + let canRegisterFingerprint: boolean = false; + for (const contextAuthenticator of context.authenticators) { + if ( + contextAuthenticator.aaid === Aaid.BIOMETRIC.rawValue() && + contextAuthenticator.registration.isRegistered(context.account.username) + ) { + isBiometricsRegistered = true; + } + + if ( + contextAuthenticator.aaid === Aaid.BIOMETRIC.rawValue() && + contextAuthenticator.isSupportedByHardware && + (await context.isPolicyCompliant(contextAuthenticator.aaid)) + ) { + canRegisterBiometrics = true; + } + + if ( + contextAuthenticator.aaid === Aaid.FINGERPRINT.rawValue() && + contextAuthenticator.isSupportedByHardware && + (await context.isPolicyCompliant(contextAuthenticator.aaid)) + ) { + canRegisterFingerprint = true; + } + } + + // If biometric can be registered (or is already registered), or if we + // cannot register fingerprint, do not propose to register fingerprint + // (we favor biometric over fingerprint). + return !isBiometricsRegistered && !canRegisterBiometrics && canRegisterFingerprint; + } +}