Make the Secure cookie attribute configurable for TLS-less dev stands
Session cookies were hardcoded Secure, which is correct for prod but silently breaks every non-localhost HTTP dev deployment: browsers (and any spec-compliant HTTP client, verified with curl against a container IP and against dev.netrunner-vpn.com directly) refuse to store a Secure cookie received over plain HTTP unless the host is localhost. The dev VPS serves everything over plain HTTP with no reverse-proxy/TLS by design, so every login there was silently failing to persist a session at all — this is a bigger, more fundamental gap than the cookie-sharing-with-the- landing question that led to finding it. Added COOKIE_SECURE (defaults true) alongside the existing COOKIE_DOMAIN, logs a loud warning when disabled, and turned it off for the dev-remote and local docker-dev env templates. Verified end-to-end against a non-localhost address that the cookie now actually gets stored and a follow-up authenticated request succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,20 +53,37 @@ fn cookie_domain_attr(state: &ApiState) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// `; Secure` почти всегда обязателен — но на dev-стендах без TLS браузер
|
||||
/// молча выбрасывает Secure-cookie, полученную по голому HTTP (кроме
|
||||
/// `localhost`), и сессия не переживает ни одного запроса. См. `ApiState::cookie_secure`.
|
||||
fn cookie_secure_attr(state: &ApiState) -> &'static str {
|
||||
if state.cookie_secure {
|
||||
"; Secure"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Ставит обе сессионные cookie: короткоживущий access-JWT на весь `/`, и
|
||||
/// непрозрачный refresh-токен, ограниченный путём `/api/v1/auth/refresh`
|
||||
/// (чтобы он не улетал на каждый обычный запрос — там он не нужен, а
|
||||
/// поверхность атаки только растёт).
|
||||
fn set_session_cookies(headers: &mut HeaderMap, state: &ApiState, session: &Session) {
|
||||
let domain = cookie_domain_attr(state);
|
||||
let secure = cookie_secure_attr(state);
|
||||
|
||||
let access_cookie = format!(
|
||||
"{}={}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age={}{}",
|
||||
ACCESS_COOKIE, session.access_token, ACCESS_MAX_AGE_SEC, domain
|
||||
"{}={}; HttpOnly{}; SameSite=Lax; Path=/; Max-Age={}{}",
|
||||
ACCESS_COOKIE, session.access_token, secure, ACCESS_MAX_AGE_SEC, domain
|
||||
);
|
||||
let refresh_cookie = format!(
|
||||
"{}={}; HttpOnly; Secure; SameSite=Lax; Path={}; Max-Age={}{}",
|
||||
REFRESH_COOKIE, session.refresh_token, REFRESH_COOKIE_PATH, REFRESH_MAX_AGE_SEC, domain
|
||||
"{}={}; HttpOnly{}; SameSite=Lax; Path={}; Max-Age={}{}",
|
||||
REFRESH_COOKIE,
|
||||
session.refresh_token,
|
||||
secure,
|
||||
REFRESH_COOKIE_PATH,
|
||||
REFRESH_MAX_AGE_SEC,
|
||||
domain
|
||||
);
|
||||
|
||||
headers.append(SET_COOKIE, access_cookie.parse().unwrap());
|
||||
@@ -76,13 +93,14 @@ fn set_session_cookies(headers: &mut HeaderMap, state: &ApiState, session: &Sess
|
||||
/// Немедленно затирает обе cookie (logout / отозванная сессия).
|
||||
fn clear_session_cookies(headers: &mut HeaderMap, state: &ApiState) {
|
||||
let domain = cookie_domain_attr(state);
|
||||
let secure = cookie_secure_attr(state);
|
||||
let clear_access = format!(
|
||||
"{}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0{}",
|
||||
ACCESS_COOKIE, domain
|
||||
"{}=; HttpOnly{}; SameSite=Lax; Path=/; Max-Age=0{}",
|
||||
ACCESS_COOKIE, secure, domain
|
||||
);
|
||||
let clear_refresh = format!(
|
||||
"{}=; HttpOnly; Secure; SameSite=Lax; Path={}; Max-Age=0{}",
|
||||
REFRESH_COOKIE, REFRESH_COOKIE_PATH, domain
|
||||
"{}=; HttpOnly{}; SameSite=Lax; Path={}; Max-Age=0{}",
|
||||
REFRESH_COOKIE, secure, REFRESH_COOKIE_PATH, domain
|
||||
);
|
||||
headers.append(SET_COOKIE, clear_access.parse().unwrap());
|
||||
headers.append(SET_COOKIE, clear_refresh.parse().unwrap());
|
||||
|
||||
Reference in New Issue
Block a user