UNPKG

capacitor-biometric-authentication

Version:

Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!

843 lines (831 loc) 34.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./adapters/CapacitorAdapter')) : typeof define === 'function' && define.amd ? define(['exports', './adapters/CapacitorAdapter'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.BiometricAuth = {}, global.CapacitorAdapter)); })(this, (function (exports, CapacitorAdapter) { 'use strict'; exports.BiometricErrorCode = void 0; (function (BiometricErrorCode) { BiometricErrorCode["BIOMETRIC_UNAVAILABLE"] = "BIOMETRIC_UNAVAILABLE"; BiometricErrorCode["AUTHENTICATION_FAILED"] = "AUTHENTICATION_FAILED"; BiometricErrorCode["USER_CANCELLED"] = "USER_CANCELLED"; BiometricErrorCode["TIMEOUT"] = "TIMEOUT"; BiometricErrorCode["LOCKOUT"] = "LOCKOUT"; BiometricErrorCode["NOT_ENROLLED"] = "NOT_ENROLLED"; BiometricErrorCode["PLATFORM_NOT_SUPPORTED"] = "PLATFORM_NOT_SUPPORTED"; BiometricErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; })(exports.BiometricErrorCode || (exports.BiometricErrorCode = {})); var BiometryType; (function (BiometryType) { BiometryType["FINGERPRINT"] = "fingerprint"; BiometryType["FACE_ID"] = "faceId"; BiometryType["TOUCH_ID"] = "touchId"; BiometryType["IRIS"] = "iris"; BiometryType["MULTIPLE"] = "multiple"; BiometryType["UNKNOWN"] = "unknown"; })(BiometryType || (BiometryType = {})); class PlatformDetector { constructor() { this.platformInfo = null; } static getInstance() { if (!PlatformDetector.instance) { PlatformDetector.instance = new PlatformDetector(); } return PlatformDetector.instance; } detect() { var _a, _b; if (this.platformInfo) { return this.platformInfo; } const info = { name: 'unknown', isCapacitor: false, isReactNative: false, isCordova: false, isWeb: false, isIOS: false, isAndroid: false, isElectron: false }; // Check if we're in a browser environment if (typeof window !== 'undefined' && typeof document !== 'undefined') { info.isWeb = true; // Check for Capacitor if (window.Capacitor) { info.isCapacitor = true; const capacitor = window.Capacitor; const platform = (_a = capacitor === null || capacitor === void 0 ? void 0 : capacitor.getPlatform) === null || _a === void 0 ? void 0 : _a.call(capacitor); if (platform) { info.name = platform; info.isIOS = platform === 'ios'; info.isAndroid = platform === 'android'; info.isWeb = platform === 'web'; } } // Check for Cordova else if (window.cordova || window.phonegap) { info.isCordova = true; info.name = 'cordova'; const userAgent = navigator.userAgent || navigator.vendor || window.opera || ''; if (/android/i.test(userAgent)) { info.isAndroid = true; info.name = 'android'; } else if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { info.isIOS = true; info.name = 'ios'; } } // Check for Electron else if (((_b = window.process) === null || _b === void 0 ? void 0 : _b.type) === 'renderer' || navigator.userAgent.indexOf('Electron') !== -1) { info.isElectron = true; info.name = 'electron'; } // Default to web else { info.name = 'web'; } } // Check for React Native else if (typeof global !== 'undefined' && global.nativePerformanceNow) { info.isReactNative = true; info.name = 'react-native'; // Try to detect platform in React Native try { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any const { Platform } = require('react-native'); if (Platform) { info.name = Platform.OS; info.isIOS = Platform.OS === 'ios'; info.isAndroid = Platform.OS === 'android'; } } catch (_c) { // React Native not available } } // Node.js environment else if (typeof process !== 'undefined' && process.versions && process.versions.node) { info.name = 'node'; } // Get version info if available if (info.isCapacitor && typeof window !== 'undefined') { const capacitor = window.Capacitor; if (capacitor === null || capacitor === void 0 ? void 0 : capacitor.version) { info.version = capacitor.version; } } else if (info.isReactNative) { try { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any const { Platform } = require('react-native'); info.version = Platform.Version; } catch (_d) { // Ignore } } this.platformInfo = info; return info; } isSupported() { const info = this.detect(); return info.isWeb || info.isCapacitor || info.isReactNative || info.isCordova; } getPlatformName() { return this.detect().name; } isNativePlatform() { const info = this.detect(); return (info.isIOS || info.isAndroid) && (info.isCapacitor || info.isReactNative || info.isCordova); } } class BiometricAuthCore { constructor() { this.config = { adapter: 'auto', debug: false, sessionDuration: 300000, // 5 minutes }; this.adapters = new Map(); this.currentAdapter = null; this.state = { isAuthenticated: false }; this.platformDetector = PlatformDetector.getInstance(); this.subscribers = new Set(); this.initialize(); } static getInstance() { if (!BiometricAuthCore.instance) { BiometricAuthCore.instance = new BiometricAuthCore(); } return BiometricAuthCore.instance; } async initialize() { // Detect platform and load appropriate adapter const platformInfo = this.platformDetector.detect(); if (this.config.debug) { console.warn('[BiometricAuth] Platform detected:', platformInfo); } // Load adapter based on platform await this.loadAdapter(platformInfo.name); } async loadAdapter(platform) { var _a; try { // Try custom adapters first if ((_a = this.config.customAdapters) === null || _a === void 0 ? void 0 : _a[platform]) { this.currentAdapter = this.config.customAdapters[platform]; return; } // Try to load from registered adapters if (this.adapters.has(platform)) { this.currentAdapter = this.adapters.get(platform); return; } // Dynamic import based on platform switch (platform) { case 'web': const { WebAdapter } = await Promise.resolve().then(function () { return WebAdapter$1; }); this.currentAdapter = new WebAdapter(); break; case 'ios': case 'android': // Check if Capacitor is available if (this.platformDetector.detect().isCapacitor) { const { CapacitorAdapter } = await import('../adapters/CapacitorAdapter'); this.currentAdapter = new CapacitorAdapter(); } else if (this.platformDetector.detect().isReactNative) { // Dynamic import for React Native try { const { ReactNativeAdapter } = await import('../adapters/ReactNativeAdapter'); this.currentAdapter = new ReactNativeAdapter(); } catch (_b) { throw new Error('React Native biometric module not installed. Please install react-native-biometrics'); } } break; case 'electron': const { ElectronAdapter } = await Promise.resolve().then(function () { return ElectronAdapter$1; }); this.currentAdapter = new ElectronAdapter(); break; default: throw new Error(`Platform ${platform} not supported`); } } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] Failed to load adapter:', error); } // Fallback to web adapter if available if (platform !== 'web' && this.platformDetector.detect().isWeb) { const { WebAdapter } = await Promise.resolve().then(function () { return WebAdapter$1; }); this.currentAdapter = new WebAdapter(); } } } configure(config) { this.config = Object.assign(Object.assign({}, this.config), config); // Re-initialize if adapter changed if (config.adapter && config.adapter !== 'auto') { this.loadAdapter(config.adapter); } } registerAdapter(name, adapter) { this.adapters.set(name, adapter); } async isAvailable() { if (!this.currentAdapter) { return false; } try { return await this.currentAdapter.isAvailable(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] isAvailable error:', error); } return false; } } async getSupportedBiometrics() { if (!this.currentAdapter) { return []; } try { return await this.currentAdapter.getSupportedBiometrics(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] getSupportedBiometrics error:', error); } return []; } } async authenticate(options) { if (!this.currentAdapter) { return { success: false, error: { code: exports.BiometricErrorCode.PLATFORM_NOT_SUPPORTED, message: 'No biometric adapter available for this platform' } }; } try { const result = await this.currentAdapter.authenticate(options); if (result.success) { this.updateState({ isAuthenticated: true, sessionId: result.sessionId, lastAuthTime: Date.now(), biometryType: result.biometryType, error: undefined }); // Set up session timeout if (this.config.sessionDuration && this.config.sessionDuration > 0) { setTimeout(() => { this.logout(); }, this.config.sessionDuration); } } else { this.updateState({ isAuthenticated: false, error: result.error }); } return result; } catch (error) { const errorResult = { success: false, error: { code: exports.BiometricErrorCode.UNKNOWN_ERROR, message: error instanceof Error ? error.message : 'Unknown error occurred', details: error } }; this.updateState({ isAuthenticated: false, error: errorResult.error }); return errorResult; } } async deleteCredentials() { if (!this.currentAdapter) { throw new Error('No biometric adapter available'); } await this.currentAdapter.deleteCredentials(); this.logout(); } async hasCredentials() { if (!this.currentAdapter) { return false; } try { return await this.currentAdapter.hasCredentials(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] hasCredentials error:', error); } return false; } } logout() { this.updateState({ isAuthenticated: false, sessionId: undefined, lastAuthTime: undefined, biometryType: undefined, error: undefined }); } getState() { return Object.assign({}, this.state); } isAuthenticated() { if (!this.state.isAuthenticated || !this.state.lastAuthTime) { return false; } // Check if session is still valid if (this.config.sessionDuration && this.config.sessionDuration > 0) { const elapsed = Date.now() - this.state.lastAuthTime; if (this.config.sessionDuration && elapsed > this.config.sessionDuration) { this.logout(); return false; } } return true; } subscribe(callback) { this.subscribers.add(callback); // Return unsubscribe function return () => { this.subscribers.delete(callback); }; } updateState(newState) { this.state = Object.assign(Object.assign({}, this.state), newState); // Notify subscribers this.subscribers.forEach(callback => { callback(this.getState()); }); } // Utility methods for common use cases async requireAuthentication(callback, options) { var _a; if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || 'Authentication failed'); } } return callback(); } async withAuthentication(callback, options) { var _a; if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || 'Authentication failed'); } } return callback(); } } class WebAdapter { constructor() { this.platform = 'web'; this.credentials = new Map(); // Set default Relying Party info this.rpId = window.location.hostname; this.rpName = document.title || 'Biometric Authentication'; } async isAvailable() { // Check if WebAuthn is supported if (!window.PublicKeyCredential) { return false; } // Check if platform authenticator is available try { const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); return available; } catch (_a) { return false; } } async getSupportedBiometrics() { if (!(await this.isAvailable())) { return []; } // WebAuthn doesn't provide specific biometry types // Return generic "multiple" as modern devices support various methods return [BiometryType.MULTIPLE]; } async authenticate(options) { var _a; try { // Check if WebAuthn is available if (!(await this.isAvailable())) { return { success: false, error: { code: exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'WebAuthn is not available on this device' } }; } const webOptions = ((_a = options === null || options === void 0 ? void 0 : options.platform) === null || _a === void 0 ? void 0 : _a.web) || {}; // Try to get existing credential first const existingCredential = await this.getExistingCredential(webOptions); if (existingCredential) { return { success: true, biometryType: BiometryType.MULTIPLE, sessionId: this.generateSessionId(), platform: 'web' }; } // If no existing credential, create a new one const credential = await this.createCredential((options === null || options === void 0 ? void 0 : options.reason) || 'Authentication required', webOptions); if (credential) { // Store credential for future use const credentialId = this.arrayBufferToBase64(credential.rawId); this.credentials.set(credentialId, credential); this.saveCredentialId(credentialId); return { success: true, biometryType: BiometryType.MULTIPLE, sessionId: this.generateSessionId(), platform: 'web' }; } return { success: false, error: { code: exports.BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to authenticate' } }; } catch (error) { return this.handleError(error); } } async deleteCredentials() { this.credentials.clear(); localStorage.removeItem('biometric_credential_ids'); } async hasCredentials() { const storedIds = this.getStoredCredentialIds(); return storedIds.length > 0; } async getExistingCredential(options) { const storedIds = this.getStoredCredentialIds(); if (storedIds.length === 0) { return null; } try { const challenge = options.challenge || crypto.getRandomValues(new Uint8Array(32)); const publicKeyOptions = { challenge, rpId: options.rpId || this.rpId, timeout: options.timeout || 60000, userVerification: options.userVerification || 'preferred', allowCredentials: storedIds.map(id => ({ id: this.base64ToArrayBuffer(id), type: 'public-key' })) }; const credential = await navigator.credentials.get({ publicKey: publicKeyOptions }); return credential; } catch (_a) { return null; } } async createCredential(_reason, options) { try { const challenge = options.challenge || crypto.getRandomValues(new Uint8Array(32)); const userId = crypto.getRandomValues(new Uint8Array(32)); const publicKeyOptions = { challenge, rp: { id: options.rpId || this.rpId, name: options.rpName || this.rpName }, user: { id: userId, name: 'user@' + this.rpId, displayName: 'User' }, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -257 } // RS256 ], authenticatorSelection: options.authenticatorSelection || { authenticatorAttachment: 'platform', userVerification: 'preferred', requireResidentKey: false, residentKey: 'discouraged' }, timeout: options.timeout || 60000, attestation: options.attestation || 'none' }; const credential = await navigator.credentials.create({ publicKey: publicKeyOptions }); return credential; } catch (_a) { return null; } } handleError(error) { let code = exports.BiometricErrorCode.UNKNOWN_ERROR; let message = 'An unknown error occurred'; if (error instanceof DOMException) { switch (error.name) { case 'NotAllowedError': code = exports.BiometricErrorCode.USER_CANCELLED; message = 'User cancelled the authentication'; break; case 'AbortError': code = exports.BiometricErrorCode.USER_CANCELLED; message = 'Authentication was aborted'; break; case 'SecurityError': code = exports.BiometricErrorCode.AUTHENTICATION_FAILED; message = 'Security error during authentication'; break; case 'InvalidStateError': code = exports.BiometricErrorCode.AUTHENTICATION_FAILED; message = 'Invalid state for authentication'; break; case 'NotSupportedError': code = exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE; message = 'WebAuthn is not supported'; break; default: message = error.message || message; } } else if (error instanceof Error) { message = error.message; } return { success: false, error: { code, message, details: error } }; } generateSessionId() { return Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => b.toString(16).padStart(2, '0')) .join(''); } arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } base64ToArrayBuffer(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } getStoredCredentialIds() { const stored = localStorage.getItem('biometric_credential_ids'); if (!stored) { return []; } try { return JSON.parse(stored); } catch (_a) { return []; } } saveCredentialId(id) { const existing = this.getStoredCredentialIds(); if (!existing.includes(id)) { existing.push(id); localStorage.setItem('biometric_credential_ids', JSON.stringify(existing)); } } } var WebAdapter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, WebAdapter: WebAdapter }); var _a; // Create singleton instance const biometricAuth = BiometricAuthCore.getInstance(); // Export the main API (provider-less, like Zustand) const BiometricAuth = { // Core methods configure: (config) => biometricAuth.configure(config), isAvailable: () => biometricAuth.isAvailable(), getSupportedBiometrics: () => biometricAuth.getSupportedBiometrics(), authenticate: (options) => biometricAuth.authenticate(options), deleteCredentials: () => biometricAuth.deleteCredentials(), hasCredentials: () => biometricAuth.hasCredentials(), // State management logout: () => biometricAuth.logout(), getState: () => biometricAuth.getState(), isAuthenticated: () => biometricAuth.isAuthenticated(), subscribe: (callback) => biometricAuth.subscribe(callback), // Utility methods requireAuthentication: (callback, options) => biometricAuth.requireAuthentication(callback, options), withAuthentication: (callback, options) => biometricAuth.withAuthentication(callback, options), // Advanced usage registerAdapter: (name, adapter) => biometricAuth.registerAdapter(name, adapter), }; // For backward compatibility with Capacitor plugin registration if (typeof window !== 'undefined') { const capacitorGlobal = window; if ((_a = capacitorGlobal.Capacitor) === null || _a === void 0 ? void 0 : _a.registerPlugin) { // Register as a Capacitor plugin for backward compatibility const { registerPlugin } = capacitorGlobal.Capacitor; if (registerPlugin) { try { // Create a Capacitor-compatible plugin interface const BiometricAuthPlugin = { isAvailable: async () => ({ isAvailable: await BiometricAuth.isAvailable() }), getSupportedBiometrics: async () => ({ biometryTypes: await BiometricAuth.getSupportedBiometrics() }), authenticate: async (options) => { const result = await BiometricAuth.authenticate(options); return { success: result.success, error: result.error, biometryType: result.biometryType }; }, deleteCredentials: async () => { await BiometricAuth.deleteCredentials(); return {}; } }; // Register the plugin registerPlugin('BiometricAuth', { web: BiometricAuthPlugin }); } catch (_b) { // Ignore registration errors - not critical } } } } class ElectronAdapter { constructor() { this.platform = 'electron'; // Electron-specific initialization } async isAvailable() { try { // Check if we're in Electron main or renderer process if (typeof process !== 'undefined' && process.versions && process.versions.electron) { // In Electron, we can use TouchID on macOS if (process.platform === 'darwin') { // eslint-disable-next-line @typescript-eslint/no-require-imports const electronModule = require('electron'); const { systemPreferences } = electronModule.remote || electronModule; return systemPreferences.canPromptTouchID(); } // Windows Hello support could be added here return false; } return false; } catch (_a) { return false; } } async getSupportedBiometrics() { if (!(await this.isAvailable())) { return []; } // On macOS, we support Touch ID if (process.platform === 'darwin') { return [BiometryType.TOUCH_ID]; } return []; } async authenticate(options) { try { if (!(await this.isAvailable())) { return { success: false, error: { code: exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'Biometric authentication is not available' } }; } if (process.platform === 'darwin') { // eslint-disable-next-line @typescript-eslint/no-require-imports const electronModule = require('electron'); const { systemPreferences } = electronModule.remote || electronModule; try { await systemPreferences.promptTouchID((options === null || options === void 0 ? void 0 : options.reason) || 'authenticate with Touch ID'); return { success: true, biometryType: BiometryType.TOUCH_ID, sessionId: this.generateSessionId(), platform: 'electron' }; } catch (_a) { return { success: false, error: { code: exports.BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Touch ID authentication failed' } }; } } return { success: false, error: { code: exports.BiometricErrorCode.PLATFORM_NOT_SUPPORTED, message: 'Platform not supported' } }; } catch (error) { return { success: false, error: this.mapError(error) }; } } async deleteCredentials() { // Electron doesn't store biometric credentials // This is a no-op } async hasCredentials() { // In Electron, we don't store credentials // Return true if biometrics are available return await this.isAvailable(); } mapError(error) { let code = exports.BiometricErrorCode.UNKNOWN_ERROR; let message = 'An unknown error occurred'; if (error instanceof Error) { message = error.message; if (message.includes('cancelled') || message.includes('canceled')) { code = exports.BiometricErrorCode.USER_CANCELLED; } else if (message.includes('failed')) { code = exports.BiometricErrorCode.AUTHENTICATION_FAILED; } } return { code, message, details: error }; } generateSessionId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } } var ElectronAdapter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, ElectronAdapter: ElectronAdapter }); Object.defineProperty(exports, "CapacitorAdapter", { enumerable: true, get: function () { return CapacitorAdapter.CapacitorAdapter; } }); exports.BiometricAuth = BiometricAuth; exports.BiometricAuthCore = BiometricAuthCore; exports.PlatformDetector = PlatformDetector; exports.WebAdapter = WebAdapter; exports.default = BiometricAuth; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=web.umd.js.map