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:
678
resources/js/modules/api-manager/BiometricAuthManager.js
Normal file
678
resources/js/modules/api-manager/BiometricAuthManager.js
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user