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

200 lines
6.3 KiB
JavaScript

// 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'
});
}
});
}
};