fix: cookies Secure flag based on X-Forwarded-Proto, not dev_mode

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) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-28 03:09:57 +00:00
parent ab3310faac
commit 6487ae3673

View File

@ -144,8 +144,19 @@ impl RpcHandler {
Arc::clone(&self.mesh_service) Arc::clone(&self.mesh_service)
} }
fn cookie_suffix(&self) -> &'static str { fn cookie_suffix_for_request(&self, headers: &hyper::header::HeaderMap) -> &'static str {
if self.config.dev_mode { "" } else { "; Secure" } // 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( pub async fn handle(
@ -155,6 +166,7 @@ impl RpcHandler {
// Extract session cookie before consuming the request // Extract session cookie before consuming the request
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let session_token = session::extract_session_cookie(&parts.headers); 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 let body_bytes = hyper::body::to_bytes(body).await
.context("Failed to read body")?; .context("Failed to read body")?;
@ -327,6 +339,7 @@ impl RpcHandler {
&login_params, &login_params,
&new_session_cookies, &new_session_cookies,
client_ip, client_ip,
secure_suffix,
).await; ).await;
Ok(response) Ok(response)
@ -372,6 +385,7 @@ impl RpcHandler {
login_params: &Option<serde_json::Value>, login_params: &Option<serde_json::Value>,
new_session_cookies: &Option<(String, String)>, new_session_cookies: &Option<(String, String)>,
client_ip: std::net::IpAddr, client_ip: std::net::IpAddr,
secure_suffix: &str,
) { ) {
// Track failed login attempts for rate limiting // Track failed login attempts for rate limiting
if method == "auth.login" && rpc_resp.error.is_some() { 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) { if let Ok(secret) = crate::totp::decrypt_secret(&totp_data, password) {
let token = self.session_store.create_pending(secret).await; let token = self.session_store.create_pending(secret).await;
let csrf_token = derive_csrf_token(&token).await; let csrf_token = derive_csrf_token(&token).await;
self.set_session_cookie(response, &token); self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token); self.set_csrf_cookie(response, &csrf_token, secure_suffix);
let totp_body = serde_json::json!({ let totp_body = serde_json::json!({
"result": { "requires_totp": true }, "result": { "requires_totp": true },
"error": null "error": null
@ -406,9 +420,9 @@ impl RpcHandler {
let token = self.session_store.create().await; let token = self.session_store.create().await;
let csrf_token = derive_csrf_token(&token).await; let csrf_token = derive_csrf_token(&token).await;
let remember_token = self.session_store.create_remember_token().await; let remember_token = self.session_store.create_remember_token().await;
self.set_session_cookie(response, &token); self.set_session_cookie(response, &token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token); self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token); self.set_remember_cookie(response, &remember_token, secure_suffix);
} }
} }
@ -426,9 +440,9 @@ impl RpcHandler {
if let Some(new_token) = new_token_opt { if let Some(new_token) = new_token_opt {
let csrf_token = derive_csrf_token(&new_token).await; let csrf_token = derive_csrf_token(&new_token).await;
let remember_token = self.session_store.create_remember_token().await; let remember_token = self.session_store.create_remember_token().await;
self.set_session_cookie(response, &new_token); self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token); self.set_csrf_cookie(response, &csrf_token, secure_suffix);
self.set_remember_cookie(response, &remember_token); self.set_remember_cookie(response, &remember_token, secure_suffix);
// Strip the token from the response body // Strip the token from the response body
if let Some(result) = rpc_resp.result.as_mut() { if let Some(result) = rpc_resp.result.as_mut() {
if let Some(obj) = result.as_object_mut() { if let Some(obj) = result.as_object_mut() {
@ -445,8 +459,8 @@ impl RpcHandler {
if let Some(token) = session_token { if let Some(token) = session_token {
let new_token = self.session_store.rotate(token).await; let new_token = self.session_store.rotate(token).await;
let csrf_token = derive_csrf_token(&new_token).await; let csrf_token = derive_csrf_token(&new_token).await;
self.set_session_cookie(response, &new_token); self.set_session_cookie(response, &new_token, secure_suffix);
self.set_csrf_cookie(response, &csrf_token); self.set_csrf_cookie(response, &csrf_token, secure_suffix);
} }
} }
@ -455,7 +469,6 @@ impl RpcHandler {
if let Some(token) = session_token { if let Some(token) = session_token {
self.session_store.remove(token).await; self.session_store.remove(token).await;
} }
let secure_suffix = if self.config.dev_mode { "" } else { "; Secure" };
response.headers_mut().append( response.headers_mut().append(
"Set-Cookie", "Set-Cookie",
cookie_header(&format!("session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{}", secure_suffix)), 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 session was auto-restored from remember-me, set new cookies
if let Some((new_session, new_csrf)) = new_session_cookies { if let Some((new_session, new_csrf)) = new_session_cookies {
self.set_session_cookie(response, new_session); self.set_session_cookie(response, new_session, secure_suffix);
self.set_csrf_cookie(response, new_csrf); self.set_csrf_cookie(response, new_csrf, secure_suffix);
} }
} }
fn set_session_cookie(&self, response: &mut Response<hyper::Body>, token: &str) { fn set_session_cookie(&self, response: &mut Response<hyper::Body>, token: &str, secure_suffix: &str) {
response.headers_mut().append( response.headers_mut().append(
"Set-Cookie", "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<hyper::Body>, csrf_token: &str) { fn set_csrf_cookie(&self, response: &mut Response<hyper::Body>, csrf_token: &str, secure_suffix: &str) {
response.headers_mut().append( response.headers_mut().append(
"Set-Cookie", "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<hyper::Body>, remember_token: &str) { fn set_remember_cookie(&self, response: &mut Response<hyper::Body>, remember_token: &str, secure_suffix: &str) {
response.headers_mut().append( response.headers_mut().append(
"Set-Cookie", "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)),
); );
} }
} }