// Discoverable passkey authentication (usernameless login) import { bufferToBase64url, base64urlToBuffer, getCsrfToken } from './webauthn_utils' document.addEventListener('DOMContentLoaded', () => { const discoverableBtn = document.getElementById('passkey-discoverable-btn') const errorDiv = document.getElementById('passkey-discoverable-error') if (!discoverableBtn || !errorDiv) return const showError = (message) => { errorDiv.classList.remove('hidden') errorDiv.querySelector('p').textContent = message } const hideError = () => { errorDiv.classList.add('hidden') errorDiv.querySelector('p').textContent = '' } const authenticateWithDiscoverablePasskey = async () => { try { hideError() discoverableBtn.disabled = true discoverableBtn.textContent = 'Authenticating...' // Fetch challenge from server (no email required) const challengeResponse = await fetch('/api/webauthn/authentication/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() } }) if (!challengeResponse.ok) { const error = await challengeResponse.json() showError(error.error || 'Failed to get authentication challenge') return } const challengeData = await challengeResponse.json() // Convert challenge to ArrayBuffer const challenge = base64urlToBuffer(challengeData.challenge) const publicKeyCredentialRequestOptions = { challenge, rpId: challengeData.rpId, timeout: challengeData.timeout, userVerification: challengeData.userVerification // No allowCredentials - let the authenticator show all credentials for this RP ID } // Get assertion from authenticator const assertion = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }) const authData = { 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 } } } // 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 { showError('Authentication failed') } } catch (error) { console.error('Discoverable authentication error:', error) let errorMessage = 'Failed to authenticate' if (error.name === 'NotAllowedError') { errorMessage = 'Authentication was cancelled or timed out' } else if (error.name === 'NotSupportedError') { errorMessage = 'Your browser or authenticator does not support passkeys' } else if (error.message) { errorMessage = error.message } showError(errorMessage) } finally { discoverableBtn.disabled = false discoverableBtn.innerHTML = ' Log in with passkey' } } discoverableBtn.addEventListener('click', authenticateWithDiscoverablePasskey) })