83 lines
3.0 KiB
JavaScript
83 lines
3.0 KiB
JavaScript
// WebAuthn ceremony bridge: converts the JSON options from the server into the
|
|
// ArrayBuffers navigator.credentials needs, runs the create/get ceremony, and posts
|
|
// the (base64url-encoded) result back to the Livewire component.
|
|
// Exposed as window.clusevWebauthn for the Settings + 2FA-challenge views.
|
|
|
|
const b64uToBuf = (s) => {
|
|
const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
|
|
const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
|
|
const buf = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
return buf.buffer;
|
|
};
|
|
|
|
const bufToB64u = (buf) => {
|
|
let bin = '';
|
|
const bytes = new Uint8Array(buf);
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
};
|
|
|
|
// Map server creation options (base64url strings) → PublicKeyCredentialCreationOptions.
|
|
const toCreateOptions = (o) => {
|
|
const pk = { ...o };
|
|
pk.challenge = b64uToBuf(o.challenge);
|
|
pk.user = { ...o.user, id: b64uToBuf(o.user.id) };
|
|
pk.excludeCredentials = (o.excludeCredentials || []).map((c) => ({ ...c, id: b64uToBuf(c.id) }));
|
|
return pk;
|
|
};
|
|
|
|
const toRequestOptions = (o) => {
|
|
const pk = { ...o };
|
|
pk.challenge = b64uToBuf(o.challenge);
|
|
pk.allowCredentials = (o.allowCredentials || []).map((c) => ({ ...c, id: b64uToBuf(c.id) }));
|
|
return pk;
|
|
};
|
|
|
|
const encodeAttestation = (cred) => ({
|
|
id: cred.id,
|
|
type: cred.type,
|
|
rawId: bufToB64u(cred.rawId),
|
|
response: {
|
|
clientDataJSON: bufToB64u(cred.response.clientDataJSON),
|
|
attestationObject: bufToB64u(cred.response.attestationObject),
|
|
},
|
|
clientExtensionResults: cred.getClientExtensionResults?.() ?? {},
|
|
});
|
|
|
|
const encodeAssertion = (cred) => ({
|
|
id: cred.id,
|
|
type: cred.type,
|
|
rawId: bufToB64u(cred.rawId),
|
|
response: {
|
|
clientDataJSON: bufToB64u(cred.response.clientDataJSON),
|
|
authenticatorData: bufToB64u(cred.response.authenticatorData),
|
|
signature: bufToB64u(cred.response.signature),
|
|
userHandle: cred.response.userHandle ? bufToB64u(cred.response.userHandle) : null,
|
|
},
|
|
clientExtensionResults: cred.getClientExtensionResults?.() ?? {},
|
|
});
|
|
|
|
window.clusevWebauthn = {
|
|
async register($wire) {
|
|
try {
|
|
const options = await $wire.options();
|
|
const cred = await navigator.credentials.create({ publicKey: toCreateOptions(options) });
|
|
await $wire.register(encodeAttestation(cred));
|
|
} catch (e) {
|
|
// user cancelled or the authenticator failed — surface nothing noisy
|
|
console.warn('WebAuthn registration aborted', e);
|
|
}
|
|
},
|
|
|
|
async login($wire) {
|
|
try {
|
|
const options = await $wire.assertionOptions();
|
|
const cred = await navigator.credentials.get({ publicKey: toRequestOptions(options) });
|
|
await $wire.verifyWebauthn(encodeAssertion(cred));
|
|
} catch (e) {
|
|
console.warn('WebAuthn login aborted', e);
|
|
}
|
|
},
|
|
};
|