UNPKG

capacitor-biometric-authentication

Version:

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

440 lines 20.1 kB
import { WebPlugin } from '@capacitor/core'; import { BiometricType, BiometricUnavailableReason, BiometricErrorCode, } from './definitions'; import { mergeCreateOptions, mergeGetOptions, arrayBufferToBase64, storeCredentialId, getStoredCredentialIds, clearStoredCredentialIds, } from './utils/webauthn'; export class BiometricAuthWeb extends WebPlugin { constructor() { super(...arguments); this.config = { sessionDuration: 3600, // 1 hour default requireAuthenticationForEveryAccess: false, fallbackMethods: [], }; this.sessions = new Map(); } // Helper function for base64url encoding arrayBufferToBase64URL(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } async isAvailable() { // Check if Web Authentication API is available if (!window.PublicKeyCredential) { return { available: false, reason: BiometricUnavailableReason.NOT_SUPPORTED, errorMessage: 'Web Authentication API is not supported in this browser', }; } try { // Check if platform authenticator is available const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); if (!available) { return { available: false, reason: BiometricUnavailableReason.NO_HARDWARE, errorMessage: 'No platform authenticator available', }; } return { available: true, }; } catch (error) { return { available: false, reason: BiometricUnavailableReason.HARDWARE_UNAVAILABLE, errorMessage: error instanceof Error ? error.message : 'Unknown error', }; } } async getSupportedBiometrics() { const result = await this.isAvailable(); if (!result.available) { return { biometrics: [], }; } // Web Authentication API doesn't specify biometric types // Return generic biometric authentication as supported return { biometrics: [ BiometricType.FINGERPRINT, BiometricType.FACE_AUTHENTICATION, ], }; } async authenticate(options) { var _a, _b, _c, _d, _e, _f; try { // Check availability first const availability = await this.isAvailable(); if (!availability.available) { return { success: false, error: { code: BiometricErrorCode.NOT_AVAILABLE, message: availability.errorMessage || 'Biometric authentication not available', }, }; } // If WebAuthn options are provided, use them directly if ((_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get) { return this.authenticateWithWebAuthnOptions(options); } // Get stored credentials for the user const userId = ((_c = (_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.get) === null || _c === void 0 ? void 0 : _c.rpId) || ((_f = (_e = (_d = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _d === void 0 ? void 0 : _d.create) === null || _e === void 0 ? void 0 : _e.user) === null || _f === void 0 ? void 0 : _f.name); const storedCredentialIds = getStoredCredentialIds(userId); // If no stored credentials and user wants to save credentials, register instead if (storedCredentialIds.length === 0 && (options === null || options === void 0 ? void 0 : options.saveCredentials)) { return this.register(options); } // If we have stored credentials, try to authenticate with them if (storedCredentialIds.length > 0) { return this.authenticateWithCredentials(options); } // No credentials found, register new ones return this.register(options); } catch (error) { return this.handleWebAuthnError(error); } } async authenticateWithWebAuthnOptions(options) { var _a, _b; try { // Use the provided WebAuthn options directly const getOptions = mergeGetOptions((_a = options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get); // Get the credential const credential = (await navigator.credentials.get({ publicKey: getOptions, })); if (credential && credential.response instanceof AuthenticatorAssertionResponse) { // Generate session token and include credential data const sessionId = crypto.randomUUID(); const credentialId = arrayBufferToBase64(credential.rawId); // Create enhanced token with credential data for backend verification const credentialData = { id: credential.id, rawId: this.arrayBufferToBase64URL(credential.rawId), response: { authenticatorData: this.arrayBufferToBase64URL(credential.response.authenticatorData), clientDataJSON: this.arrayBufferToBase64URL(credential.response.clientDataJSON), signature: this.arrayBufferToBase64URL(credential.response.signature), userHandle: credential.response.userHandle ? this.arrayBufferToBase64URL(credential.response.userHandle) : undefined, }, type: credential.type, clientExtensionResults: JSON.stringify(((_b = credential.getClientExtensionResults) === null || _b === void 0 ? void 0 : _b.call(credential)) || {}), authenticatorAttachment: credential.authenticatorAttachment, }; const token = btoa(JSON.stringify({ credentialId, timestamp: Date.now(), sessionId, type: 'authentication', credentialData, // Include full credential data })); // Store session const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000; this.sessions.set(sessionId, { token, expiresAt }); // Clean up expired sessions this.cleanupExpiredSessions(); return { success: true, token, sessionId, }; } return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to authenticate with credential', }, }; } catch (error) { return this.handleWebAuthnError(error); } } async authenticateWithCredentials(options) { var _a, _b, _c, _d, _e, _f; try { // Get stored credential IDs const userId = ((_b = (_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.rpId) || ((_e = (_d = (_c = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _c === void 0 ? void 0 : _c.create) === null || _d === void 0 ? void 0 : _d.user) === null || _e === void 0 ? void 0 : _e.name); const storedCredentialIds = getStoredCredentialIds(userId); // Prepare allowed credentials const allowCredentials = storedCredentialIds.map((id) => ({ id: Uint8Array.from(atob(id), (c) => c.charCodeAt(0)), type: 'public-key', transports: ['internal'], })); // Merge user options with defaults const getOptions = mergeGetOptions((_f = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _f === void 0 ? void 0 : _f.get, { rpId: window.location.hostname, userVerification: 'required', allowCredentials: allowCredentials.length > 0 ? allowCredentials : undefined, }); // Get the credential const credential = (await navigator.credentials.get({ publicKey: getOptions, })); if (credential && credential.response instanceof AuthenticatorAssertionResponse) { // Generate session token const sessionId = crypto.randomUUID(); const credentialId = arrayBufferToBase64(credential.rawId); const token = btoa(JSON.stringify({ credentialId, timestamp: Date.now(), sessionId, type: 'authentication', })); // Store session const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000; this.sessions.set(sessionId, { token, expiresAt }); // Clean up expired sessions this.cleanupExpiredSessions(); return { success: true, token, sessionId, }; } return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to authenticate with credential', }, }; } catch (error) { return this.handleWebAuthnError(error); } } async register(options) { var _a, _b, _c, _d, _e; try { // Check availability first const availability = await this.isAvailable(); if (!availability.available) { return { success: false, error: { code: BiometricErrorCode.NOT_AVAILABLE, message: availability.errorMessage || 'Biometric authentication not available', }, }; } // If WebAuthn options are provided, use them directly if ((_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.create) { return this.registerWithWebAuthnOptions(options); } // Merge user options with defaults for fallback const createOptions = mergeCreateOptions((_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.create, { rp: { name: (options === null || options === void 0 ? void 0 : options.title) || 'Biometric Authentication', }, authenticatorSelection: { authenticatorAttachment: 'platform', userVerification: 'required', }, }); // Create the credential const credential = (await navigator.credentials.create({ publicKey: createOptions, })); if (credential && credential.response instanceof AuthenticatorAttestationResponse) { // Store credential ID for future authentication const credentialId = arrayBufferToBase64(credential.rawId); const userId = (_e = (_d = (_c = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _c === void 0 ? void 0 : _c.create) === null || _d === void 0 ? void 0 : _d.user) === null || _e === void 0 ? void 0 : _e.name; storeCredentialId(credentialId, userId); // Generate session token const sessionId = crypto.randomUUID(); const token = btoa(JSON.stringify({ credentialId, timestamp: Date.now(), sessionId, type: 'registration', })); // Store session const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000; this.sessions.set(sessionId, { token, expiresAt }); // Clean up expired sessions this.cleanupExpiredSessions(); return { success: true, token, sessionId, }; } return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to create credential', }, }; } catch (error) { return this.handleWebAuthnError(error); } } async registerWithWebAuthnOptions(options) { var _a, _b, _c, _d, _e, _f, _g; try { // Use the provided WebAuthn options directly const createOptions = mergeCreateOptions((_a = options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.create); // Create the credential const credential = (await navigator.credentials.create({ publicKey: createOptions, })); if (credential && credential.response instanceof AuthenticatorAttestationResponse) { // Store credential ID for future authentication const credentialId = arrayBufferToBase64(credential.rawId); const userId = (_d = (_c = (_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.create) === null || _c === void 0 ? void 0 : _c.user) === null || _d === void 0 ? void 0 : _d.name; storeCredentialId(credentialId, userId); // Create enhanced token with credential data for backend verification const credentialData = { id: credential.id, rawId: this.arrayBufferToBase64URL(credential.rawId), response: { attestationObject: this.arrayBufferToBase64URL(credential.response.attestationObject), clientDataJSON: this.arrayBufferToBase64URL(credential.response.clientDataJSON), transports: ((_f = (_e = credential.response).getTransports) === null || _f === void 0 ? void 0 : _f.call(_e)) || [], }, type: credential.type, clientExtensionResults: JSON.stringify(((_g = credential.getClientExtensionResults) === null || _g === void 0 ? void 0 : _g.call(credential)) || {}), authenticatorAttachment: credential.authenticatorAttachment, }; // Generate session token const sessionId = crypto.randomUUID(); const token = btoa(JSON.stringify({ credentialId, timestamp: Date.now(), sessionId, type: 'registration', credentialData, // Include full credential data })); // Store session const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000; this.sessions.set(sessionId, { token, expiresAt }); // Clean up expired sessions this.cleanupExpiredSessions(); return { success: true, token, sessionId, }; } return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to create credential', }, }; } catch (error) { return this.handleWebAuthnError(error); } } async deleteCredentials() { // Clear all sessions this.sessions.clear(); // Clear stored credential IDs clearStoredCredentialIds(); // Clear any other stored data try { const keys = Object.keys(localStorage).filter((key) => key.startsWith('biometric_auth_')); keys.forEach((key) => localStorage.removeItem(key)); } catch (error) { console.error('Failed to clear stored credentials:', error); } } async configure(config) { this.config = Object.assign(Object.assign({}, this.config), config); // Validate configuration if (config.sessionDuration && config.sessionDuration < 0) { throw new Error('Session duration must be positive'); } if (config.encryptionSecret && config.encryptionSecret.length < 32) { console.warn('Encryption secret should be at least 32 characters for security'); } } cleanupExpiredSessions() { const now = Date.now(); const expiredSessions = []; this.sessions.forEach((session, id) => { if (session.expiresAt < now) { expiredSessions.push(id); } }); expiredSessions.forEach((id) => this.sessions.delete(id)); } handleWebAuthnError(error) { if (error instanceof Error) { if (error.name === 'NotAllowedError') { return { success: false, error: { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled the authentication', }, }; } else if (error.name === 'NotSupportedError') { return { success: false, error: { code: BiometricErrorCode.NOT_AVAILABLE, message: 'Biometric authentication not supported', }, }; } else if (error.name === 'InvalidStateError') { return { success: false, error: { code: BiometricErrorCode.INVALID_CONTEXT, message: 'Invalid authentication context', }, }; } else if (error.name === 'SecurityError') { return { success: false, error: { code: BiometricErrorCode.INVALID_CONTEXT, message: 'Security requirements not met', }, }; } } return { success: false, error: { code: BiometricErrorCode.UNKNOWN, message: error instanceof Error ? error.message : 'Unknown error occurred', }, }; } } //# sourceMappingURL=web.js.map