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:
parent
ab3310faac
commit
6487ae3673
@ -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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user