aboutsummaryrefslogtreecommitdiff
path: root/internal/ui/static/js/webauthn_handler.js
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <f@miniflux.net> 2023-11-06 17:28:25 +0000
committerGravatar Frédéric Guillot <f@miniflux.net> 2023-11-06 19:55:32 +0100
commit2b8342fcd5aa77e2f26f9349f3b18f471bd50011 (patch)
treec60b3ce373db432f3784dd2941ef793d3e242c67 /internal/ui/static/js/webauthn_handler.js
parenta75256bed524c203d8d44fdccf5805671471d376 (diff)
downloadv2-2b8342fcd5aa77e2f26f9349f3b18f471bd50011.tar.gz
v2-2b8342fcd5aa77e2f26f9349f3b18f471bd50011.tar.zst
v2-2b8342fcd5aa77e2f26f9349f3b18f471bd50011.zip
Refactor WebAuthn Javascript code
Diffstat (limited to 'internal/ui/static/js/webauthn_handler.js')
-rw-r--r--internal/ui/static/js/webauthn_handler.js177
1 files changed, 177 insertions, 0 deletions
diff --git a/internal/ui/static/js/webauthn_handler.js b/internal/ui/static/js/webauthn_handler.js
new file mode 100644
index 00000000..0835ae0d
--- /dev/null
+++ b/internal/ui/static/js/webauthn_handler.js
@@ -0,0 +1,177 @@
+class WebAuthnHandler {
+ static isWebAuthnSupported() {
+ return window.PublicKeyCredential;
+ }
+
+ static showErrorMessage(errorMessage) {
+ console.log("webauthn error: " + errorMessage);
+ let alertElement = document.getElementById("webauthn-error");
+ if (alertElement) {
+ alertElement.textContent += " (" + errorMessage + ")";
+ alertElement.classList.remove("hidden");
+ }
+ }
+
+ 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;
+ }
+
+ let 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));
+ }
+
+ let 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);
+ }
+
+ let 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;
+ }
+
+ let 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();
+ }
+}