diff --git a/who-am-i/src/expiring_task_map.rs b/who-am-i/src/expiring_task_map.rs index 9a8f3ba..7277b71 100644 --- a/who-am-i/src/expiring_task_map.rs +++ b/who-am-i/src/expiring_task_map.rs @@ -49,8 +49,8 @@ impl ExpiringTaskMap { .run_until_cancelled(sleep(expiration)) .await .is_some() + // the (sleep) task completed first { - // is Some if the (sleep) task completed first map.remove(&k); cancel.cancel(); metrics::counter!("whoami_task_map_completions", "result" => "expired") @@ -62,9 +62,14 @@ impl ExpiringTaskMap { } pub fn take(&self, key: &str) -> Option> { - metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1); - // when the _guard drops, the token gets cancelled for us - self.0.map.remove(key).map(|(_, (_guard, handle))| handle) + if let Some((_key, (_guard, handle))) = self.0.map.remove(key) { + // when the _guard drops, it cancels the token for us + metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1); + Some(handle) + } else { + metrics::counter!("whoami_task_map_gones").increment(1); + None + } } } diff --git a/who-am-i/src/server.rs b/who-am-i/src/server.rs index eb5bbbe..96c1dd5 100644 --- a/who-am-i/src/server.rs +++ b/who-am-i/src/server.rs @@ -1,10 +1,10 @@ use atrium_api::types::string::Did; use axum::{ Router, - extract::{FromRef, Query, State}, + extract::{FromRef, Json as ExtractJson, Query, State}, http::{ StatusCode, - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS}, + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER}, }, response::{IntoResponse, Json, Redirect, Response}, routing::{get, post}, @@ -87,7 +87,7 @@ pub async fn serve( .route("/favicon.ico", get(favicon)) // todo MIME .route("/style.css", get(css)) .route("/prompt", get(prompt)) - .route("/user-info", get(user_info)) + .route("/user-info", post(user_info)) .route("/auth", get(start_oauth)) .route("/authorized", get(complete_oauth)) .route("/disconnect", post(disconnect)) @@ -137,10 +137,7 @@ async fn hello( } else { json!({}) }; - let frame_headers = [ - (X_FRAME_OPTIONS, "deny"), - (CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"), - ]; + let frame_headers = [(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'")]; (frame_headers, jar, RenderHtml("hello", engine, info)).into_response() } @@ -205,13 +202,8 @@ async fn prompt( return err("Referer origin is opaque", true); } - let frame_headers = [ - (X_FRAME_OPTIONS, format!("allow-from {parent_origin}")), - ( - CONTENT_SECURITY_POLICY, - format!("frame-ancestors {parent_origin}"), - ), - ]; + let csp = format!("frame-ancestors {parent_origin}"); + let frame_headers = [(CONTENT_SECURITY_POLICY, &csp)]; if let Some(did) = jar.get(DID_COOKIE_KEY) { let Ok(did) = Did::new(did.value_trimmed().to_string()) else { @@ -258,7 +250,6 @@ async fn prompt( } #[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] struct UserInfoParams { fetch_key: String, } @@ -266,7 +257,7 @@ async fn user_info( State(AppState { resolve_handles, .. }): State, - Query(params): Query, + ExtractJson(params): ExtractJson, ) -> impl IntoResponse { let err = |status, reason: &str| { metrics::counter!("whoami_user_info", "found" => "false", "reason" => reason.to_string()) diff --git a/who-am-i/templates/hello.hbs b/who-am-i/templates/hello.hbs index 291b866..1009eeb 100644 --- a/who-am-i/templates/hello.hbs +++ b/who-am-i/templates/hello.hbs @@ -38,7 +38,6 @@ ({{{json did}}}) && (async () => { const handle = await lookUp({{{json fetch_key}}}); - console.log('got handle', handle); loaderEl.classList.add('hidden'); handleViewEl.textContent = `@${handle}`; @@ -54,11 +53,13 @@ })(); async function lookUp(fetch_key) { - const user_info = new URL('/user-info', window.location); - user_info.searchParams.set('fetch-key', fetch_key); let info; try { - const resp = await fetch(user_info); + const resp = await fetch('/user-info', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ fetch_key }), + }); if (!resp.ok) throw resp; info = await resp.json(); } catch (e) { diff --git a/who-am-i/templates/prompt.hbs b/who-am-i/templates/prompt.hbs index 05b6f59..9e0278e 100644 --- a/who-am-i/templates/prompt.hbs +++ b/who-am-i/templates/prompt.hbs @@ -49,10 +49,7 @@ function err(e, msg) { // already-known user ({{{json did}}}) && (async () => { - const handle = await lookUp({{{json fetch_key}}}); - console.log('got handle', handle); - loaderEl.classList.add('hidden'); handleViewEl.textContent = `@${handle}`; allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); @@ -74,20 +71,15 @@ window.addEventListener('storage', async e => { // so if you have two flows going, it grants for both (or the first responder?) if you grant for either. // (letting this slide while parent pages are allowlisted to microcosm only) - const fail = (e, msg) => { - loaderEl.classList.add('hidden'); - formEl.classList.remove('hidden'); - handleInputEl.focus(); - handleInputEl.select(); - err(e, msg); - } + if (e.key !== 'who-am-i') return; + if (e.newValue === null) return; - const details = localStorage.getItem("who-am-i"); + const details = e.newValue; if (!details) { - console.error("hmm, heard from localstorage but did not get DID"); - return; + console.error("hmm, heard from localstorage but did not get DID", details, e); + err('sorry, something went wrong getting your details'); } - localStorage.removeItem("who-am-i"); + localStorage.removeItem(e.key); let parsed; try { @@ -96,6 +88,14 @@ window.addEventListener('storage', async e => { err(e, "something went wrong getting the details back"); } + const fail = (e, msg) => { + loaderEl.classList.add('hidden'); + formEl.classList.remove('hidden'); + handleInputEl.focus(); + handleInputEl.select(); + err(e, msg); + } + if (parsed.result === "fail") { fail(`uh oh: ${parsed.reason}`); } @@ -108,19 +108,21 @@ window.addEventListener('storage', async e => { const handle = await lookUp(parsed.fetch_key); - shareAllow(handle, token); + shareAllow(handle, parsed.token); }); async function lookUp(fetch_key) { - const user_info = new URL('/user-info', window.location); - user_info.searchParams.set('fetch-key', fetch_key); let info; try { - const resp = await fetch(user_info); + const resp = await fetch('/user-info', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fetch_key }), + }); if (!resp.ok) throw resp; info = await resp.json(); } catch (e) { - err(e, 'failed to resolve handle from DID') + err(e, `failed to resolve handle from DID with ${fetch_key}`); } return info.handle; } @@ -130,6 +132,7 @@ const shareAllow = (handle, token) => { { action: "allow", handle, token }, {{{json parent_origin}}}, ); + promptEl.textContent = '✔️ shared'; } const shareDeny = reason => {