// Discoverable passkey authentication (usernameless login) import { bufferToBase64url, base64urlToBuffer, getCsrfToken } from './webauthn_utils' import type { WebAuthnAuthChallengeData, ErrorResponse, AuthSuccessResponse } from './types/webauthn' document.addEventListener('DOMContentLoaded', () => { const discoverableBtn = document.getElementById('passkey-discoverable-btn') as HTMLButtonElement | null const errorDiv = document.getElementById('passkey-discoverable-error') if (!discoverableBtn || !errorDiv) return const showError = (message: string) => { errorDiv.classList.remove('hidden') const errorText = errorDiv.querySelector('p') if (errorText) { errorText.textContent = message } } const hideError = () => { errorDiv.classList.add('hidden') const errorText = errorDiv.querySelector('p') if (errorText) { errorText.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: ErrorResponse = await challengeResponse.json() showError(error.error || 'Failed to get authentication challenge') return } const challengeData: WebAuthnAuthChallengeData = await challengeResponse.json() // Convert challenge to ArrayBuffer const challenge = base64urlToBuffer(challengeData.challenge) const publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions = { challenge, rpId: challengeData.rpId, timeout: challengeData.timeout, userVerification: challengeData.userVerification as UserVerificationRequirement // No allowCredentials - let the authenticator show all credentials for this RP ID } // Get assertion from authenticator const assertion = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }) as PublicKeyCredential if (!assertion.response || !(assertion.response instanceof AuthenticatorAssertionResponse)) { throw new Error('Invalid assertion response') } 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: AuthSuccessResponse = 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 as Error).name === 'NotAllowedError') { errorMessage = 'Authentication was cancelled or timed out' } else if ((error as Error).name === 'NotSupportedError') { errorMessage = 'Your browser or authenticator does not support passkeys' } else if ((error as Error).message) { errorMessage = (error as Error).message } showError(errorMessage) } finally { discoverableBtn.disabled = false discoverableBtn.innerHTML = ' Log in with passkey' } } discoverableBtn.addEventListener('click', authenticateWithDiscoverablePasskey) })