// WebAuthn utility functions for passkey authentication import { base64urlToBuffer, bufferToBase64url, getCsrfToken } from './webauthn_utils' const WebAuthn = { // Registration flow async register(challengeData, credentialName) { // Convert challenge and user ID to ArrayBuffer const challenge = base64urlToBuffer(challengeData.challenge); const userId = base64urlToBuffer(challengeData.user.id); const publicKeyCredentialCreationOptions = { challenge, rp: challengeData.rp, user: { id: userId, name: challengeData.user.name, displayName: challengeData.user.displayName }, pubKeyCredParams: challengeData.pubKeyCredParams, authenticatorSelection: challengeData.authenticatorSelection, timeout: challengeData.timeout, attestation: challengeData.attestation }; try { const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions }); return { name: credentialName, attestation: { id: credential.id, rawId: bufferToBase64url(credential.rawId), type: credential.type, response: { clientDataJSON: bufferToBase64url(credential.response.clientDataJSON), attestationObject: bufferToBase64url(credential.response.attestationObject) } } }; } catch (error) { console.error('WebAuthn registration error:', error); throw error; } }, // Authentication flow async authenticate(challengeData) { const challenge = base64urlToBuffer(challengeData.challenge); const allowCredentials = challengeData.allowCredentials.map(cred => ({ type: cred.type, id: base64urlToBuffer(cred.id), transports: cred.transports })); const publicKeyCredentialRequestOptions = { challenge, rpId: challengeData.rpId, allowCredentials, timeout: challengeData.timeout, userVerification: challengeData.userVerification }; try { const assertion = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }); return { assertion: { id: assertion.id, rawId: bufferToBase64url(assertion.rawId), type: assertion.type, response: { clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON), authenticatorData: bufferToBase64url(assertion.response.authenticatorData), signature: bufferToBase64url(assertion.response.signature), userHandle: assertion.response.userHandle ? bufferToBase64url(assertion.response.userHandle) : null } } }; } catch (error) { console.error('WebAuthn authentication error:', error); throw error; } } }; // LiveView Hook for Registration (in credential management page) export const WebAuthnRegister = { mounted() { this.handleEvent("start_registration", async ({ name }) => { try { // Fetch challenge from server const response = await fetch('/api/webauthn/registration/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() } }); if (!response.ok) { const error = await response.json(); this.pushEvent("registration_failed", { error: error.error || 'Failed to get registration challenge' }); return; } const challengeData = await response.json(); // Perform WebAuthn registration const registrationData = await WebAuthn.register(challengeData, name); // Send to server const registerResponse = await fetch('/api/webauthn/register', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify(registrationData) }); if (registerResponse.ok) { this.pushEvent("registration_success", {}); } else { const error = await registerResponse.json(); this.pushEvent("registration_failed", { error: error.error || 'Failed to register passkey' }); } } catch (error) { console.error('Registration error:', error); this.pushEvent("registration_failed", { error: error.name === 'NotAllowedError' ? 'Registration was cancelled or timed out' : error.message || 'Failed to register passkey' }); } }); } }; // LiveView Hook for Authentication (on login page) export const WebAuthnLogin = { mounted() { this.handleEvent("start_authentication", async ({ email }) => { try { // Fetch challenge from server const response = await fetch('/api/webauthn/authentication/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify({ email }) }); if (!response.ok) { const error = await response.json(); this.pushEvent("authentication_failed", { error: error.error || 'No passkeys found for this email' }); return; } const challengeData = await response.json(); // Perform WebAuthn authentication const authData = await WebAuthn.authenticate(challengeData); // Send to server const authResponse = await fetch('/api/webauthn/authenticate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify(authData) }); if (authResponse.ok) { const result = await authResponse.json(); // Redirect to the provided URL window.location.href = result.redirect; } else { this.pushEvent("authentication_failed", { error: 'Authentication failed' }); } } catch (error) { console.error('Authentication error:', error); this.pushEvent("authentication_failed", { error: error.name === 'NotAllowedError' ? 'Authentication was cancelled or timed out' : error.message || 'Failed to authenticate' }); } }); } };