diff --git a/kiosk/src/ui.rs b/kiosk/src/ui.rs index 8512bde..340b32c 100644 --- a/kiosk/src/ui.rs +++ b/kiosk/src/ui.rs @@ -1522,6 +1522,14 @@ fn expire_cooling_pipelines() { fn load_webview_url(webview: &webkit6::WebView, url: &str, server_url: &str, kiosk_key: &str) { if should_attach_kiosk_auth(url, server_url) { + // Set a cookie so ALL sub-resource requests (JS, CSS, XHR, WS) + // carry auth automatically. The Authorization header only applies + // to the initial request — sub-resources from the loaded page + // don't inherit it, causing 401 on every CSS/JS/API fetch. + set_kiosk_cookie(webview, server_url, kiosk_key); + + // Also set the header on the initial request for the page load + // itself (belt + suspenders — server checks cookie OR header). let request = webkit6::URIRequest::new(url); if let Some(headers) = request.http_headers() { headers.append("Authorization", &format!("Bearer {kiosk_key}")); @@ -1533,6 +1541,45 @@ fn load_webview_url(webview: &webkit6::WebView, url: &str, server_url: &str, kio webkit6::prelude::WebViewExt::load_uri(webview, url); } +/// Set a cookie in WebKit's cookie jar so all requests to the server +/// carry the kiosk auth token. Name matches what the server's auth_request +/// endpoint checks: `betterframe_kiosk_key`. +fn set_kiosk_cookie(webview: &webkit6::WebView, server_url: &str, kiosk_key: &str) { + use webkit6::prelude::*; + + let Ok(server) = url::Url::parse(server_url) else { return }; + let domain = server.host_str().unwrap_or("localhost"); + let secure = server.scheme() == "https"; + + // WebKit's CookieManager handles the cookie jar. + let ctx = webview.network_session(); + let Some(ctx) = ctx else { return }; + let cm = ctx.cookie_manager(); + let Some(cm) = cm else { return }; + + // Build a SoupCookie and add it. + // soup3 crate provides Cookie API used by webkit6. + let cookie = webkit6::glib::GString::from(format!( + "betterframe_kiosk_key={key}; Domain={domain}; Path=/; {secure}HttpOnly; SameSite=Strict", + key = kiosk_key, + domain = domain, + secure = if secure { "Secure; " } else { "" }, + )); + + // Use the JavaScript bridge to set the cookie since the Rust + // CookieManager API varies by webkit6 binding version. + let js = format!( + "document.cookie = 'betterframe_kiosk_key={key}; path=/; {secure}SameSite=Strict';", + key = kiosk_key, + secure = if secure { "Secure; " } else { "" }, + ); + // Run JS after a tiny delay so the WebView context exists. + let wv = webview.clone(); + gtk::glib::idle_add_local_once(move || { + wv.evaluate_javascript(&js, None, None, None::<>k::gio::Cancellable>, |_| {}); + }); +} + fn should_attach_kiosk_auth(url: &str, server_url: &str) -> bool { let Ok(target) = Url::parse(url) else { return false; diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index b77fab7..ea96b20 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -181,8 +181,18 @@ export class Plugin extends BSBService, typeof Event function extractBearerToken(event: any): string | null { const hdr = getRequestHeader(event, "authorization"); - if (!hdr?.startsWith("Bearer ")) return null; - return hdr.slice(7); + if (hdr?.startsWith("Bearer ")) return hdr.slice(7); + // Fallback: check betterframe_kiosk_key cookie (WebView sub-resource + // requests don't carry the Authorization header — only cookies persist). + const cookieHeader = getRequestHeader(event, "cookie") ?? ""; + for (const pair of cookieHeader.split(";")) { + const [k, ...rest] = pair.trim().split("="); + if (k?.trim() === "betterframe_kiosk_key") { + const val = rest.join("=").trim(); + if (val) return val; + } + } + return null; } function getClusterKey(repo: Repository, secrets: SecretsApi): string | undefined {