UNPKG

@aparajita/capacitor-biometric-auth

Version:

Provides access to the native biometric auth & device security APIs for Capacitor 7+ apps

344 lines (332 loc) 13.3 kB
'use strict'; var core = require('@capacitor/core'); var app = require('@capacitor/app'); // noinspection JSUnusedGlobalSymbols /** * The type of biometry supported by the device. */ exports.BiometryType = void 0; (function (BiometryType) { BiometryType[BiometryType["none"] = 0] = "none"; /** * iOS Touch ID */ BiometryType[BiometryType["touchId"] = 1] = "touchId"; /** * iOS Face ID */ BiometryType[BiometryType["faceId"] = 2] = "faceId"; /** * Android fingerprint authentication */ BiometryType[BiometryType["fingerprintAuthentication"] = 3] = "fingerprintAuthentication"; /** * Android face authentication */ BiometryType[BiometryType["faceAuthentication"] = 4] = "faceAuthentication"; /** * Android iris authentication */ BiometryType[BiometryType["irisAuthentication"] = 5] = "irisAuthentication"; })(exports.BiometryType || (exports.BiometryType = {})); exports.AndroidBiometryStrength = void 0; (function (AndroidBiometryStrength) { /** * `authenticate()` will present any available biometry. */ AndroidBiometryStrength[AndroidBiometryStrength["weak"] = 0] = "weak"; /** * `authenticate()` will only present strong biometry. */ AndroidBiometryStrength[AndroidBiometryStrength["strong"] = 1] = "strong"; })(exports.AndroidBiometryStrength || (exports.AndroidBiometryStrength = {})); /** * If the `authenticate()` method throws an exception, the `BiometryError` * instance contains a `.code` property which will contain one of these strings, * indicating what the error was. * * See https://developer.apple.com/documentation/localauthentication/laerror * for a description of each error code. */ exports.BiometryErrorType = void 0; (function (BiometryErrorType) { BiometryErrorType["none"] = ""; BiometryErrorType["appCancel"] = "appCancel"; BiometryErrorType["authenticationFailed"] = "authenticationFailed"; BiometryErrorType["invalidContext"] = "invalidContext"; BiometryErrorType["notInteractive"] = "notInteractive"; BiometryErrorType["passcodeNotSet"] = "passcodeNotSet"; BiometryErrorType["systemCancel"] = "systemCancel"; BiometryErrorType["userCancel"] = "userCancel"; BiometryErrorType["userFallback"] = "userFallback"; BiometryErrorType["biometryLockout"] = "biometryLockout"; BiometryErrorType["biometryNotAvailable"] = "biometryNotAvailable"; BiometryErrorType["biometryNotEnrolled"] = "biometryNotEnrolled"; BiometryErrorType["noDeviceCredential"] = "noDeviceCredential"; })(exports.BiometryErrorType || (exports.BiometryErrorType = {})); function isBiometryErrorType(value) { return (typeof value === 'string' && // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion Object.values(exports.BiometryErrorType).includes(value)); } /** * `authenticate()` throws instances of this class. */ class BiometryError extends Error { constructor(message, code) { super(message); this.code = code; this.name = 'BiometryError'; // Set the prototype explicitly to ensure instanceof works correctly. // This is recommended for custom error classes in TypeScript. Object.setPrototypeOf(this, BiometryError.prototype); } } const kBiometryTypeNameMap = { [exports.BiometryType.none]: '', [exports.BiometryType.touchId]: 'Touch ID', [exports.BiometryType.faceId]: 'Face ID', [exports.BiometryType.fingerprintAuthentication]: 'Fingerprint Authentication', [exports.BiometryType.faceAuthentication]: 'Face Authentication', [exports.BiometryType.irisAuthentication]: 'Iris Authentication', }; /** * Return a human-readable name for a BiometryType. */ function getBiometryName(type) { return kBiometryTypeNameMap[type] || ''; } const proxy = core.registerPlugin('BiometricAuthNative', { web: async () => { const module = await Promise.resolve().then(function () { return web; }); return new module.BiometricAuthWeb(); }, ios: async () => { const module = await Promise.resolve().then(function () { return native; }); return new module.BiometricAuthNative(proxy); }, android: async () => { const module = await Promise.resolve().then(function () { return native; }); return new module.BiometricAuthNative(proxy); }, }); class BiometricAuthBase extends core.WebPlugin { async authenticate(options) { try { await this.internalAuthenticate(options); } catch (error) { // error will be an instance of CapacitorException on native platforms, // an instance of BiometryError on the web. throw error instanceof core.CapacitorException && isBiometryErrorType(error.code) ? new BiometryError(error.message, error.code) : error; } } async addResumeListener(listener) { return app.App.addListener('appStateChange', ({ isActive }) => { if (isActive) { (async () => { try { const info = await this.checkBiometry(); listener(info); } catch (error) { console.error(error); } })(); } }); } } /* eslint-disable @typescript-eslint/require-await */ class BiometricAuthWeb extends BiometricAuthBase { constructor() { super(...arguments); this.biometryType = exports.BiometryType.none; this.biometryTypes = []; this.biometryIsEnrolled = false; this.deviceIsSecure = false; } // On the web, return the fake biometry set by setBiometryType(). async checkBiometry() { const hasBiometry = this.biometryType !== exports.BiometryType.none; const available = hasBiometry && this.biometryIsEnrolled; let reason = ''; let code = exports.BiometryErrorType.none; if (!hasBiometry) { reason = 'No biometry is available'; code = exports.BiometryErrorType.biometryNotAvailable; } else if (!this.biometryIsEnrolled) { reason = 'Biometry is not enrolled'; code = exports.BiometryErrorType.biometryNotEnrolled; } return { isAvailable: available, strongBiometryIsAvailable: this.biometryIsEnrolled && this.hasStrongBiometry(), biometryType: this.biometryType, biometryTypes: this.biometryTypes, deviceIsSecure: this.deviceIsSecure, reason, code, }; } hasStrongBiometry() { return this.biometryTypes.some((type) => type === exports.BiometryType.faceId || type === exports.BiometryType.touchId || type === exports.BiometryType.fingerprintAuthentication); } // On the web, fake authentication with a confirm dialog. async internalAuthenticate(options) { const result = await this.checkBiometry(); // First try biometry if available. if (result.isAvailable && // oxlint-disable-next-line no-alert confirm( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to use the default value if options?.reason is an empty string (options === null || options === void 0 ? void 0 : options.reason) || `Authenticate with ${result.biometryTypes .map((type) => getBiometryName(type)) .join(' or ')}?`)) { return; } if (options === null || options === void 0 ? void 0 : options.allowDeviceCredential) { // Either biometry is not available, or the user declined to use it // and device security is allowed. if (result.deviceIsSecure) { // oxlint-disable-next-line no-alert if (confirm('Authenticate with device security?')) { return; } throw new BiometryError('User cancelled', exports.BiometryErrorType.userCancel); } else if (result.isAvailable) { throw new BiometryError('Device is not secure', exports.BiometryErrorType.noDeviceCredential); } } else if (!result.isAvailable) { // Biometry is not available and device security is not allowed. throw result.biometryType === exports.BiometryType.none ? new BiometryError('Biometry is not available', exports.BiometryErrorType.biometryNotAvailable) : new BiometryError('Biometry is not enrolled', exports.BiometryErrorType.biometryNotEnrolled); } // The user declined to use biometry and device credentials not allowed. throw new BiometryError('User cancelled', exports.BiometryErrorType.userCancel); } // Web only, used for simulating biometric authentication. async setBiometryType(type) { if (type === undefined) { return; } const types = Array.isArray(type) ? type : [type]; this.biometryTypes = []; this.biometryType = exports.BiometryType.none; if (types.length === 0) { return; } if (isBiometryTypes(types)) { this.biometryType = types[0]; if (this.biometryType !== exports.BiometryType.none) { this.biometryTypes = types; } } else { for (const [i, theType] of types.entries()) { if (isBiometryType(theType)) { if (this.biometryType === exports.BiometryType.none) { this.biometryTypes = []; } else { this.biometryTypes.push(theType); } if (i === 0) { this.biometryType = theType; } } } } } // Web only, used for simulating device unlock security. async setBiometryIsEnrolled(enrolled) { this.biometryIsEnrolled = enrolled; } // Web only, used for simulating device unlock security. async setDeviceIsSecure(isSecure) { this.deviceIsSecure = isSecure; } } function isBiometryType(value) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return Object.values(exports.BiometryType).includes(value); } function isBiometryTypes(value) { return value.every((type) => isBiometryType(type)); } /* eslint-enable @typescript-eslint/require-await */ var web = /*#__PURE__*/Object.freeze({ __proto__: null, BiometricAuthWeb: BiometricAuthWeb }); /* eslint-disable @typescript-eslint/require-await */ // oxlint-disable eslint/class-methods-use-this -- Protected methods don't use `this` but need to be overridden class BiometricAuthNative extends BiometricAuthBase { constructor(capProxy) { super(); /* In order to call native methods and maintain the ability to call pure Javascript methods as well, we have to bind the native methods to the proxy. capProxy is a proxy of an instance of this class, so it is safe to cast it to this class. */ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const proxy = capProxy; /* eslint-disable @typescript-eslint/unbound-method */ this.checkBiometry = proxy.checkBiometry; this.internalAuthenticate = proxy.internalAuthenticate; /* eslint-enable @typescript-eslint/unbound-method */ } // @native async checkBiometry() { // Never used, but we have to satisfy the compiler. return { isAvailable: false, strongBiometryIsAvailable: false, biometryType: exports.BiometryType.none, biometryTypes: [], deviceIsSecure: false, reason: '', code: exports.BiometryErrorType.none, strongReason: '', strongCode: exports.BiometryErrorType.none, }; } // @native // On native platforms, this will present the native authentication UI. async internalAuthenticate(_options) { // This method is implemented natively } // Web only, used for simulating biometric authentication. async setBiometryType(_type) { console.warn('setBiometryType() is web only'); } // Web only, used for simulating biometry enrollment. async setBiometryIsEnrolled(_enrolled) { console.warn('setBiometryEnrolled() is web only'); } // Web only, used for simulating device security. async setDeviceIsSecure(_isSecure) { console.warn('setDeviceIsSecure() is web only'); } } /* eslint-enable @typescript-eslint/require-await */ var native = /*#__PURE__*/Object.freeze({ __proto__: null, BiometricAuthNative: BiometricAuthNative }); exports.BiometricAuth = proxy; exports.BiometryError = BiometryError; exports.getBiometryName = getBiometryName; exports.isBiometryErrorType = isBiometryErrorType;