From 66049566ae865e1a4bd049257d6afc0abded16e9 Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 19 Sep 2022 17:30:38 +0300 Subject: 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) --- kittybox-rs/src/frontend/indieauth.js | 107 ++++++++++++++++++++++++++++++++++ kittybox-rs/src/frontend/mod.rs | 1 + kittybox-rs/src/frontend/style.css | 9 ++- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 kittybox-rs/src/frontend/indieauth.js (limited to 'kittybox-rs/src/frontend') 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) -> 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; +} -- cgit 1.4.1