101 lines
3.6 KiB
JavaScript
101 lines
3.6 KiB
JavaScript
// WebAuthn helpers — base64url encoding compatible with Fido2NetLib
|
|
|
|
function base64urlToBuffer(base64url) {
|
|
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
const binary = atob(base64);
|
|
return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;
|
|
}
|
|
|
|
function bufferToBase64url(buffer) {
|
|
const bytes = new Uint8Array(buffer);
|
|
let binary = '';
|
|
for (const b of bytes) binary += String.fromCharCode(b);
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
}
|
|
|
|
function prepareCreationOptions(options) {
|
|
options.challenge = base64urlToBuffer(options.challenge);
|
|
options.user.id = base64urlToBuffer(options.user.id);
|
|
if (options.excludeCredentials) {
|
|
options.excludeCredentials = options.excludeCredentials.map(c => ({
|
|
...c,
|
|
id: base64urlToBuffer(c.id),
|
|
}));
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function prepareRequestOptions(options) {
|
|
options.challenge = base64urlToBuffer(options.challenge);
|
|
if (options.allowCredentials) {
|
|
options.allowCredentials = options.allowCredentials.map(c => ({
|
|
...c,
|
|
id: base64urlToBuffer(c.id),
|
|
}));
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function serializeAttestation(credential) {
|
|
return {
|
|
id: credential.id,
|
|
rawId: bufferToBase64url(credential.rawId),
|
|
type: credential.type,
|
|
response: {
|
|
attestationObject: bufferToBase64url(credential.response.attestationObject),
|
|
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
|
|
transports: credential.response.getTransports ? credential.response.getTransports() : [],
|
|
},
|
|
extensions: credential.getClientExtensionResults(),
|
|
};
|
|
}
|
|
|
|
function serializeAssertion(credential) {
|
|
return {
|
|
id: credential.id,
|
|
rawId: bufferToBase64url(credential.rawId),
|
|
type: credential.type,
|
|
response: {
|
|
authenticatorData: bufferToBase64url(credential.response.authenticatorData),
|
|
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
|
|
signature: bufferToBase64url(credential.response.signature),
|
|
userHandle: credential.response.userHandle
|
|
? bufferToBase64url(credential.response.userHandle)
|
|
: null,
|
|
},
|
|
extensions: credential.getClientExtensionResults(),
|
|
};
|
|
}
|
|
|
|
window.passkeyEnroll = async function () {
|
|
const optRes = await fetch('/api/auth/register/options', {method: 'POST'});
|
|
if (!optRes.ok) throw new Error(await optRes.text());
|
|
const options = prepareCreationOptions(await optRes.json());
|
|
|
|
const credential = await navigator.credentials.create({publicKey: options});
|
|
|
|
const completeRes = await fetch('/api/auth/register/complete', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(serializeAttestation(credential)),
|
|
});
|
|
if (!completeRes.ok) throw new Error(await completeRes.text());
|
|
return await completeRes.json();
|
|
};
|
|
|
|
window.passkeyLogin = async function () {
|
|
const optRes = await fetch('/api/auth/login/options', {method: 'POST'});
|
|
if (!optRes.ok) throw new Error(await optRes.text());
|
|
const options = prepareRequestOptions(await optRes.json());
|
|
|
|
const credential = await navigator.credentials.get({publicKey: options});
|
|
|
|
const completeRes = await fetch('/api/auth/login/complete', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(serializeAssertion(credential)),
|
|
});
|
|
if (!completeRes.ok) throw new Error(await completeRes.text());
|
|
return await completeRes.json();
|
|
};
|