@phemium-costaisa/fingerprint-auth
Version:
Automatic plugin to work with FaceID and TouchID authentication
698 lines (685 loc) • 29.1 kB
JavaScript
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