fix(webview): set kiosk auth cookie for sub-resource requests

WebView "URL can't be shown" — Authorization header only applies to
the initial page load. CSS/JS/XHR/WebSocket sub-resources from the
loaded page don't inherit it → Angie auth_request rejects → page breaks.

Kiosk side: set_kiosk_cookie() injects betterframe_kiosk_key cookie
into WebKit's cookie jar via JS bridge before loading the URL. Cookie
persists across all sub-resource requests automatically.

Server side: extractBearerToken() now checks betterframe_kiosk_key
cookie as fallback when no Authorization header present. Same
verifyKioskKey path, just different transport.
This commit is contained in:
Mitchell R 2026-05-23 01:23:56 +02:00
parent a513d165dc
commit 592bdad10b
No known key found for this signature in database
2 changed files with 59 additions and 2 deletions

View file

@ -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::<&gtk::gio::Cancellable>, |_| {});
});
}
fn should_attach_kiosk_auth(url: &str, server_url: &str) -> bool {
let Ok(target) = Url::parse(url) else {
return false;

View file

@ -181,8 +181,18 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, 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 {