towerops/assets/js/passkey_discoverable.js
2026-01-09 12:26:32 -06:00

111 lines
3.7 KiB
JavaScript

// 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 = '<svg class="h-5 w-5"><use href="#hero-key"></use></svg> Log in with passkey'
}
}
discoverableBtn.addEventListener('click', authenticateWithDiscoverablePasskey)
})