Files
michaelschiemer/resources/js/modules/api-manager/BiometricAuthManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

678 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 '<div class="alert alert-warning">⚠️ Biometric authentication not supported in this browser</div>';
}
if (!availability.platform && !availability.hasCredentials) {
return '<div class="alert alert-info"> No biometric authenticators available</div>';
}
if (availability.hasCredentials) {
return '<div class="alert alert-success">✅ Biometric authentication available</div>';
}
return '<div class="alert alert-info">🔐 Biometric authentication can be set up</div>';
},
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()
};
}
}