security: prevent re-use of refresh token (reduce surface for any stolen token)
Security: session hardening Refresh token rotation with theft detection (family_id) - Added family_id column to auth.sessions (migration 20260507000000_session_family.sql) grouping all tokens issued from the same login into a family - On refresh, the new session inherits the parent's family_id - If a revoked token is replayed (indicates the token was stolen after rotation), the entire family is immediately invalidated and a warning is logged — forcing re-authentication on all devices SameSite=Strict on refresh cookie - Access cookie stays SameSite=Lax (needed for top-level navigation) - Refresh cookie upgraded to SameSite=Strict — it is only ever used for explicit POST to /api/auth/refresh, never via cross-site navigation Refresh token TTL: 30 days → 7 days - With rotation, active sessions auto-renew and effectively never expire - Inactive sessions expire after 7 days instead of 30, reducing the theft window
This commit is contained in:
@@ -43,7 +43,7 @@ fn cookie_secure() -> bool {
|
||||
let secure = v == "true" || v == "1";
|
||||
if !secure {
|
||||
tracing::warn!(
|
||||
"OXICLOUD_COOKIE_SECURE is explicitly disabled — \
|
||||
"⚠️ SECURITY: OXICLOUD_COOKIE_SECURE is explicitly disabled — \
|
||||
cookies will be sent over plain HTTP. \
|
||||
Do NOT use this in production."
|
||||
);
|
||||
@@ -55,7 +55,7 @@ fn cookie_secure() -> bool {
|
||||
Ok(url) if url.starts_with("https") => true,
|
||||
Ok(url) if url.starts_with("http://") => {
|
||||
tracing::info!(
|
||||
"OXICLOUD_BASE_URL is HTTP — cookie Secure flag is OFF. \
|
||||
"⚠️ SECURITY: OXICLOUD_BASE_URL is HTTP — cookie Secure flag is OFF. \
|
||||
Set OXICLOUD_COOKIE_SECURE=true to override if your proxy terminates TLS."
|
||||
);
|
||||
false
|
||||
@@ -63,7 +63,7 @@ fn cookie_secure() -> bool {
|
||||
_ => {
|
||||
// Default to false for compatibility with HTTP deployments
|
||||
tracing::info!(
|
||||
"OXICLOUD_BASE_URL not set — defaulting to non-secure cookies \
|
||||
"⚠️ SECURITY: OXICLOUD_BASE_URL not set — defaulting to non-secure cookies \
|
||||
for HTTP compatibility. Set OXICLOUD_COOKIE_SECURE=true for HTTPS deployments."
|
||||
);
|
||||
false
|
||||
@@ -72,9 +72,11 @@ fn cookie_secure() -> bool {
|
||||
}
|
||||
|
||||
/// Build a `Set-Cookie` header value.
|
||||
fn build_cookie(name: &str, value: &str, path: &str, max_age_secs: i64) -> String {
|
||||
fn build_cookie(name: &str, value: &str, path: &str, max_age_secs: i64, same_site: &str) -> String {
|
||||
let secure = if cookie_secure() { "; Secure" } else { "" };
|
||||
format!("{name}={value}; HttpOnly; SameSite=Lax; Path={path}; Max-Age={max_age_secs}{secure}",)
|
||||
format!(
|
||||
"{name}={value}; HttpOnly; SameSite={same_site}; Path={path}; Max-Age={max_age_secs}{secure}",
|
||||
)
|
||||
}
|
||||
|
||||
/// Append `Set-Cookie` headers for both access and refresh tokens.
|
||||
@@ -96,6 +98,7 @@ pub fn append_auth_cookies(
|
||||
access_token,
|
||||
"/",
|
||||
access_expiry_secs,
|
||||
"Lax", // Lax: cookie is sent on top-level navigations (links from other sites)
|
||||
)) {
|
||||
headers.append(SET_COOKIE, val);
|
||||
}
|
||||
@@ -104,6 +107,7 @@ pub fn append_auth_cookies(
|
||||
refresh_token,
|
||||
"/api/auth",
|
||||
refresh_expiry_secs,
|
||||
"Strict", // Strict: refresh endpoint is never reached via cross-site navigation
|
||||
)) {
|
||||
headers.append(SET_COOKIE, val);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user