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)
}
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<serde_json::Value>,
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<hyper::Body>, token: &str) {
fn set_session_cookie(&self, response: &mut Response<hyper::Body>, 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<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(
"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(
"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)),
);
}
}