111 lines
3.7 KiB
JavaScript
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)
|
|
})
|