200 lines
6.3 KiB
JavaScript
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'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|