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
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,678 @@
// 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()
};
}
}