aboutsummaryrefslogtreecommitdiff
path: root/internal/ui/static/js/webauthn.js
diff options
context:
space:
mode:
authorGravatar Florian RĂ¼chel <florian.ruechel.github@inexplicity.de> 2023-11-06 04:27:35 +1030
committerGravatar GitHub <noreply@github.com> 2023-11-05 18:57:35 +0100
commit62ef8ed57aab9f2b05a64b153d231ae4f42769f4 (patch)
treeacc33ab1fd02113f8fc93751e593dc67ff504a84 /internal/ui/static/js/webauthn.js
parent62188b49f072ea3c2bf30a8ed42f8b9303840191 (diff)
downloadv2-62ef8ed57aab9f2b05a64b153d231ae4f42769f4.tar.gz
v2-62ef8ed57aab9f2b05a64b153d231ae4f42769f4.tar.zst
v2-62ef8ed57aab9f2b05a64b153d231ae4f42769f4.zip
Add WebAuthn / Passkey integration
This is a rebase of #1618 in which @dave-atx added WebAuthn support. Closes #1618
Diffstat (limited to 'internal/ui/static/js/webauthn.js')
-rw-r--r--internal/ui/static/js/webauthn.js196
1 files changed, 196 insertions, 0 deletions
diff --git a/internal/ui/static/js/webauthn.js b/internal/ui/static/js/webauthn.js
new file mode 100644
index 00000000..465aa49e
--- /dev/null
+++ b/internal/ui/static/js/webauthn.js
@@ -0,0 +1,196 @@
+function isWebAuthnSupported() {
+ return window.PublicKeyCredential;
+}
+
+async function isConditionalLoginSupported() {
+ return isWebAuthnSupported() &&
+ window.PublicKeyCredential.isConditionalMediationAvailable &&
+ window.PublicKeyCredential.isConditionalMediationAvailable();
+}
+
+// URLBase64 to ArrayBuffer
+function bufferDecode(value) {
+ return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
+}
+
+// ArrayBuffer to URLBase64
+function bufferEncode(value) {
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=/g, "");
+}
+
+function getCsrfToken() {
+ let element = document.querySelector("body[data-csrf-token]");
+ if (element !== null) {
+ return element.dataset.csrfToken;
+ }
+ return "";
+}
+
+async function post(urlKey, username, data) {
+ var 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 function get(urlKey, username) {
+ var url = document.body.dataset[urlKey];
+ if (username) {
+ url += "?username=" + username;
+ }
+ return fetch(url);
+}
+
+function showError(error) {
+ console.log("webauthn error: " + error);
+ let alert = document.getElementById("webauthn-error");
+ if (alert) {
+ alert.classList.remove("hidden");
+ }
+}
+
+async function register() {
+ let beginRegisterURL = "webauthnRegisterBeginUrl";
+ let r = await get(beginRegisterURL);
+ let credOptions = await r.json();
+ credOptions.publicKey.challenge = bufferDecode(credOptions.publicKey.challenge);
+ credOptions.publicKey.user.id = bufferDecode(credOptions.publicKey.user.id);
+ if(Object.hasOwn(credOptions.publicKey, 'excludeCredentials')) {
+ credOptions.publicKey.excludeCredentials.forEach((credential) => credential.id = bufferDecode(credential.id));
+ }
+ let attestation = await navigator.credentials.create(credOptions);
+ let cred = {
+ id: attestation.id,
+ rawId: bufferEncode(attestation.rawId),
+ type: attestation.type,
+ response: {
+ attestationObject: bufferEncode(attestation.response.attestationObject),
+ clientDataJSON: bufferEncode(attestation.response.clientDataJSON),
+ },
+ };
+ let finishRegisterURL = "webauthnRegisterFinishUrl";
+ let response = await post(finishRegisterURL, null, cred);
+ if (!response.ok) {
+ throw new Error("Login failed with HTTP status " + response.status);
+ }
+ console.log("registration successful");
+
+ let jsonData = await response.json();
+ let redirect = jsonData.redirect;
+ window.location.href = redirect;
+}
+
+async function login(username, conditional) {
+ let beginLoginURL = "webauthnLoginBeginUrl";
+ let r = await get(beginLoginURL, username);
+ let credOptions = await r.json();
+ credOptions.publicKey.challenge = bufferDecode(credOptions.publicKey.challenge);
+ if(Object.hasOwn(credOptions.publicKey, 'allowCredentials')) {
+ credOptions.publicKey.allowCredentials.forEach((credential) => credential.id = bufferDecode(credential.id));
+ }
+ if (conditional) {
+ credOptions.signal = abortController.signal;
+ credOptions.mediation = "conditional";
+ }
+
+ var assertion;
+ try {
+ assertion = await navigator.credentials.get(credOptions);
+ }
+ catch (err) {
+ // swallow aborted conditional logins
+ if (err instanceof DOMException && err.name == "AbortError") {
+ return;
+ }
+ throw err;
+ }
+
+ if (!assertion) {
+ return;
+ }
+
+ let assertionResponse = {
+ id: assertion.id,
+ rawId: bufferEncode(assertion.rawId),
+ type: assertion.type,
+ response: {
+ authenticatorData: bufferEncode(assertion.response.authenticatorData),
+ clientDataJSON: bufferEncode(assertion.response.clientDataJSON),
+ signature: bufferEncode(assertion.response.signature),
+ userHandle: bufferEncode(assertion.response.userHandle),
+ },
+ };
+
+ let finishLoginURL = "webauthnLoginFinishUrl";
+ let response = await post(finishLoginURL, username, assertionResponse);
+ if (!response.ok) {
+ throw new Error("Login failed with HTTP status " + response.status);
+ }
+ window.location.reload();
+}
+
+async function conditionalLogin() {
+ if (await isConditionalLoginSupported()) {
+ login("", true);
+ }
+}
+
+async function removeCreds(event) {
+ event.preventDefault();
+ let removeCredsURL = "webauthnDeleteAllUrl";
+ await post(removeCredsURL, null, {});
+ window.location.reload();
+}
+
+let abortController = new AbortController();
+document.addEventListener("DOMContentLoaded", function () {
+ if (!isWebAuthnSupported()) {
+ return;
+ }
+
+ let registerButton = document.getElementById("webauthn-register");
+ if (registerButton != null) {
+ registerButton.disabled = false;
+ registerButton.addEventListener("click", (e) => {
+ e.preventDefault();
+ register().catch((err) => showError(err));
+ });
+ }
+
+ let removeCredsButton = document.getElementById("webauthn-delete");
+ if (removeCredsButton != null) {
+ removeCredsButton.addEventListener("click", removeCreds);
+ }
+
+ let loginButton = document.getElementById("webauthn-login");
+ if (loginButton != null) {
+ loginButton.disabled = false;
+ let usernameField = document.getElementById("form-username");
+ if (usernameField != null) {
+ usernameField.autocomplete += " webauthn";
+ }
+ let passwordField = document.getElementById("form-password");
+ if (passwordField != null) {
+ passwordField.autocomplete += " webauthn";
+ }
+
+ loginButton.addEventListener("click", (e) => {
+ e.preventDefault();
+ abortController.abort();
+ login(usernameField.value).catch(err => showError(err));
+ });
+
+ conditionalLogin().catch(err => showError(err));
+ }
+});