From 6487ae367321f168e4744f7bc03d64f4e167182f Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 28 Mar 2026 03:09:57 +0000 Subject: [PATCH] fix: cookies Secure flag based on X-Forwarded-Proto, not dev_mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Secure flag on session cookies broke HTTP LAN access — browsers refuse to send Secure cookies over plain HTTP, causing 401 redirect loop. Fix: check X-Forwarded-Proto header. Only set Secure when request came over HTTPS. HTTP on LAN works, HTTPS still gets Secure cookies. Co-Authored-By: Claude Opus 4.6 (1M context) --- core/archipelago/src/api/rpc/mod.rs | 55 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/core/archipelago/src/api/rpc/mod.rs b/core/archipelago/src/api/rpc/mod.rs index 774c3993..13f58059 100644 --- a/core/archipelago/src/api/rpc/mod.rs +++ b/core/archipelago/src/api/rpc/mod.rs @@ -144,8 +144,19 @@ impl RpcHandler { Arc::clone(&self.mesh_service) } - fn cookie_suffix(&self) -> &'static str { - if self.config.dev_mode { "" } else { "; Secure" } + fn cookie_suffix_for_request(&self, headers: &hyper::header::HeaderMap) -> &'static str { + // Only set Secure flag when the original request was over HTTPS. + // Nginx sends X-Forwarded-Proto: https for HTTPS connections. + // On LAN HTTP, Secure flag prevents browsers from sending cookies back. + if self.config.dev_mode { + return ""; + } + if let Some(proto) = headers.get("x-forwarded-proto") { + if proto.as_bytes() == b"https" { + return "; Secure"; + } + } + "" } pub async fn handle( @@ -155,6 +166,7 @@ impl RpcHandler { // Extract session cookie before consuming the request let (parts, body) = req.into_parts(); let session_token = session::extract_session_cookie(&parts.headers); + let secure_suffix = self.cookie_suffix_for_request(&parts.headers); let body_bytes = hyper::body::to_bytes(body).await .context("Failed to read body")?; @@ -327,6 +339,7 @@ impl RpcHandler { &login_params, &new_session_cookies, client_ip, + secure_suffix, ).await; Ok(response) @@ -372,6 +385,7 @@ impl RpcHandler { login_params: &Option, new_session_cookies: &Option<(String, String)>, client_ip: std::net::IpAddr, + secure_suffix: &str, ) { // Track failed login attempts for rate limiting if method == "auth.login" && rpc_resp.error.is_some() { @@ -391,8 +405,8 @@ impl RpcHandler { if let Ok(secret) = crate::totp::decrypt_secret(&totp_data, password) { let token = self.session_store.create_pending(secret).await; let csrf_token = derive_csrf_token(&token).await; - self.set_session_cookie(response, &token); - self.set_csrf_cookie(response, &csrf_token); + self.set_session_cookie(response, &token, secure_suffix); + self.set_csrf_cookie(response, &csrf_token, secure_suffix); let totp_body = serde_json::json!({ "result": { "requires_totp": true }, "error": null @@ -406,9 +420,9 @@ impl RpcHandler { let token = self.session_store.create().await; let csrf_token = derive_csrf_token(&token).await; let remember_token = self.session_store.create_remember_token().await; - self.set_session_cookie(response, &token); - self.set_csrf_cookie(response, &csrf_token); - self.set_remember_cookie(response, &remember_token); + self.set_session_cookie(response, &token, secure_suffix); + self.set_csrf_cookie(response, &csrf_token, secure_suffix); + self.set_remember_cookie(response, &remember_token, secure_suffix); } } @@ -426,9 +440,9 @@ impl RpcHandler { if let Some(new_token) = new_token_opt { let csrf_token = derive_csrf_token(&new_token).await; let remember_token = self.session_store.create_remember_token().await; - self.set_session_cookie(response, &new_token); - self.set_csrf_cookie(response, &csrf_token); - self.set_remember_cookie(response, &remember_token); + self.set_session_cookie(response, &new_token, secure_suffix); + self.set_csrf_cookie(response, &csrf_token, secure_suffix); + self.set_remember_cookie(response, &remember_token, secure_suffix); // Strip the token from the response body if let Some(result) = rpc_resp.result.as_mut() { if let Some(obj) = result.as_object_mut() { @@ -445,8 +459,8 @@ impl RpcHandler { if let Some(token) = session_token { let new_token = self.session_store.rotate(token).await; let csrf_token = derive_csrf_token(&new_token).await; - self.set_session_cookie(response, &new_token); - self.set_csrf_cookie(response, &csrf_token); + self.set_session_cookie(response, &new_token, secure_suffix); + self.set_csrf_cookie(response, &csrf_token, secure_suffix); } } @@ -455,7 +469,6 @@ impl RpcHandler { if let Some(token) = session_token { self.session_store.remove(token).await; } - let secure_suffix = if self.config.dev_mode { "" } else { "; Secure" }; response.headers_mut().append( "Set-Cookie", cookie_header(&format!("session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{}", secure_suffix)), @@ -468,29 +481,29 @@ impl RpcHandler { // If session was auto-restored from remember-me, set new cookies if let Some((new_session, new_csrf)) = new_session_cookies { - self.set_session_cookie(response, new_session); - self.set_csrf_cookie(response, new_csrf); + self.set_session_cookie(response, new_session, secure_suffix); + self.set_csrf_cookie(response, new_csrf, secure_suffix); } } - fn set_session_cookie(&self, response: &mut Response, token: &str) { + fn set_session_cookie(&self, response: &mut Response, token: &str, secure_suffix: &str) { response.headers_mut().append( "Set-Cookie", - cookie_header(&format!("session={}; HttpOnly; SameSite=Lax; Path=/{}", token, self.cookie_suffix())), + cookie_header(&format!("session={}; HttpOnly; SameSite=Lax; Path=/{}", token, secure_suffix)), ); } - fn set_csrf_cookie(&self, response: &mut Response, csrf_token: &str) { + fn set_csrf_cookie(&self, response: &mut Response, csrf_token: &str, secure_suffix: &str) { response.headers_mut().append( "Set-Cookie", - cookie_header(&format!("csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, self.cookie_suffix())), + cookie_header(&format!("csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, secure_suffix)), ); } - fn set_remember_cookie(&self, response: &mut Response, remember_token: &str) { + fn set_remember_cookie(&self, response: &mut Response, remember_token: &str, secure_suffix: &str) { response.headers_mut().append( "Set-Cookie", - cookie_header(&format!("remember={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}", remember_token, REMEMBER_TTL, self.cookie_suffix())), + cookie_header(&format!("remember={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}", remember_token, REMEMBER_TTL, secure_suffix)), ); } }