diff --git a/who-am-i/src/server.rs b/who-am-i/src/server.rs index 64f682e..9e24733 100644 --- a/who-am-i/src/server.rs +++ b/who-am-i/src/server.rs @@ -5,7 +5,7 @@ use axum::{ extract::{FromRef, Json as ExtractJson, Query, State}, http::{ StatusCode, - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER}, + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, ORIGIN, REFERER}, }, response::{IntoResponse, Json, Redirect, Response}, routing::{get, post}, @@ -211,6 +211,11 @@ fn cookie(did: &Did) -> Cookie<'static> { .into() } +#[derive(Debug, Deserialize)] +struct PromptQuery { + // this must *ONLY* be used for the postmessage target origin + app: Option, +} async fn prompt( State(AppState { allowed_hosts, @@ -221,42 +226,64 @@ async fn prompt( tokens, .. }): State, + Query(params): Query, jar: SignedCookieJar, headers: HeaderMap, ) -> impl IntoResponse { - let err = |reason, check_frame| { + let err = |reason, check_frame, detail| { metrics::counter!("whoami_auth_prompt", "ok" => "false", "reason" => reason).increment(1); - let info = json!({ "reason": reason, "check_frame": check_frame }); + let info = json!({ + "reason": reason, + "check_frame": check_frame, + "detail": detail, + }); let html = RenderHtml("prompt-error", engine.clone(), info); (StatusCode::BAD_REQUEST, html).into_response() }; - let Some(referrer) = headers.get(REFERER) else { - return err("Missing referer", true); + let Some(parent) = headers.get(ORIGIN).or_else(|| { + eprintln!("referrer fallback"); + // TODO: referer should only be used for localhost?? + headers.get(REFERER) + }) else { + return err("Missing origin and no referrer for fallback", true, None); }; - let Ok(referrer) = referrer.to_str() else { - return err("Unreadable referer", true); + let Ok(parent) = parent.to_str() else { + return err("Unreadable origin or referrer", true, None); }; - let Ok(url) = Url::parse(referrer) else { - return err("Bad referer", true); + eprintln!( + "rolling with parent: {parent:?} (from origin? {})", + headers.get(ORIGIN).is_some() + ); + let Ok(url) = Url::parse(parent) else { + return err("Bad origin or referrer", true, None); }; let Some(parent_host) = url.host_str() else { - return err("Referer missing host", true); + return err("Origin or referrer missing host", true, None); }; if !allowed_hosts.contains(parent_host) { - return err("Login is not allowed on this page", false); + return err( + "Login is not allowed on this page", + false, + Some(parent_host), + ); } let parent_origin = url.origin().ascii_serialization(); if parent_origin == "null" { - return err("Referer origin is opaque", true); + return err("Origin or referrer header value is opaque", true, None); } - let csp = format!("frame-ancestors {parent_origin}"); + let all_allowed = allowed_hosts + .iter() + .map(|h| format!("https://{h}")) + .collect::>() + .join(" "); + let csp = format!("frame-ancestors 'self' {parent_origin} {all_allowed}"); 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 { - return err("Bad cookie", false); + return err("Bad cookie", false, None); }; // push cookie expiry @@ -266,7 +293,7 @@ async fn prompt( Ok(t) => t, Err(e) => { eprintln!("failed to create JWT: {e:?}"); - return err("failed to create JWT", false); + return err("failed to create JWT", false, None); } }; @@ -286,6 +313,7 @@ async fn prompt( "fetch_key": fetch_key, "parent_host": parent_host, "parent_origin": parent_origin, + "parent_target": params.app.map(|h| format!("https://{h}")), }); (frame_headers, jar, RenderHtml("prompt", engine, info)).into_response() } else { diff --git a/who-am-i/static/style.css b/who-am-i/static/style.css index 6d732dc..36a3a5c 100644 --- a/who-am-i/static/style.css +++ b/who-am-i/static/style.css @@ -12,7 +12,7 @@ body { overflow: hidden; display: flex; flex-direction: column; - height: 100vh; + min-height: 100vh; } .wrap.unframed { border-radius: 0; diff --git a/who-am-i/templates/prompt-error.hbs b/who-am-i/templates/prompt-error.hbs index 91672ad..26fa8c4 100644 --- a/who-am-i/templates/prompt-error.hbs +++ b/who-am-i/templates/prompt-error.hbs @@ -2,6 +2,7 @@

Something went wrong :(

{{ reason }}

+

{{ detail }}

diff --git a/who-am-i/templates/prompt.hbs b/who-am-i/templates/prompt.hbs index 14b97bf..27d2499 100644 --- a/who-am-i/templates/prompt.hbs +++ b/who-am-i/templates/prompt.hbs @@ -89,7 +89,10 @@ if ('hasStorageAccess' in document) { cookies: true, localStorage: true, }).then( - () => desperation.textContent = "(maybe helped?)", + () => { + desperation.textContent = "(maybe helped?)"; + setTimeout(() => location.reload(), 350); + }, () => desperation.textContent = "(doubtful)", ); }) @@ -157,18 +160,24 @@ async function lookUp(fetch_key) { return info.handle; } +const parentTarget = {{{json parent_target}}} ?? {{{json parent_origin}}}; + const shareAllow = (handle, token) => { - top.postMessage( - { action: "allow", handle, token }, - {{{json parent_origin}}}, - ); + try { + top.postMessage( + { action: "allow", handle, token }, + parentTarget, + ); + } catch (e) { + err(e, 'Identity verified but failed to connect with app'); + }; promptEl.textContent = '✔️ shared'; } const shareDeny = reason => { top.postMessage( { action: "deny", reason }, - {{{json parent_origin}}}, + parentTarget, ); }