@ -28,6 +28,20 @@ pub struct AuthUser {
pub role : UserRole ,
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 < String > {
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 < AppState > for AuthUser {
impl FromRequestParts < AppState > for AuthUser {
type Rejection = AppError ;
type Rejection = AppError ;
@ -36,7 +50,6 @@ impl FromRequestParts<AppState> for AuthUser {
state : & AppState ,
state : & AppState ,
) -> impl std ::future ::Future < Output = Result < Self , Self ::Rejection > > + Send {
) -> impl std ::future ::Future < Output = Result < Self , Self ::Rejection > > + Send {
let pool = state . pool . clone ( ) ;
let pool = state . pool . clone ( ) ;
let cookie_prefix = format! ( "{}=" , SESSION_COOKIE_NAME ) ;
// Extract the session cookie value from the Cookie header
// Extract the session cookie value from the Cookie header
let session_token = parts
let session_token = parts
@ -44,11 +57,7 @@ impl FromRequestParts<AppState> for AuthUser {
. get_all ( header ::COOKIE )
. get_all ( header ::COOKIE )
. iter ( )
. iter ( )
. filter_map ( | v | v . to_str ( ) . ok ( ) )
. filter_map ( | v | v . to_str ( ) . ok ( ) )
. flat_map ( | s | s . split ( ';' ) )
. find_map ( | header_value | extract_session_token ( header_value , SESSION_COOKIE_NAME ) ) ;
. map ( | s | s . trim ( ) )
. find ( | s | s . starts_with ( & cookie_prefix ) )
. and_then ( | s | s . strip_prefix ( & cookie_prefix ) )
. map ( | s | s . to_string ( ) ) ;
async move {
async move {
let session_token = session_token
let session_token = session_token
@ -102,3 +111,50 @@ impl FromRequestParts<AppState> 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 ) ;
}
}