diff --git a/backend/src/middleware/auth.rs b/backend/src/middleware/auth.rs index d595e28..1b01762 100644 --- a/backend/src/middleware/auth.rs +++ b/backend/src/middleware/auth.rs @@ -28,6 +28,20 @@ pub struct AuthUser { pub role: UserRole, } +/// Extract session token from a Cookie header value. +/// +/// Splits the header on `;`, trims whitespace, and returns the value of the +/// first cookie whose name matches `cookie_name`. Returns `None` when no +/// matching cookie is present. +fn extract_session_token(cookie_header: &str, cookie_name: &str) -> Option { + cookie_header + .split(';') + .map(|s| s.trim()) + .find(|s| s.starts_with(&format!("{}=", cookie_name))) + .and_then(|s| s.strip_prefix(&format!("{}=", cookie_name))) + .map(|s| s.to_string()) +} + impl FromRequestParts for AuthUser { type Rejection = AppError; @@ -36,7 +50,6 @@ impl FromRequestParts for AuthUser { state: &AppState, ) -> impl std::future::Future> + Send { let pool = state.pool.clone(); - let cookie_prefix = format!("{}=", SESSION_COOKIE_NAME); // Extract the session cookie value from the Cookie header let session_token = parts @@ -44,11 +57,7 @@ impl FromRequestParts for AuthUser { .get_all(header::COOKIE) .iter() .filter_map(|v| v.to_str().ok()) - .flat_map(|s| s.split(';')) - .map(|s| s.trim()) - .find(|s| s.starts_with(&cookie_prefix)) - .and_then(|s| s.strip_prefix(&cookie_prefix)) - .map(|s| s.to_string()); + .find_map(|header_value| extract_session_token(header_value, SESSION_COOKIE_NAME)); async move { let session_token = session_token @@ -102,3 +111,50 @@ impl FromRequestParts for AdminUser { } } } + +#[cfg(test)] +mod tests { + use super::extract_session_token; + use crate::services::auth::SESSION_COOKIE_NAME; + + #[test] + fn test_valid_session_cookie_extracted() { + let header = format!("{}=abc123def", SESSION_COOKIE_NAME); + assert_eq!( + extract_session_token(&header, SESSION_COOKIE_NAME), + Some("abc123def".to_string()) + ); + } + + #[test] + fn test_missing_session_cookie_returns_none() { + let header = "other=value; another=thing"; + assert_eq!(extract_session_token(header, SESSION_COOKIE_NAME), None); + } + + #[test] + fn test_multiple_cookies_correct_one_picked() { + let header = format!("theme=dark; {}=mytoken123; lang=fr", SESSION_COOKIE_NAME); + assert_eq!( + extract_session_token(&header, SESSION_COOKIE_NAME), + Some("mytoken123".to_string()) + ); + } + + #[test] + fn test_cookie_with_whitespace_trimmed() { + // The whole segment is trimmed before matching, so leading/trailing + // whitespace around name=value pairs is stripped. The value itself + // also has its trailing spaces removed as part of that segment trim. + let header = format!(" {}=abc123 ; other=val ", SESSION_COOKIE_NAME); + assert_eq!( + extract_session_token(&header, SESSION_COOKIE_NAME), + Some("abc123".to_string()) + ); + } + + #[test] + fn test_empty_cookie_header_returns_none() { + assert_eq!(extract_session_token("", SESSION_COOKIE_NAME), None); + } +}