diff options
author | 2023-11-06 17:28:25 +0000 | |
---|---|---|
committer | 2023-11-06 19:55:32 +0100 | |
commit | 2b8342fcd5aa77e2f26f9349f3b18f471bd50011 (patch) | |
tree | c60b3ce373db432f3784dd2941ef793d3e242c67 /internal/ui/static/js/webauthn_handler.js | |
parent | a75256bed524c203d8d44fdccf5805671471d376 (diff) | |
download | v2-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.js | 177 |
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(); + } +} |