1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-09-30 19:22:11 +00:00
miniflux-v2/internal/ui/static/js/webauthn_handler.js
2025-08-01 18:34:05 -07:00

194 lines
6.6 KiB
JavaScript

class WebAuthnHandler {
static isWebAuthnSupported() {
return window.PublicKeyCredential;
}
static showErrorMessage(errorMessage) {
console.log("webauthn error: " + errorMessage);
const alertElement = document.getElementById("webauthn-error-alert");
if (alertElement) {
alertElement.remove();
}
const alertTemplateElement = document.getElementById("webauthn-error");
if (alertTemplateElement) {
const clonedElement = alertTemplateElement.content.cloneNode(true);
const errorMessageElement = clonedElement.getElementById("webauthn-error-message");
if (errorMessageElement) {
errorMessageElement.textContent = errorMessage;
}
alertTemplateElement.parentNode.insertBefore(clonedElement, alertTemplateElement);
}
}
async isConditionalLoginSupported() {
return (
WebAuthnHandler.isWebAuthnSupported() &&
window.PublicKeyCredential.isConditionalMediationAvailable &&
window.PublicKeyCredential.isConditionalMediationAvailable()
);
}
async conditionalLogin(abortController) {
if (await this.isConditionalLoginSupported()) {
this.login("", abortController);
}
}
decodeBuffer(value) {
return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
}
encodeBuffer(value) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}
async post(urlKey, username, data) {
let url = document.body.dataset[urlKey];
if (username) {
url += "?username=" + username;
}
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Csrf-Token": getCsrfToken(),
},
body: JSON.stringify(data),
});
}
async get(urlKey, username) {
let url = document.body.dataset[urlKey];
if (username) {
url += "?username=" + username;
}
return fetch(url);
}
async removeAllCredentials() {
try {
await this.post("webauthnDeleteAllUrl", null, {});
} catch (err) {
WebAuthnHandler.showErrorMessage(err);
return;
}
window.location.reload();
}
async register() {
let registerBeginResponse;
try {
registerBeginResponse = await this.get("webauthnRegisterBeginUrl");
} catch (err) {
WebAuthnHandler.showErrorMessage(err);
return;
}
const credentialCreationOptions = await registerBeginResponse.json();
credentialCreationOptions.publicKey.challenge = this.decodeBuffer(
credentialCreationOptions.publicKey.challenge
);
credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
if (Object.hasOwn(credentialCreationOptions.publicKey, "excludeCredentials")) {
credentialCreationOptions.publicKey.excludeCredentials.forEach(
(credential) => (credential.id = this.decodeBuffer(credential.id))
);
}
const attestation = await navigator.credentials.create(credentialCreationOptions);
let registrationFinishResponse;
try {
registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
id: attestation.id,
rawId: this.encodeBuffer(attestation.rawId),
type: attestation.type,
response: {
attestationObject: this.encodeBuffer(attestation.response.attestationObject),
clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
},
});
} catch (err) {
WebAuthnHandler.showErrorMessage(err);
return;
}
if (!registrationFinishResponse.ok) {
throw new Error("Login failed with HTTP status code " + response.status);
}
const jsonData = await registrationFinishResponse.json();
window.location.href = jsonData.redirect;
}
async login(username, abortController) {
let loginBeginResponse;
try {
loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
} catch (err) {
WebAuthnHandler.showErrorMessage(err);
return;
}
const credentialRequestOptions = await loginBeginResponse.json();
credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
if (Object.hasOwn(credentialRequestOptions.publicKey, "allowCredentials")) {
credentialRequestOptions.publicKey.allowCredentials.forEach(
(credential) => (credential.id = this.decodeBuffer(credential.id))
);
}
if (abortController) {
credentialRequestOptions.signal = abortController.signal;
credentialRequestOptions.mediation = "conditional";
}
let assertion;
try {
assertion = await navigator.credentials.get(credentialRequestOptions);
} catch (err) {
// Swallow aborted conditional logins
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
WebAuthnHandler.showErrorMessage(err);
return;
}
if (!assertion) {
return;
}
let loginFinishResponse;
try {
loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
id: assertion.id,
rawId: this.encodeBuffer(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
signature: this.encodeBuffer(assertion.response.signature),
userHandle: this.encodeBuffer(assertion.response.userHandle),
},
});
} catch (err) {
WebAuthnHandler.showErrorMessage(err);
return;
}
if (!loginFinishResponse.ok) {
throw new Error("Login failed with HTTP status code " + loginFinishResponse.status);
}
window.location.reload();
}
}