UNPKG

@phemium-costaisa/fingerprint-auth

Version:

Automatic plugin to work with FaceID and TouchID authentication

698 lines (685 loc) 29.1 kB
import { __awaiter } from 'tslib'; import { Injectable, InjectionToken, Optional, Inject, Pipe, APP_INITIALIZER, NgModule, Component, ChangeDetectionStrategy } from '@angular/core'; import { FingerprintAIO } from '@ionic-native/fingerprint-aio/ngx'; import { AndroidFingerprintAuth } from '@ionic-native/android-fingerprint-auth/ngx'; import { TouchID } from '@ionic-native/touch-id/ngx'; import { Keychain } from '@ionic-native/keychain/ngx'; import { Platform, NavController, IonicModule } from '@ionic/angular'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { forkJoin, from, of } from 'rxjs'; import { tap, map, switchMap } from 'rxjs/operators'; import { Router, ActivatedRoute, RouterModule } from '@angular/router'; import { Storage } from '@ionic/storage-angular'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; class StorageService { constructor() { this._storage = null; this.init(); } init() { return __awaiter(this, void 0, void 0, function* () { const storageConfig = { name: '_biometricstorage', storeName: '_biometrickv', dbKey: '_biometrickey' }; const myStorage = new Storage(storageConfig); this._storage = yield myStorage.create(); }); } set(key, value) { var _a; return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.set(key, value); } get(key) { var _a; return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.get(key); } remove(key) { return this._storage.remove(key); } clear() { return this._storage.clear(); } keys() { return this._storage.keys(); } } StorageService.decorators = [ { type: Injectable } ]; StorageService.ctorParameters = () => []; /** InjectionToken for Global Configuration */ const Config = new InjectionToken('PLUGIN_CONFIGURATION'); var Biometric; (function (Biometric) { Biometric["Face"] = "face"; Biometric["Fingerprint"] = "finger"; Biometric["Common"] = "biometric"; })(Biometric || (Biometric = {})); class FingerprintService { constructor(platform, faio, androidFingerprintAuth, touchId, keychain, router, storage, translateService, config) { this.platform = platform; this.faio = faio; this.androidFingerprintAuth = androidFingerprintAuth; this.touchId = touchId; this.keychain = keychain; this.router = router; this.storage = storage; this.translateService = translateService; this.config = config; } /** * Use this method in Login page to check if user * should be redirected to the Biometric Activator */ checkIfNeedsBiometric(user) { return __awaiter(this, void 0, void 0, function* () { if (!this.isPlatformMobile()) { this.config.debug && console.log('[BiometricService]', 'isPlatformMobile():', this.isPlatformMobile()); return false; } // Check FaceID enabled const biometricFaceActive = yield this.isBiometricActive(user, 'face'); this.config.debug && console.log('[BiometricService]', 'biometricFaceActive:', biometricFaceActive); // Check TouchID enabled const biometricTouchActive = yield this.isBiometricActive(user, 'touch'); this.config.debug && console.log('[BiometricService]', 'biometricTouchActive:', biometricTouchActive); // Check if device has some biometric capability const biometricCapabilities = yield this.retrieveDeviceBiometrics(); this.config.debug && console.log('[BiometricService]', 'biometricCapabilities:', biometricCapabilities); const isBiometricCapable = biometricCapabilities.face || biometricCapabilities.touch; // Redirect to Biometric Activator if conditions are met return (biometricFaceActive === null && biometricTouchActive === null && isBiometricCapable); }); } /** * Use in Login page to open Biometric prompt * @param user string */ showBiometricPrompt(user) { return __awaiter(this, void 0, void 0, function* () { const face = yield this.showFingerprintId(user, 'face').toPromise(); const touch = yield this.showFingerprintId(user, 'touch').toPromise(); // Here we have a big dilemma // In case the user device is capable of FaceID and TouchID, // for the moment we will mark `face` as preferent, // as iOS the mostly one with FaceID if (face) { // Login with FaceID const password = yield this.launchFaceID(user); if (password) { return { user, password }; } else { throw 'Something went wrong in launchFaceID'; } } else if (touch) { // Login with TouchID const password = yield this.launchTouchID(user); if (password) { return { user, password }; } else { throw 'Something went wrong in launchTouchID'; } } else { throw 'Looks like we have an error in our code.'; } }); } _iosShowTouchPrompt() { return __awaiter(this, void 0, void 0, function* () { // As found out, iOS devices with touch capability // needs to use TouchID plugin with text fallback function // otherwise the device will save the fingerprint without prompting the user try { return yield this.touchId.verifyFingerprintWithCustomPasswordFallback('Scan your fingerprint please'); } catch (err) { // -3 code means the user used the Use Password fallback, which is ok for us if ((err === null || err === void 0 ? void 0 : err.code) !== -3) { throw err; } return true; } }); } _sanitizeUser(user) { return `${user}`.toLowerCase(); } _getStorageKey(user, biometricType) { user = this._sanitizeUser(user); return `biometricLoginActive_${biometricType}_${user}`; } _getStorageToken(user, biometricType) { user = this._sanitizeUser(user); return `token_${biometricType}_${user}`; } getLang() { var _a; return ((_a = this.translateService) === null || _a === void 0 ? void 0 : _a.currentLang) || 'es'; } isBiometricActive(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { return yield this.storage.get(this._getStorageKey(user, biometricType)); }); } isPlatformMobile() { return (this.platform.is('ios') || this.platform.is('ipad') || this.platform.is('iphone') || this.platform.is('android')); } clearBiometricData(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { yield this.storage.remove(this._getStorageKey(user, biometricType)); yield this.storage.remove(this._getStorageToken(user, biometricType)); if (this._isIOS()) { yield this.keychain.remove(this._getStorageToken(user, biometricType)); } }); } disableBiometricData(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { yield this.storage.set(this._getStorageKey(user, biometricType), false); yield this.storage.remove(this._getStorageToken(user, biometricType)); if (this._isIOS()) { yield this.keychain.remove(this._getStorageToken(user, biometricType)); } }); } showFingerprintId(user, biometricType) { this.config.debug && console.log('[BiometricService]', 'showFingerPrintId with type:', biometricType); this.config.debug && console.log('[BiometricService]', 'Is IOS:', this._isIOS()); // Retrieve the getter for iOS or Android const tokenPromise = this._isIOS() ? this.keychain.get(this._getStorageToken(user, biometricType)) : this.storage.get(this._getStorageToken(user, biometricType)); this.config.debug && console.log('[BiometricService]', 'Token Promise:', tokenPromise); // User user to retrieve status of biometric and check if it has token return forkJoin([ from(this.storage.get(this._getStorageKey(user, biometricType))), from(tokenPromise), ]).pipe(tap(([active, token]) => this.config.debug && console.log('[BiometricService]', 'Biometric result |', 'Active:', active, 'Token:', token)), map(([active, token]) => !!active && !!token)); } /** * Use in Login page to show the Biometric Activator page * @param user User * @param password Password * @param callbackUrl URL to return after success or fail */ showBiometricActivator(user, password, callbackUrl) { this.router.navigate(['biometric-activator'], { queryParams: { user, password, callbackUrl }, }); } /** * Checks and returns which biometrics methods are available * for the current device * @returns Promise<Biometric[]> */ retrieveDeviceBiometrics() { return __awaiter(this, void 0, void 0, function* () { yield this.platform.ready(); let iosResult = ''; let androidResult = null; if (this._isIOS()) { try { iosResult = yield this.touchId.isAvailable(); } catch (err) { } } else { try { androidResult = yield this.androidFingerprintAuth.isAvailable(); } catch (err) { } } return { face: iosResult === 'face', touch: iosResult === 'touch' || (androidResult === null || androidResult === void 0 ? void 0 : androidResult.isAvailable) || false, }; }); } _isIOS() { return (this.platform.is('ios') || this.platform.is('ipad') || this.platform.is('iphone')); } /** * Stores the credentials used for biometrics * @param user Client user * @param password Client password */ _storePassword(user, password, biometricType) { return __awaiter(this, void 0, void 0, function* () { // Save user for later use in Login if (this._isIOS()) { // IOS - Use Keychain const biometric = yield this.touchId.isAvailable(); // Set Biometric login in storage yield this.storage.set(this._getStorageKey(user, biometricType), true); // We check for `touch` or `face`, although is irrelevant // as the encryption prompt as already been shown if (biometric == 'touch' || biometric == 'face') { if (biometric == 'touch') { yield this._iosShowTouchPrompt(); } // Although we define the prefix "token_", it doesn't actually contain a token, // it will contain the password in the encrypted iOS storage yield this.keychain.set(this._getStorageToken(user, biometricType), password, false); } } else if (this.platform.is('android')) { // Android - Use AndroidFingerprintAuth const fingerprint = yield this.androidFingerprintAuth.isAvailable(); if (fingerprint.isAvailable) { // Fingerprint is available, encrypt! let fingerprintResult = yield this.androidFingerprintAuth.encrypt({ clientId: user, username: user, password: password, locale: this.getLang(), disableBackup: true, }); if (fingerprintResult.withFingerprint) { // User used fingerprint to decrypt password this.storage.set(this._getStorageKey(user, biometricType), true); this.storage.set(this._getStorageToken(user, biometricType), fingerprintResult.token); this.config.debug && console.log(`Token (${user}): ${fingerprintResult.token}`); } else if (fingerprintResult.withBackup) { // DEPRECATED: Disabled "Use backup" option from encrypt prompt as it is useless // console.log('Successfully authenticated with backup password!'); } else { this.config.debug && console.log("Didn't authenticate!"); } } } }); } /** * Shows the FaceID dialog to the user * @param user Client user * @returns Promise<void> */ activateFaceID(user, password) { return __awaiter(this, void 0, void 0, function* () { const biometricConfig = { cancelButtonTitle: 'Cancelar', description: 'Activar FaceID', disableBackup: true, title: 'Activate', fallbackButtonTitle: 'Back Button', subtitle: 'Act', }; try { // Show FaceID dialog to user yield this.faio.show(biometricConfig); } catch (err) { console.log('Match not found'); console.log(err); } // Save encrypted password yield this._storePassword(user, password, 'face'); }); } /** * Shows the TouchID dialog to the user * @param user Client user * @param password Client password */ activateTouchID(user, password) { return this._storePassword(user, password, 'touch'); } /** * Executes an attempt to check for the client fingerprint * Used mainly in Login * @returns Promise<void> */ launchTouchID(user) { return __awaiter(this, void 0, void 0, function* () { if (this._isIOS()) { yield this.touchId.isAvailable(); // Will reject if not available yield this._iosShowTouchPrompt(); // Will reject if fingerprint was failed const password = (yield this.keychain.get(this._getStorageToken(user, 'touch'))); if (password) { return password; } else { console.log('Password not found in keychain'); } } else { const fingerprintAvailable = (yield this.androidFingerprintAuth.isAvailable()).isAvailable; if (fingerprintAvailable) { const token = (yield this.storage.get(this._getStorageToken(user, 'touch'))); if (token) { const fingerprintResult = yield this.androidFingerprintAuth.decrypt({ clientId: user, username: user, token: token, locale: this.getLang(), }); if (fingerprintResult.password) { return fingerprintResult.password; } else { console.log('Password not found in decrypted fingerprint'); } } else { console.log('Token not found in storage'); } } } }); } /** * Executes an attempt to check for the client face * Used mainly in Login * @returns Promise<void> */ launchFaceID(user) { return __awaiter(this, void 0, void 0, function* () { const faceAvailable = yield this.faio.isAvailable(); if (faceAvailable) { try { yield this.faio.show({ cancelButtonTitle: 'Cancel', description: 'Acceder con FaceId', disableBackup: true, title: 'Scan', fallbackButtonTitle: 'FB back Button', subtitle: 'Subtitle', }); } catch (err) { console.log('No match found'); console.log(err); return null; } const password = (yield this.keychain.get(this._getStorageToken(user, 'face'))); if (password) { return password; } else { console.log('Password not found in keychain'); } } else { console.log('FaceID is not available'); } }); } } FingerprintService.decorators = [ { type: Injectable } ]; FingerprintService.ctorParameters = () => [ { type: Platform }, { type: FingerprintAIO }, { type: AndroidFingerprintAuth }, { type: TouchID }, { type: Keychain }, { type: Router }, { type: StorageService }, { type: TranslateService, decorators: [{ type: Optional }] }, { type: undefined, decorators: [{ type: Inject, args: [Config,] }] } ]; class FingerprintMockService { /** * Use this method in Login page to check if user * should be redirected to the Biometric Activator */ checkIfNeedsBiometric(user) { return __awaiter(this, void 0, void 0, function* () { return Promise.resolve(false); }); } /** * Use in Login page to show the Biometric Activator page * @param user User * @param password Password * @param callbackUrl URL to return after success or fail */ showBiometricActivator(user, password, callbackUrl) { } /** * Use in Login page to open Biometric prompt * @param user string */ showBiometricPrompt(user) { return __awaiter(this, void 0, void 0, function* () { return Promise.resolve({ user, password: null }); }); } /** * Checks and returns which biometrics methods are available * for the current device * @returns Promise<Biometric[]> */ retrieveDeviceBiometrics() { return __awaiter(this, void 0, void 0, function* () { return { face: false, touch: false }; }); } /** * Shows the FaceID dialog to the user * @param user Client user * @returns Promise<void> */ activateFaceID(user, password) { return __awaiter(this, void 0, void 0, function* () { }); } /** * Shows the TouchID dialog to the user * @param user Client user * @param password Client password */ activateTouchID(user, password) { } isBiometricActive(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { }); } clearBiometricData(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { }); } disableBiometricData(user, biometricType) { return __awaiter(this, void 0, void 0, function* () { }); } showFingerprintId(user, biometricType) { return of(false); } } FingerprintMockService.decorators = [ { type: Injectable } ]; class BiometricLoginActivePipe { constructor(fingerprintService, platform, config) { this.fingerprintService = fingerprintService; this.platform = platform; this.config = config; } transform(user) { this.config.debug && console.log('[BiometricLoginActive]', user); return from(this.platform.ready()).pipe(tap(() => this.config.debug && console.log('[BiometricLoginActive]', 'Platform is ready')), switchMap(() => { return forkJoin([ this.fingerprintService.showFingerprintId(user, 'face'), this.fingerprintService.showFingerprintId(user, 'touch') ]); }), tap(([face, touch]) => this.config.debug && console.log('[BiometricLoginActive]', 'Face:', face, 'Touch:', touch)), map(([face, touch]) => face || touch)); } } BiometricLoginActivePipe.decorators = [ { type: Pipe, args: [{ name: 'biometricLoginActive' },] } ]; BiometricLoginActivePipe.ctorParameters = () => [ { type: FingerprintService }, { type: Platform }, { type: undefined, decorators: [{ type: Inject, args: [Config,] }] } ]; const defaultConfig = { debug: false, enabled: true }; function initStorage(storageService) { return () => storageService.init(); } const storageProvider = { provide: APP_INITIALIZER, useFactory: initStorage, deps: [StorageService], multi: true }; class FingerprintAuthModule { static forRoot(options = {}) { options = Object.assign(defaultConfig, options); if (options.enabled) { return { ngModule: FingerprintAuthModule, providers: [ FingerprintService, TouchID, Keychain, FingerprintAIO, AndroidFingerprintAuth, StorageService, storageProvider, { provide: Config, useValue: options } ] }; } else { // Mock service return { ngModule: FingerprintAuthModule, providers: [ { provide: FingerprintService, useClass: FingerprintMockService }, TouchID, Keychain, FingerprintAIO, AndroidFingerprintAuth, StorageService, storageProvider, { provide: Config, useValue: options } ] }; } } static forChild() { return { ngModule: FingerprintAuthModule }; } } FingerprintAuthModule.decorators = [ { type: NgModule, args: [{ declarations: [ BiometricLoginActivePipe ], exports: [ BiometricLoginActivePipe ] },] } ]; class BiometricActivator { constructor(navCntrl, route, fingerprintService) { this.navCntrl = navCntrl; this.route = route; this.fingerprintService = fingerprintService; this.biometrics$ = from(this.fingerprintService.retrieveDeviceBiometrics()); } close() { const callbackUrl = this.route.snapshot.queryParamMap.get('callbackUrl'); this.navCntrl.navigateForward(callbackUrl); } activateFaceID() { const callbackUrl = this.route.snapshot.queryParamMap.get('callbackUrl'); const password = this.route.snapshot.queryParamMap.get('password'); const user = this.route.snapshot.queryParamMap.get('user'); this.fingerprintService.activateFaceID(user, password).then(() => { this.navCntrl.navigateForward(callbackUrl); }); } activateTouchID() { const callbackUrl = this.route.snapshot.queryParamMap.get('callbackUrl'); const password = this.route.snapshot.queryParamMap.get('password'); const user = this.route.snapshot.queryParamMap.get('user'); this.fingerprintService.activateTouchID(user, password).then(() => { this.navCntrl.navigateForward(callbackUrl); }); } } BiometricActivator.decorators = [ { type: Component, args: [{ selector: 'app-biometric-activator', template: "<ion-content class=\"ion-padding-horizontal\">\r\n <ion-icon class=\"close-btn\" name=\"close-outline\" color=\"secondary\" (click)=\"close()\"></ion-icon>\r\n <ion-grid *ngIf=\"biometrics$ | async as biometrics\" class=\"p-0\">\r\n \r\n <!-- IF FACEID AND NOT TOUCHID -->\r\n <ng-container *ngIf=\"biometrics.face && !biometrics.touch\">\r\n <ion-row>\r\n <ion-col class=\"px-5\">\r\n <h1 class=\"ion-text-left mt-5\">{{ 'LOGIN-FACEID'| translate }}</h1>\r\n <span class=\"my-4\">{{ 'FACEID-NOT-SETTED'| translate }}</span><br/>\r\n </ion-col>\r\n </ion-row>\r\n <ion-row >\r\n <ion-col>\r\n <img class=\"icon-bio\" src=\"/assets/icon/face-id-bio.svg\" />\r\n </ion-col>\r\n </ion-row>\r\n \r\n <ion-button class=\"btn\" color=\"primary\" (click)=\"activateFaceID()\">\r\n {{ 'FACEID-SET' | translate }}\r\n </ion-button>\r\n </ng-container>\r\n <!-- END IF FACEID -->\r\n \r\n <!-- IF TOUCHID -->\r\n <ng-container *ngIf=\"biometrics.touch\">\r\n <ion-row>\r\n <ion-col class=\"px-5\">\r\n <h1 class=\"ion-text-left mt-5\">{{ 'LOGIN-TOUCH-ID'| translate }}</h1>\r\n <p class=\"my-4\">{{ 'TOUCH-ID-NOT-SETTED'| translate }}</p>\r\n </ion-col>\r\n </ion-row>\r\n \r\n <ion-row>\r\n <ion-col>\r\n <img class=\"icon-bio\" src=\"/assets/icon/Icon_Touch_ID-bio.svg\" />\r\n </ion-col>\r\n </ion-row>\r\n \r\n <ion-button class=\"btn\" color=\"primary\" (click)=\"activateTouchID()\">\r\n {{ 'TOUCH-ID-SET' | translate }}\r\n </ion-button>\r\n </ng-container>\r\n <!-- END IF TOUCHID -->\r\n \r\n </ion-grid>\r\n</ion-content> ", changeDetection: ChangeDetectionStrategy.OnPush, styles: ["ion-content{--padding-start: 0;--padding-end: 0}ion-grid{height:100%;display:flex;flex-direction:column;justify-content:space-between}ion-content h1{font-size:1.6em!important}ion-content p{font-size:1.6m!important}img{position:absolute;left:0;right:0;margin:0 auto;top:50%;bottom:50%;transform:translateY(-50%);height:15vh;fill:#c4c8cb!important}\n"] },] } ]; BiometricActivator.ctorParameters = () => [ { type: NavController }, { type: ActivatedRoute }, { type: FingerprintService } ]; const routes = [ { path: '', component: BiometricActivator } ]; class BiometricActivatorPageRoutingModule { } BiometricActivatorPageRoutingModule.decorators = [ { type: NgModule, args: [{ imports: [RouterModule.forChild(routes)], exports: [RouterModule] },] } ]; class BiometricActivatorModule { } BiometricActivatorModule.decorators = [ { type: NgModule, args: [{ imports: [ CommonModule, ReactiveFormsModule, TranslateModule, IonicModule, BiometricActivatorPageRoutingModule ], declarations: [BiometricActivator] },] } ]; /* * Public API Surface of fingerprint-auth */ /** * Generated bundle index. Do not edit. */ export { BiometricActivator, BiometricActivatorModule, BiometricActivatorPageRoutingModule, BiometricLoginActivePipe, FingerprintAuthModule, FingerprintService, Config as ɵa, StorageService as ɵb }; //# sourceMappingURL=phemium-costaisa-fingerprint-auth.js.map