fix: session_expired after login on HTTP deployments (#241)
Three changes to fix the immediate-logout issue reported by multiple Docker users: 1. Add explicit `credentials: 'same-origin'` to the login fetch call. This was the only fetch in the entire codebase missing it. While modern browsers default to 'same-origin', some privacy configs or older engines may default to 'omit', silently dropping Set-Cookie headers from the login response. 2. Post-login cookie verification: after a successful login, the frontend now checks that the CSRF cookie (non-HttpOnly, readable by JS) was actually stored before redirecting. If the browser rejected the cookies, a clear error message is shown explaining the OXICLOUD_COOKIE_SECURE / HTTP mismatch. 3. Server-side diagnostic: the login handler now warns in logs when Secure cookies are set on a request that didn't arrive via HTTPS (no X-Forwarded-Proto: https header), pointing admins to the OXICLOUD_COOKIE_SECURE=false fix. Root cause: users who set OXICLOUD_BASE_URL=https://... (or have OXICLOUD_COOKIE_SECURE=true) but access via plain HTTP get cookies with the Secure flag, which browsers silently reject over HTTP.
This commit is contained in:
@@ -34,6 +34,10 @@ pub const CSRF_HEADER: &str = "x-csrf-token";
|
|||||||
/// 4. **Default: `false`** for compatibility with HTTP deployments
|
/// 4. **Default: `false`** for compatibility with HTTP deployments
|
||||||
/// (Docker, local development). Set `OXICLOUD_COOKIE_SECURE=true`
|
/// (Docker, local development). Set `OXICLOUD_COOKIE_SECURE=true`
|
||||||
/// explicitly for production HTTPS environments.
|
/// explicitly for production HTTPS environments.
|
||||||
|
pub fn is_cookie_secure() -> bool {
|
||||||
|
cookie_secure()
|
||||||
|
}
|
||||||
|
|
||||||
fn cookie_secure() -> bool {
|
fn cookie_secure() -> bool {
|
||||||
if let Ok(v) = std::env::var("OXICLOUD_COOKIE_SECURE") {
|
if let Ok(v) = std::env::var("OXICLOUD_COOKIE_SECURE") {
|
||||||
let secure = v == "true" || v == "1";
|
let secure = v == "true" || v == "1";
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ async fn register(
|
|||||||
|
|
||||||
async fn login(
|
async fn login(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
headers: HeaderMap,
|
||||||
Json(dto): Json<LoginDto>,
|
Json(dto): Json<LoginDto>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// Add detailed logging for debugging
|
// Add detailed logging for debugging
|
||||||
@@ -204,6 +205,25 @@ async fn login(
|
|||||||
state.core.config.auth.refresh_token_expiry_secs,
|
state.core.config.auth.refresh_token_expiry_secs,
|
||||||
);
|
);
|
||||||
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
|
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
|
||||||
|
|
||||||
|
// Diagnostic: warn when Secure cookies are set but the request
|
||||||
|
// arrived over plain HTTP — the browser will reject them (#241).
|
||||||
|
if cookie_auth::is_cookie_secure() {
|
||||||
|
let is_tls = headers
|
||||||
|
.get("x-forwarded-proto")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.is_some_and(|p| p.eq_ignore_ascii_case("https"));
|
||||||
|
if !is_tls {
|
||||||
|
tracing::warn!(
|
||||||
|
"Login for '{}': Secure cookies are enabled but the request \
|
||||||
|
does not appear to be over HTTPS (no X-Forwarded-Proto: https). \
|
||||||
|
The browser may reject the cookies. Set OXICLOUD_COOKIE_SECURE=false \
|
||||||
|
in .env if you access OxiCloud via plain HTTP.",
|
||||||
|
dto.username,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -898,7 +898,21 @@ if (isLoginPage && loginForm) {
|
|||||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user));
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to main app
|
// Redirect to main app — but first verify the browser accepted
|
||||||
|
// the auth cookies. The CSRF cookie (oxicloud_csrf) is non-HttpOnly
|
||||||
|
// so JS can read it. If it's missing the browser rejected the
|
||||||
|
// Set-Cookie (usually because of Secure flag over plain HTTP).
|
||||||
|
const csrfStored = document.cookie.split('; ').some(c => c.startsWith('oxicloud_csrf='));
|
||||||
|
if (!csrfStored) {
|
||||||
|
console.error('Auth cookies were NOT stored by the browser. '
|
||||||
|
+ 'This usually means OXICLOUD_COOKIE_SECURE=true (or OXICLOUD_BASE_URL=https://...) '
|
||||||
|
+ 'is set but you are accessing via plain HTTP.');
|
||||||
|
loginError.textContent = 'Login succeeded but the browser rejected the session cookie. '
|
||||||
|
+ 'If you are accessing via HTTP, set OXICLOUD_COOKIE_SECURE=false in your .env file '
|
||||||
|
+ 'or access via HTTPS through a reverse proxy.';
|
||||||
|
loginError.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
redirectToMainApp();
|
redirectToMainApp();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loginError.textContent = error.message || 'Error logging in';
|
loginError.textContent = error.message || 'Error logging in';
|
||||||
@@ -1030,6 +1044,7 @@ async function login(username, password) {
|
|||||||
|
|
||||||
const response = await fetch(LOGIN_ENDPOINT, {
|
const response = await fetch(LOGIN_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...getCsrfHeaders()
|
...getCsrfHeaders()
|
||||||
|
|||||||
Reference in New Issue
Block a user