about summary refs log tree commit diff
path: root/kittybox-rs/src/frontend
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-09-19 17:30:38 +0300
committerVika <vika@fireburn.ru>2022-09-19 17:30:38 +0300
commit66049566ae865e1a4bd049257d6afc0abded16e9 (patch)
tree6013a26fa98a149d103eb4402ca91d698ef02ac2 /kittybox-rs/src/frontend
parent696458657b26032e6e2a987c059fd69aaa10508d (diff)
downloadkittybox-66049566ae865e1a4bd049257d6afc0abded16e9.tar.zst
feat: indieauth support
Working:
 - Tokens and codes
 - Authenticating with a password

Not working:
 - Setting the password (need to patch onboarding)
 - WebAuthn (the JavaScript is too complicated)
Diffstat (limited to 'kittybox-rs/src/frontend')
-rw-r--r--kittybox-rs/src/frontend/indieauth.js107
-rw-r--r--kittybox-rs/src/frontend/mod.rs1
-rw-r--r--kittybox-rs/src/frontend/style.css9
3 files changed, 116 insertions, 1 deletions
diff --git a/kittybox-rs/src/frontend/indieauth.js b/kittybox-rs/src/frontend/indieauth.js
new file mode 100644
index 0000000..03626b8
--- /dev/null
+++ b/kittybox-rs/src/frontend/indieauth.js
@@ -0,0 +1,107 @@
+const WEBAUTHN_TIMEOUT = 60 * 1000;
+
+async function webauthn_create_credential() {
+  const response = await fetch("/.kittybox/webauthn/pre_register");
+  const { challenge, rp, user } = await response.json();
+
+  return await navigator.credentials.create({
+    publicKey: {
+      challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+      rp: rp,
+      user: {
+        id: Uint8Array.from(user.cred_id),
+        name: user.name,
+        displayName: user.displayName
+      },
+      pubKeyCredParams: [{alg: -7, type: "public-key"}],
+      authenticatorSelection: {},
+      timeout: WEBAUTHN_TIMEOUT,
+      attestation: "none"
+    }
+  });
+}
+
+async function webauthn_authenticate() {
+  const response = await fetch("/.kittybox/webauthn/pre_auth");
+  const { challenge, credentials } = await response.json();
+
+  try {
+    return await navigator.credentials.get({
+      publicKey: {
+        challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+        allowCredentials: credentials.map(cred => ({
+          id: Uint8Array.from(cred.id, c => c.charCodeAt(0)),
+          type: cred.type
+        })),
+        timeout: WEBAUTHN_TIMEOUT
+      }
+    })
+  } catch (e) {
+    console.error("WebAuthn authentication failed:", e);
+    alert("Using your authenticator failed. (Check the DevTools for details)");
+    throw e;
+  }
+}
+
+async function submit_handler(e) {
+  e.preventDefault();
+  const form = e.target;
+
+  const scopes = Array.from(form.elements.scope)
+      .filter(e => e.checked)
+      .map(e => e.value);
+
+  const authorization_request = {
+    response_type: form.elements.response_type.value,
+    client_id: form.elements.client_id.value,
+    redirect_uri: form.elements.redirect_uri.value,
+    state: form.elements.state.value,
+    code_challenge: form.elements.code_challenge.value,
+    code_challenge_method: form.elements.code_challenge_method.value,
+    // I would love to leave that as a list, but such is the form of
+    // IndieAuth.  application/x-www-form-urlencoded doesn't have
+    // lists, so scopes are space-separated instead. It is annoying.
+    scope: scopes.length > 0 ? scopes.join(" ") : undefined,
+  };
+
+  let credential = null;
+  switch (form.elements.auth_method.value) {
+  case "password":
+    credential = form.elements.user_password.value;
+    if (credential.length == 0) {
+      alert("Please enter a password.")
+      return
+    }
+    break;
+  case "webauthn":
+    credential = await webauthn_authenticate();
+    break;
+  default:
+    alert("Please choose an authentication method.")
+    return;
+  }
+
+  console.log("Authorization request:", authorization_request);
+  console.log("Authentication method:", credential);
+
+  const body = JSON.stringify({
+    request: authorization_request,
+    authorization_method: credential
+  });
+  console.log(body);
+  
+  const response = await fetch(form.action, {
+    method: form.method,
+    body: body,
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+
+  if (response.ok) {
+    window.location.href = response.headers.get("Location")
+  }
+}
+
+document.getElementById("indieauth_page")
+  .addEventListener("submit", submit_handler);
diff --git a/kittybox-rs/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs
index 00d3ba6..0797ba6 100644
--- a/kittybox-rs/src/frontend/mod.rs
+++ b/kittybox-rs/src/frontend/mod.rs
@@ -282,6 +282,7 @@ pub async fn statics(Path(name): Path<String>) -> impl IntoResponse {
         "style.css" => (StatusCode::OK, [(CONTENT_TYPE, MIME_CSS)], STYLE_CSS),
         "onboarding.js" => (StatusCode::OK, [(CONTENT_TYPE, MIME_JS)], ONBOARDING_JS),
         "onboarding.css" => (StatusCode::OK, [(CONTENT_TYPE, MIME_CSS)], ONBOARDING_CSS),
+        "indieauth.js" => (StatusCode::OK, [(CONTENT_TYPE, MIME_JS)], INDIEAUTH_JS),
         _ => (
             StatusCode::NOT_FOUND,
             [(CONTENT_TYPE, MIME_PLAIN)],
diff --git a/kittybox-rs/src/frontend/style.css b/kittybox-rs/src/frontend/style.css
index 109bba0..a8ef6e4 100644
--- a/kittybox-rs/src/frontend/style.css
+++ b/kittybox-rs/src/frontend/style.css
@@ -177,7 +177,7 @@ article.h-card img.u-photo {
     aspect-ratio: 1;
 }
 
-.mini-h-card img {
+.mini-h-card img, #indieauth_page img {
     height: 2em;
     display: inline-block;
     border: 2px solid gray;
@@ -192,3 +192,10 @@ article.h-card img.u-photo {
 .mini-h-card a {
     text-decoration: none;
 }
+
+#indieauth_page > #introduction {
+    border: .125rem solid gray;
+    border-radius: .75rem;
+    margin: 1.25rem;
+    padding: .75rem;
+}