summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/template/templates/common/layout.html3
-rw-r--r--internal/template/templates/views/settings.html1
-rw-r--r--internal/ui/static/js/app.js11
-rw-r--r--internal/ui/static/js/bootstrap.js40
-rw-r--r--internal/ui/static/js/request_builder.js11
-rw-r--r--internal/ui/static/js/webauthn.js196
-rw-r--r--internal/ui/static/js/webauthn_handler.js177
-rw-r--r--internal/ui/static/static.go4
-rw-r--r--internal/ui/webauthn.go1
9 files changed, 226 insertions, 218 deletions
diff --git a/internal/template/templates/common/layout.html b/internal/template/templates/common/layout.html
index 47d5bd0e..f1654d6f 100644
--- a/internal/template/templates/common/layout.html
+++ b/internal/template/templates/common/layout.html
@@ -44,9 +44,6 @@
<script src="{{ route "javascript" "name" "app" "checksum" .app_js_checksum }}" defer></script>
<script src="{{ route "javascript" "name" "service-worker" "checksum" .sw_js_checksum }}" defer id="service-worker-script"></script>
- {{ if .webAuthnEnabled }}
- <script src="{{ route "javascript" "name" "webauthn" "checksum" .webauthn_js_checksum }}" defer></script>
- {{ end }}
</head>
<body
{{ if .csrf }}data-csrf-token="{{ .csrf }}"{{ end }}
diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html
index 71f6c602..3b6f5692 100644
--- a/internal/template/templates/views/settings.html
+++ b/internal/template/templates/views/settings.html
@@ -48,7 +48,6 @@
</div>
</fieldset>
-
{{ if .webAuthnEnabled }}
<fieldset>
<legend>{{ t "page.settings.webauthn.passkeys" }}</legend>
diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js
index 80781ee2..2af87dcd 100644
--- a/internal/ui/static/js/app.js
+++ b/internal/ui/static/js/app.js
@@ -688,4 +688,13 @@ function checkShareAPI(title, url) {
console.error(err);
window.location.reload();
}
-} \ No newline at end of file
+}
+
+function getCsrfToken() {
+ let element = document.querySelector("body[data-csrf-token]");
+ if (element !== null) {
+ return element.dataset.csrfToken;
+ }
+
+ return "";
+}
diff --git a/internal/ui/static/js/bootstrap.js b/internal/ui/static/js/bootstrap.js
index 0cd878ef..3d7725ff 100644
--- a/internal/ui/static/js/bootstrap.js
+++ b/internal/ui/static/js/bootstrap.js
@@ -1,4 +1,4 @@
-document.addEventListener("DOMContentLoaded", function () {
+document.addEventListener("DOMContentLoaded", () => {
handleSubmitButtons();
if (!document.querySelector("body[data-disable-keyboard-shortcuts=true]")) {
@@ -48,6 +48,37 @@ document.addEventListener("DOMContentLoaded", function () {
let touchHandler = new TouchHandler();
touchHandler.listen();
+ if (WebAuthnHandler.isWebAuthnSupported()) {
+ const webauthnHandler = new WebAuthnHandler();
+
+ onClick("#webauthn-delete", () => { webauthnHandler.removeAllCredentials() });
+
+ let registerButton = document.getElementById("webauthn-register");
+ if (registerButton != null) {
+ registerButton.disabled = false;
+
+ onClick("#webauthn-register", () => {
+ webauthnHandler.register().catch((err) => WebAuthnHandler.showErrorMessage(err));
+ });
+ }
+
+ let loginButton = document.getElementById("webauthn-login");
+ if (loginButton != null) {
+ const abortController = new AbortController();
+ loginButton.disabled = false;
+
+ onClick("#webauthn-login", () => {
+ let usernameField = document.getElementById("form-username");
+ if (usernameField != null) {
+ abortController.abort();
+ webauthnHandler.login(usernameField.value).catch(err => WebAuthnHandler.showErrorMessage(err));
+ }
+ });
+
+ webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err));
+ }
+ }
+
onClick("a[data-save-entry]", (event) => handleSaveEntry(event.target));
onClick("a[data-toggle-bookmark]", (event) => handleBookmark(event.target));
onClick("a[data-fetch-content-entry]", () => handleFetchOriginalContent());
@@ -116,11 +147,12 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
- // enclosure media player position save & resume
+ // Save and resume media position
const elements = document.querySelectorAll("audio[data-last-position],video[data-last-position]");
elements.forEach((element) => {
- // we set the current time of media players
- if (element.dataset.lastPosition){ element.currentTime = element.dataset.lastPosition; }
+ if (element.dataset.lastPosition) {
+ element.currentTime = element.dataset.lastPosition;
+ }
element.ontimeupdate = () => handlePlayerProgressionSave(element);
});
});
diff --git a/internal/ui/static/js/request_builder.js b/internal/ui/static/js/request_builder.js
index e19168fc..c72cfb4e 100644
--- a/internal/ui/static/js/request_builder.js
+++ b/internal/ui/static/js/request_builder.js
@@ -9,7 +9,7 @@ class RequestBuilder {
body: null,
headers: new Headers({
"Content-Type": "application/json",
- "X-Csrf-Token": this.getCsrfToken()
+ "X-Csrf-Token": getCsrfToken()
})
};
}
@@ -29,15 +29,6 @@ class RequestBuilder {
return this;
}
- getCsrfToken() {
- let element = document.querySelector("body[data-csrf-token]");
- if (element !== null) {
- return element.dataset.csrfToken;
- }
-
- return "";
- }
-
execute() {
fetch(new Request(this.url, this.options)).then((response) => {
if (this.callback) {
diff --git a/internal/ui/static/js/webauthn.js b/internal/ui/static/js/webauthn.js
deleted file mode 100644
index 465aa49e..00000000
--- a/internal/ui/static/js/webauthn.js
+++ /dev/null
@@ -1,196 +0,0 @@
-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));
- }
-});
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();
+ }
+}
diff --git a/internal/ui/static/static.go b/internal/ui/static/static.go
index bcc40fce..a3deb6d9 100644
--- a/internal/ui/static/static.go
+++ b/internal/ui/static/static.go
@@ -118,14 +118,12 @@ func GenerateJavascriptBundles() error {
"js/request_builder.js",
"js/modal_handler.js",
"js/app.js",
+ "js/webauthn_handler.js",
"js/bootstrap.js",
},
"service-worker": {
"js/service_worker.js",
},
- "webauthn": {
- "js/webauthn.js",
- },
}
var prefixes = map[string]string{
diff --git a/internal/ui/webauthn.go b/internal/ui/webauthn.go
index 0071c74c..8a671ca2 100644
--- a/internal/ui/webauthn.go
+++ b/internal/ui/webauthn.go
@@ -13,6 +13,7 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
+
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/crypto"
"miniflux.app/v2/internal/http/cookie"