// modules/api-manager/BiometricAuthManager.js import { Logger } from '../../core/logger.js'; /** * Biometric Authentication Manager - WebAuthn API */ export class BiometricAuthManager { constructor(config = {}) { this.config = { rpName: config.rpName || 'Custom PHP Framework', rpId: config.rpId || window.location.hostname, timeout: config.timeout || 60000, userVerification: config.userVerification || 'preferred', authenticatorSelection: { authenticatorAttachment: 'platform', // Prefer built-in authenticators userVerification: 'preferred', requireResidentKey: false, ...config.authenticatorSelection }, ...config }; this.credentials = new Map(); this.authSessions = new Map(); // Check WebAuthn support this.support = { webAuthn: 'credentials' in navigator && 'create' in navigator.credentials, conditionalUI: 'conditional' in window.PublicKeyCredential || false, userVerifyingPlatformAuthenticator: false, residentKey: false }; // Enhanced feature detection this.detectFeatures(); Logger.info('[BiometricAuthManager] Initialized with support:', this.support); } async detectFeatures() { if (!this.support.webAuthn) return; try { // Check for user-verifying platform authenticator const available = await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); this.support.userVerifyingPlatformAuthenticator = available; // Check for resident key support if (window.PublicKeyCredential.isConditionalMediationAvailable) { this.support.conditionalUI = await window.PublicKeyCredential.isConditionalMediationAvailable(); } Logger.info('[BiometricAuthManager] Enhanced features detected:', { platform: this.support.userVerifyingPlatformAuthenticator, conditional: this.support.conditionalUI }); } catch (error) { Logger.warn('[BiometricAuthManager] Feature detection failed:', error); } } /** * Register new biometric credential */ async register(userInfo, options = {}) { if (!this.support.webAuthn) { throw new Error('WebAuthn not supported'); } const { challenge = null, excludeCredentials = [], extensions = {} } = options; try { // Generate challenge if not provided const challengeBuffer = challenge ? this.base64ToArrayBuffer(challenge) : crypto.getRandomValues(new Uint8Array(32)); // Create credential creation options const createOptions = { rp: { name: this.config.rpName, id: this.config.rpId }, user: { id: this.stringToArrayBuffer(userInfo.id || userInfo.username), name: userInfo.username || userInfo.email, displayName: userInfo.displayName || userInfo.name || userInfo.username }, challenge: challengeBuffer, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -35 }, // ES384 { type: 'public-key', alg: -36 }, // ES512 { type: 'public-key', alg: -257 }, // RS256 { type: 'public-key', alg: -258 }, // RS384 { type: 'public-key', alg: -259 } // RS512 ], authenticatorSelection: this.config.authenticatorSelection, timeout: this.config.timeout, attestation: 'direct', extensions: { credProps: true, ...extensions } }; // Add exclude list if provided if (excludeCredentials.length > 0) { createOptions.excludeCredentials = excludeCredentials.map(cred => ({ type: 'public-key', id: typeof cred === 'string' ? this.base64ToArrayBuffer(cred) : cred, transports: ['internal', 'hybrid'] })); } Logger.info('[BiometricAuthManager] Starting registration...'); // Create credential const credential = await navigator.credentials.create({ publicKey: createOptions }); if (!credential) { throw new Error('Credential creation failed'); } // Process the credential const processedCredential = this.processCredential(credential, 'registration'); // Store credential info locally const credentialInfo = { id: processedCredential.id, rawId: processedCredential.rawId, userId: userInfo.id || userInfo.username, userDisplayName: userInfo.displayName || userInfo.name, createdAt: Date.now(), lastUsed: null, counter: processedCredential.response.counter || 0, transports: credential.response.getTransports?.() || ['internal'] }; this.credentials.set(processedCredential.id, credentialInfo); Logger.info('[BiometricAuthManager] Registration successful:', { id: processedCredential.id, user: userInfo.username, authenticator: credentialInfo.transports }); return { success: true, credential: processedCredential, info: credentialInfo, attestation: this.parseAttestation(credential.response) }; } catch (error) { Logger.error('[BiometricAuthManager] Registration failed:', error); return { success: false, error: error.message, name: error.name }; } } /** * Authenticate with biometric credential */ async authenticate(options = {}) { if (!this.support.webAuthn) { throw new Error('WebAuthn not supported'); } const { challenge = null, allowCredentials = [], userVerification = this.config.userVerification, conditional = false, extensions = {} } = options; try { // Generate challenge if not provided const challengeBuffer = challenge ? this.base64ToArrayBuffer(challenge) : crypto.getRandomValues(new Uint8Array(32)); // Create authentication options const getOptions = { challenge: challengeBuffer, timeout: this.config.timeout, userVerification, extensions: { credProps: true, ...extensions } }; // Add allow list if provided if (allowCredentials.length > 0) { getOptions.allowCredentials = allowCredentials.map(cred => ({ type: 'public-key', id: typeof cred === 'string' ? this.base64ToArrayBuffer(cred) : cred, transports: ['internal', 'hybrid', 'usb', 'nfc', 'ble'] })); } Logger.info('[BiometricAuthManager] Starting authentication...', { conditional }); // Authenticate const credential = conditional && this.support.conditionalUI ? await navigator.credentials.get({ publicKey: getOptions, mediation: 'conditional' }) : await navigator.credentials.get({ publicKey: getOptions }); if (!credential) { throw new Error('Authentication failed'); } // Process the credential const processedCredential = this.processCredential(credential, 'authentication'); // Update credential usage const credentialInfo = this.credentials.get(processedCredential.id); if (credentialInfo) { credentialInfo.lastUsed = Date.now(); credentialInfo.counter = processedCredential.response.counter || 0; } // Create authentication session const sessionId = this.generateSessionId(); const authSession = { id: sessionId, credentialId: processedCredential.id, userId: credentialInfo?.userId, authenticatedAt: Date.now(), userAgent: navigator.userAgent, ipAddress: await this.getClientIP().catch(() => 'unknown') }; this.authSessions.set(sessionId, authSession); Logger.info('[BiometricAuthManager] Authentication successful:', { sessionId, credentialId: processedCredential.id, userId: credentialInfo?.userId }); return { success: true, credential: processedCredential, session: authSession, info: credentialInfo }; } catch (error) { Logger.error('[BiometricAuthManager] Authentication failed:', error); return { success: false, error: error.message, name: error.name }; } } /** * Check if biometric authentication is available */ async isAvailable() { const availability = { webAuthn: this.support.webAuthn, platform: this.support.userVerifyingPlatformAuthenticator, conditional: this.support.conditionalUI, hasCredentials: this.credentials.size > 0, recommended: false }; // Overall recommendation availability.recommended = availability.webAuthn && (availability.platform || availability.hasCredentials); return availability; } /** * Set up conditional UI for seamless authentication */ async setupConditionalUI(inputSelector = 'input[type="email"], input[type="text"]', options = {}) { if (!this.support.conditionalUI) { Logger.warn('[BiometricAuthManager] Conditional UI not supported'); return null; } const inputs = document.querySelectorAll(inputSelector); if (inputs.length === 0) { Logger.warn('[BiometricAuthManager] No suitable inputs found for conditional UI'); return null; } try { // Set up conditional UI inputs.forEach(input => { input.addEventListener('focus', async () => { Logger.info('[BiometricAuthManager] Setting up conditional authentication'); try { const result = await this.authenticate({ ...options, conditional: true }); if (result.success) { // Trigger custom event for successful authentication const event = new CustomEvent('biometric-auth-success', { detail: result }); input.dispatchEvent(event); // Auto-fill username if available if (result.info?.userDisplayName) { input.value = result.info.userDisplayName; } } } catch (error) { Logger.warn('[BiometricAuthManager] Conditional auth failed:', error); } }); }); Logger.info('[BiometricAuthManager] Conditional UI setup complete'); return { inputs: inputs.length, selector: inputSelector }; } catch (error) { Logger.error('[BiometricAuthManager] Conditional UI setup failed:', error); return null; } } /** * Create a complete biometric login flow */ createLoginFlow(options = {}) { const { registerSelector = '#register-biometric', loginSelector = '#login-biometric', statusSelector = '#biometric-status', onRegister = null, onLogin = null, onError = null } = options; return { async init() { const availability = await this.isAvailable(); // Update status display const statusEl = document.querySelector(statusSelector); if (statusEl) { statusEl.innerHTML = this.createStatusHTML(availability); } // Set up register button const registerBtn = document.querySelector(registerSelector); if (registerBtn) { registerBtn.style.display = availability.webAuthn ? 'block' : 'none'; registerBtn.onclick = () => this.handleRegister(); } // Set up login button const loginBtn = document.querySelector(loginSelector); if (loginBtn) { loginBtn.style.display = availability.hasCredentials ? 'block' : 'none'; loginBtn.onclick = () => this.handleLogin(); } // Set up conditional UI if (availability.conditional) { await this.setupConditionalUI(); } return availability; }, async handleRegister() { try { // Get user information (could be from form or prompt) const userInfo = await this.getUserInfo(); if (!userInfo) return; const result = await this.register(userInfo); if (result.success) { if (onRegister) onRegister(result); this.showSuccess('Biometric authentication registered successfully!'); } else { if (onError) onError(result); this.showError(result.error); } } catch (error) { if (onError) onError({ error: error.message }); this.showError(error.message); } }, async handleLogin() { try { const result = await this.authenticate(); if (result.success) { if (onLogin) onLogin(result); this.showSuccess('Biometric authentication successful!'); } else { if (onError) onError(result); this.showError(result.error); } } catch (error) { if (onError) onError({ error: error.message }); this.showError(error.message); } }, async getUserInfo() { // Try to get from form first const usernameInput = document.querySelector('input[name="username"], input[name="email"]'); const nameInput = document.querySelector('input[name="name"], input[name="display_name"]'); if (usernameInput?.value) { return { id: usernameInput.value, username: usernameInput.value, displayName: nameInput?.value || usernameInput.value }; } // Fallback to prompt const username = prompt('Please enter your username or email:'); if (!username) return null; const displayName = prompt('Please enter your display name:') || username; return { id: username, username, displayName }; }, createStatusHTML(availability) { if (!availability.webAuthn) { return '
⚠️ Biometric authentication not supported in this browser
'; } if (!availability.platform && !availability.hasCredentials) { return '
ℹ️ No biometric authenticators available
'; } if (availability.hasCredentials) { return '
✅ Biometric authentication available
'; } return '
🔐 Biometric authentication can be set up
'; }, showSuccess(message) { this.showMessage(message, 'success'); }, showError(message) { this.showMessage(message, 'error'); }, showMessage(message, type = 'info') { // Create toast notification const toast = document.createElement('div'); toast.className = `biometric-toast toast-${type}`; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'error' ? '#f44336' : type === 'success' ? '#4caf50' : '#2196f3'}; color: white; padding: 1rem; border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 10001; max-width: 300px; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { if (toast.parentNode) { document.body.removeChild(toast); } }, 5000); } }; } // Helper methods processCredential(credential, type) { const processed = { id: this.arrayBufferToBase64(credential.rawId), rawId: this.arrayBufferToBase64(credential.rawId), type: credential.type, response: {} }; if (type === 'registration') { processed.response = { attestationObject: this.arrayBufferToBase64(credential.response.attestationObject), clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON), transports: credential.response.getTransports?.() || [] }; } else { processed.response = { authenticatorData: this.arrayBufferToBase64(credential.response.authenticatorData), clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON), signature: this.arrayBufferToBase64(credential.response.signature), userHandle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null }; } return processed; } parseAttestation(response) { try { // Basic attestation parsing const clientDataJSON = JSON.parse(this.arrayBufferToString(response.clientDataJSON)); return { format: 'packed', // Simplified clientData: clientDataJSON, origin: clientDataJSON.origin, challenge: clientDataJSON.challenge }; } catch (error) { Logger.warn('[BiometricAuthManager] Attestation parsing failed:', error); return null; } } async getClientIP() { try { // Use a public IP service (consider privacy implications) const response = await fetch('https://api.ipify.org?format=json'); const data = await response.json(); return data.ip; } catch (error) { return 'unknown'; } } generateSessionId() { return `biometric_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Encoding/decoding utilities 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; } stringToArrayBuffer(str) { const encoder = new TextEncoder(); return encoder.encode(str); } arrayBufferToString(buffer) { const decoder = new TextDecoder(); return decoder.decode(buffer); } /** * Get all registered credentials */ getCredentials() { return Array.from(this.credentials.values()); } /** * Get active authentication sessions */ getActiveSessions() { return Array.from(this.authSessions.values()); } /** * Revoke a credential */ revokeCredential(credentialId) { const removed = this.credentials.delete(credentialId); if (removed) { Logger.info(`[BiometricAuthManager] Credential revoked: ${credentialId}`); } return removed; } /** * End authentication session */ endSession(sessionId) { const removed = this.authSessions.delete(sessionId); if (removed) { Logger.info(`[BiometricAuthManager] Session ended: ${sessionId}`); } return removed; } /** * Cleanup expired sessions */ cleanupSessions(maxAge = 24 * 60 * 60 * 1000) { // 24 hours const now = Date.now(); let cleaned = 0; for (const [sessionId, session] of this.authSessions.entries()) { if (now - session.authenticatedAt > maxAge) { this.authSessions.delete(sessionId); cleaned++; } } if (cleaned > 0) { Logger.info(`[BiometricAuthManager] Cleaned up ${cleaned} expired sessions`); } return cleaned; } /** * Get comprehensive status report */ async getStatusReport() { const availability = await this.isAvailable(); return { availability, credentials: this.credentials.size, sessions: this.authSessions.size, support: this.support, config: { rpName: this.config.rpName, rpId: this.config.rpId, timeout: this.config.timeout }, timestamp: Date.now() }; } }