diff --git a/app/MindWork AI Studio/Tools/Security/AppAccessMiddleware.cs b/app/MindWork AI Studio/Tools/Security/AppAccessMiddleware.cs index 3711600de..bfb2a11c2 100644 --- a/app/MindWork AI Studio/Tools/Security/AppAccessMiddleware.cs +++ b/app/MindWork AI Studio/Tools/Security/AppAccessMiddleware.cs @@ -86,7 +86,7 @@ public sealed class AppAccessMiddleware(RequestDelegate next, AppAccessPolicy po context.Response.Cookies.Append(AppAccessPolicy.SESSION_COOKIE_NAME, token, new CookieOptions { HttpOnly = true, - SameSite = SameSiteMode.Strict, + SameSite = SameSiteMode.Lax, Path = "/", IsEssential = true }); diff --git a/app/Tests/Tools/AppAccess/AppAccessMiddlewareTests.cs b/app/Tests/Tools/AppAccess/AppAccessMiddlewareTests.cs index 1f73a3dab..ddf3a05e6 100644 --- a/app/Tests/Tools/AppAccess/AppAccessMiddlewareTests.cs +++ b/app/Tests/Tools/AppAccess/AppAccessMiddlewareTests.cs @@ -141,7 +141,7 @@ public sealed class AppAccessMiddlewareTests Assert.That(response.Headers.Location.ToString(), Is.EqualTo("/")); Assert.That(cookie, Does.StartWith($"{AppAccessPolicy.SESSION_COOKIE_NAME}={UI_TOKEN}")); Assert.That(cookie, Does.Contain("httponly")); - Assert.That(cookie, Does.Contain("samesite=strict")); + Assert.That(cookie, Does.Contain("samesite=lax")); Assert.That(cookie, Does.Contain("path=/")); Assert.That(cookie, Does.Not.Contain("expires")); }); diff --git a/runtime/src/app_access.rs b/runtime/src/app_access.rs index 9a618d877..54fda55bf 100644 --- a/runtime/src/app_access.rs +++ b/runtime/src/app_access.rs @@ -25,12 +25,20 @@ const DEV_BOOT_LINK_FILE: &str = "dev-boot-link.txt"; /// Builds the session cookie of the app window. /// /// The cookie is for the app host alone and lives as long as the window: it has no expiry date, -/// scripts cannot read it, and the window never sends it with a request which another site -/// started. We parse it from text because the `cookie` crate, whose builder would need its +/// and scripts cannot read it. The window never sends it with a request which a page of another +/// site makes, such as a script the page loads or a form it posts. +/// +/// The cookie is `Lax`, not `Strict`. Before the window opens the app, it shows a page of Tauri, +/// `tauri://localhost` on macOS, which is another site than the app. WKWebView counts that page +/// as the one which opened the app, and it would withhold a `Strict` cookie. A `Lax` cookie goes +/// along when a whole page of the app opens. Development builds show the page of the Tauri dev +/// server on the app host instead, which hides the difference. +/// +/// We parse the cookie from text because the `cookie` crate, whose builder would need its /// `SameSite` type, is no direct dependency of ours. pub fn session_cookie() -> Cookie<'static> { let token = UI_ACCESS_TOKEN.to_hex_text(); - Cookie::parse(format!("{SESSION_COOKIE_NAME}={token}; Domain={APP_HOST}; Path=/; HttpOnly; SameSite=Strict")) + Cookie::parse(format!("{SESSION_COOKIE_NAME}={token}; Domain={APP_HOST}; Path=/; HttpOnly; SameSite=Lax")) .expect("The session cookie consists of fixed attributes and a hexadecimal token, which always parse.") } @@ -132,7 +140,7 @@ mod tests { let cookie = session_cookie(); assert_eq!(cookie.http_only(), Some(true)); - assert!(cookie.same_site().is_some_and(|same_site| same_site.is_strict())); + assert!(cookie.same_site().is_some_and(|same_site| same_site.is_lax())); } #[test] diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 17bbe1067..e665a5a0b 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -708,10 +708,9 @@ pub async fn change_location_to(url: tauri::Url) { // // The window navigates by itself instead of through a script of the page it shows. That page - // comes from Tauri, which is another site than the app, so the app would count as opened by - // another site, and the window would withhold the session cookie. We take the window out of - // the lock first: the cookie and the navigation need the main thread, which may wait for this - // lock meanwhile. + // comes from Tauri, which is another site than the app; app_access::session_cookie explains + // why the session cookie goes along all the same. We take the window out of the lock first: + // the cookie and the navigation need the main thread, which may wait for this lock meanwhile. // // Handing over the cookie waits for the main thread. This function serves a request of the // runtime API, so the waiting happens outside the async runtime, see runtime/AGENTS.md.