#[cfg(test)]
mod tests {
use crate::session::config::SESSION_COOKIE_NAME;
use crate::session::main::session::*;
use crate::session::types::{SessionId, UserId};
use crate::storage::{CacheData, CacheKey, CachePrefix, GENERIC_CACHE_STORE};
use crate::test_utils::init_test_environment;
use chrono::{Duration, Utc};
use http::header::COOKIE;
use http::{HeaderMap, HeaderValue, Method};
fn create_header_map_with_cookie(cookie_name: &str, cookie_value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
let cookie_str = format!("{cookie_name}={cookie_value}");
if let Ok(header_value) = HeaderValue::from_str(&cookie_str) {
headers.insert(COOKIE, header_value);
}
headers
}
fn create_security_test_session(
csrf_token: &str,
user_id: &str,
expires_offset_seconds: i64,
) -> serde_json::Value {
serde_json::json!({
"user_id": user_id,
"csrf_token": csrf_token,
"expires_at": (Utc::now() + Duration::seconds(expires_offset_seconds)).to_rfc3339(),
"ttl": 3600_u64,
})
}
async fn store_session_in_cache(
session_id: &str,
session_data: serde_json::Value,
ttl: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let _session_expires_at =
if let Some(expires_at_str) = session_data.get("expires_at").and_then(|v| v.as_str()) {
chrono::DateTime::parse_from_rfc3339(expires_at_str)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now() + chrono::Duration::seconds(ttl as i64))
} else {
chrono::Utc::now() + chrono::Duration::seconds(ttl as i64)
};
let cache_data = CacheData {
value: session_data.to_string(),
};
let cache_key = CacheKey::new(session_id.to_string())
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
GENERIC_CACHE_STORE
.lock()
.await
.put_with_ttl(CachePrefix::session(), cache_key, cache_data, ttl)
.await?;
Ok(())
}
#[tokio::test]
async fn test_security_session_fixation_prevention() {
init_test_environment().await;
let user_id = "test_user_fixation";
let mut session_ids = std::collections::HashSet::new();
for i in 0..100 {
let headers_result = create_new_session_with_uid(
UserId::new(format!("{user_id}_{i}")).expect("Valid user ID"),
)
.await;
assert!(headers_result.is_ok(), "Session creation should succeed");
let headers = headers_result.unwrap();
let cookie_header = headers.get(http::header::SET_COOKIE).unwrap();
let cookie_str = cookie_header.to_str().unwrap();
let session_id = cookie_str
.split(';')
.next()
.unwrap()
.split('=')
.nth(1)
.unwrap();
assert!(!session_id.is_empty(), "Session ID should not be empty");
assert!(
session_id.len() >= 32,
"Session ID should have sufficient length for security"
);
assert!(
session_ids.insert(session_id.to_string()),
"Session ID should be unique: {session_id}"
);
}
assert_eq!(session_ids.len(), 100, "All session IDs should be unique");
let session_ids_vec: Vec<String> = session_ids.into_iter().collect();
for i in 1..session_ids_vec.len() {
let prev_id = &session_ids_vec[i - 1];
let curr_id = &session_ids_vec[i];
assert_ne!(
prev_id, curr_id,
"Consecutive session IDs should be different"
);
let prev_bytes = prev_id.as_bytes();
let curr_bytes = curr_id.as_bytes();
let mut diff_count = 0;
for (p, c) in prev_bytes.iter().zip(curr_bytes.iter()) {
if p != c {
diff_count += 1;
}
}
assert!(
diff_count >= 10,
"Session IDs should have significant entropy differences"
);
}
}
#[tokio::test]
async fn test_security_session_hijacking_prevention() {
init_test_environment().await;
let user_id = "test_user_hijacking";
let csrf_token = "test_csrf_hijacking";
let valid_session_id = "valid_session_123";
let tampered_session_id = "tampered_session_456";
let valid_session = create_security_test_session(csrf_token, user_id, 3600);
store_session_in_cache(valid_session_id, valid_session, 3600)
.await
.unwrap();
let cookie_name = SESSION_COOKIE_NAME.to_string();
let valid_headers = create_header_map_with_cookie(&cookie_name, valid_session_id);
let auth_result = is_authenticated_basic(&valid_headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Legitimate session should authenticate"
);
assert!(
auth_result.unwrap().0,
"Authentication should succeed for valid session"
);
let invalid_headers = create_header_map_with_cookie(&cookie_name, tampered_session_id);
let auth_result = is_authenticated_basic(&invalid_headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Invalid session check should not error"
);
assert!(
!auth_result.unwrap().0,
"Authentication should fail for invalid session"
);
let empty_headers = create_header_map_with_cookie(&cookie_name, "");
let auth_result = is_authenticated_basic(&empty_headers, &Method::GET).await;
assert!(auth_result.is_ok(), "Empty session check should not error");
assert!(
!auth_result.unwrap().0,
"Authentication should fail for empty session"
);
let safe_malformed_ids = vec![
"short".to_string(), "../../../etc/passwd".to_string(), "<script>alert('xss')</script>".to_string(), ];
for malformed_id in safe_malformed_ids {
let malformed_headers = create_header_map_with_cookie(&cookie_name, &malformed_id);
let auth_result = is_authenticated_basic(&malformed_headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Safe malformed session check should not error for: {malformed_id}"
);
assert!(
!auth_result.unwrap().0,
"Authentication should fail for malformed session: {malformed_id}"
);
}
let dangerous_malformed_ids = vec![
"a".repeat(1000), "session with spaces".to_string(), "session\nwith\nnewlines".to_string(), "session\x00with\x00nulls".to_string(), "'; DROP TABLE sessions; --".to_string(), ];
for dangerous_id in dangerous_malformed_ids {
let malformed_headers = create_header_map_with_cookie(&cookie_name, &dangerous_id);
let auth_result = is_authenticated_basic(&malformed_headers, &Method::GET).await;
if let Ok(auth_status) = auth_result {
assert!(
!auth_status.0,
"If dangerous malformed session doesn't error, authentication should still fail: {dangerous_id}"
);
}
}
let mut post_headers = create_header_map_with_cookie(&cookie_name, valid_session_id);
post_headers.insert(
"X-CSRF-Token",
HeaderValue::from_str("wrong_csrf_token").unwrap(),
);
let csrf_result = is_authenticated_basic(&post_headers, &Method::POST).await;
assert!(
csrf_result.is_err(),
"POST with wrong CSRF token should fail"
);
match csrf_result.unwrap_err() {
crate::session::errors::SessionError::CsrfToken(_) => {
}
other => panic!("Expected CSRF token error, got: {other:?}"),
}
}
#[tokio::test]
async fn test_security_session_invalidation() {
init_test_environment().await;
let user_id = "test_user_invalidation";
let csrf_token = "test_csrf_invalidation";
let session_id = "session_to_invalidate";
let session_data = create_security_test_session(csrf_token, user_id, 3600);
store_session_in_cache(session_id, session_data, 3600)
.await
.unwrap();
let cookie_name = SESSION_COOKIE_NAME.to_string();
let headers = create_header_map_with_cookie(&cookie_name, session_id);
let auth_result = is_authenticated_basic(&headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Session should be valid before invalidation"
);
assert!(
auth_result.unwrap().0,
"Authentication should succeed before invalidation"
);
let delete_result = delete_session_from_store_by_session_id(
SessionId::new(session_id.to_string()).expect("Valid session ID"),
)
.await;
assert!(delete_result.is_ok(), "Session deletion should succeed");
let auth_result = is_authenticated_basic(&headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Invalidated session check should not error"
);
assert!(
!auth_result.unwrap().0,
"Authentication should fail after invalidation"
);
let cache_prefix = CachePrefix::new("session".to_string()).unwrap();
let cache_key = CacheKey::new(session_id.to_string()).unwrap();
let cache_check = GENERIC_CACHE_STORE
.lock()
.await
.get(cache_prefix, cache_key)
.await;
assert!(cache_check.is_ok(), "Cache check should not error");
assert!(
cache_check.unwrap().is_none(),
"Session should be removed from cache"
);
let delete_result_2 = delete_session_from_store_by_session_id(
SessionId::new(session_id.to_string()).expect("Valid session ID"),
)
.await;
assert!(
delete_result_2.is_ok(),
"Multiple deletion attempts should not error"
);
let session_cookie = crate::SessionCookie::new(session_id.to_string()).unwrap();
let csrf_result = get_csrf_token_from_session(&session_cookie).await;
assert!(
csrf_result.is_err(),
"CSRF token retrieval should fail for invalidated session"
);
let session_cookie = crate::SessionCookie::new(session_id.to_string()).unwrap();
let user_result = get_user_from_session(&session_cookie).await;
assert!(
user_result.is_err(),
"User retrieval should fail for invalidated session"
);
}
#[tokio::test]
async fn test_security_cross_session_token_reuse_prevention() {
init_test_environment().await;
let user_id_1 = "test_user_session_1";
let user_id_2 = "test_user_session_2";
let csrf_token_1 = "csrf_token_session_1";
let csrf_token_2 = "csrf_token_session_2";
let session_id_1 = "session_1_unique";
let session_id_2 = "session_2_unique";
let session_1 = create_security_test_session(csrf_token_1, user_id_1, 3600);
let session_2 = create_security_test_session(csrf_token_2, user_id_2, 3600);
store_session_in_cache(session_id_1, session_1, 3600)
.await
.unwrap();
store_session_in_cache(session_id_2, session_2, 3600)
.await
.unwrap();
let cookie_name = SESSION_COOKIE_NAME.to_string();
let mut headers_1 = create_header_map_with_cookie(&cookie_name, session_id_1);
headers_1.insert("X-CSRF-Token", HeaderValue::from_str(csrf_token_1).unwrap());
let auth_result_1 = is_authenticated_basic(&headers_1, &Method::POST).await;
assert!(
auth_result_1.is_ok(),
"Session 1 should authenticate with its CSRF token"
);
let mut headers_2 = create_header_map_with_cookie(&cookie_name, session_id_2);
headers_2.insert("X-CSRF-Token", HeaderValue::from_str(csrf_token_2).unwrap());
let auth_result_2 = is_authenticated_basic(&headers_2, &Method::POST).await;
assert!(
auth_result_2.is_ok(),
"Session 2 should authenticate with its CSRF token"
);
let mut cross_headers_1 = create_header_map_with_cookie(&cookie_name, session_id_1);
cross_headers_1.insert("X-CSRF-Token", HeaderValue::from_str(csrf_token_2).unwrap());
let cross_result_1 = is_authenticated_basic(&cross_headers_1, &Method::POST).await;
assert!(
cross_result_1.is_err(),
"Session 1 should reject Session 2's CSRF token"
);
let mut cross_headers_2 = create_header_map_with_cookie(&cookie_name, session_id_2);
cross_headers_2.insert("X-CSRF-Token", HeaderValue::from_str(csrf_token_1).unwrap());
let cross_result_2 = is_authenticated_basic(&cross_headers_2, &Method::POST).await;
assert!(
cross_result_2.is_err(),
"Session 2 should reject Session 1's CSRF token"
);
match cross_result_1.unwrap_err() {
crate::session::errors::SessionError::CsrfToken(_) => {
}
other => panic!("Expected CSRF token error for cross-session reuse, got: {other:?}"),
}
match cross_result_2.unwrap_err() {
crate::session::errors::SessionError::CsrfToken(_) => {
}
other => panic!("Expected CSRF token error for cross-session reuse, got: {other:?}"),
}
let session_cookie_1 = crate::SessionCookie::new(session_id_1.to_string()).unwrap();
let csrf_1_result = get_csrf_token_from_session(&session_cookie_1).await;
let session_cookie_2 = crate::SessionCookie::new(session_id_2.to_string()).unwrap();
let csrf_2_result = get_csrf_token_from_session(&session_cookie_2).await;
assert!(
csrf_1_result.is_ok(),
"Session 1 CSRF retrieval should succeed"
);
assert!(
csrf_2_result.is_ok(),
"Session 2 CSRF retrieval should succeed"
);
let retrieved_csrf_1 = csrf_1_result.unwrap();
let retrieved_csrf_2 = csrf_2_result.unwrap();
assert_eq!(
retrieved_csrf_1.as_str(),
csrf_token_1,
"Session 1 should have correct CSRF token"
);
assert_eq!(
retrieved_csrf_2.as_str(),
csrf_token_2,
"Session 2 should have correct CSRF token"
);
assert_ne!(
retrieved_csrf_1.as_str(),
retrieved_csrf_2.as_str(),
"Sessions should have different CSRF tokens"
);
}
#[tokio::test]
async fn test_security_session_concurrency_safety() {
init_test_environment().await;
let user_id = "test_user_concurrency";
let csrf_token = "test_csrf_concurrency";
let session_id = "concurrent_session_test";
let cookie_name = SESSION_COOKIE_NAME.to_string();
let session_data = create_security_test_session(csrf_token, user_id, 3600);
store_session_in_cache(session_id, session_data, 3600)
.await
.unwrap();
let mut handles = vec![];
for i in 0..50 {
let session_id = session_id.to_string();
let cookie_name = cookie_name.clone();
let handle = tokio::spawn(async move {
let headers = create_header_map_with_cookie(&cookie_name, &session_id);
let auth_result = is_authenticated_basic(&headers, &Method::GET).await;
(i, auth_result)
});
handles.push(handle);
}
let mut auth_results = vec![];
for handle in handles {
let (i, result) = handle.await.unwrap();
auth_results.push((i, result));
}
for (i, result) in &auth_results {
assert!(
result.is_ok(),
"Concurrent auth attempt {i} should not error"
);
assert!(
result.as_ref().unwrap().0,
"Concurrent auth attempt {i} should succeed"
);
}
let mut deletion_handles = vec![];
let mut access_handles = vec![];
for i in 0..5 {
let session_id = session_id.to_string();
let handle = tokio::spawn(async move {
let result = delete_session_from_store_by_session_id(
SessionId::new(session_id).expect("Valid session ID"),
)
.await;
(format!("delete_{i}"), result)
});
deletion_handles.push(handle);
}
for i in 0..10 {
let session_id = session_id.to_string();
let cookie_name = cookie_name.clone();
let handle = tokio::spawn(async move {
let headers = create_header_map_with_cookie(&cookie_name, &session_id);
let result = is_authenticated_basic(&headers, &Method::GET).await;
(format!("access_{i}"), result)
});
access_handles.push(handle);
}
let mut deletion_results = vec![];
let mut access_results = vec![];
for handle in deletion_handles {
let (op_type, result) = handle.await.unwrap();
deletion_results.push((op_type, result));
}
for handle in access_handles {
let (op_type, result) = handle.await.unwrap();
access_results.push((op_type, result));
}
for (op_type, result) in &deletion_results {
assert!(
result.is_ok(),
"Deletion operation {op_type} should not error"
);
}
for (op_type, result) in &access_results {
assert!(
result.is_ok(),
"Access operation {op_type} should not error"
);
}
let cache_prefix = CachePrefix::new("session".to_string()).unwrap();
let cache_key = CacheKey::new(session_id.to_string()).unwrap();
let final_check = GENERIC_CACHE_STORE
.lock()
.await
.get(cache_prefix, cache_key)
.await;
assert!(final_check.is_ok(), "Final cache check should not error");
assert!(
final_check.unwrap().is_none(),
"Session should be deleted after concurrent operations"
);
}
#[tokio::test]
async fn test_security_session_storage_integrity() {
init_test_environment().await;
let user_id = "test_user_storage";
let csrf_token = "test_csrf_storage";
let legitimate_session_id = "legitimate_session";
let cookie_name = SESSION_COOKIE_NAME.to_string();
let legitimate_session = create_security_test_session(csrf_token, user_id, 3600);
store_session_in_cache(legitimate_session_id, legitimate_session, 3600)
.await
.unwrap();
let invalid_data_attempts = vec![
("invalid_json", r#"{"invalid": json syntax}"#),
("missing_fields", r#"{"user_id": "test"}"#),
(
"wrong_types",
r#"{"user_id": 123, "csrf_token": true, "expires_at": "not_a_date", "ttl": "not_a_number"}"#,
),
(
"null_values",
r#"{"user_id": null, "csrf_token": null, "expires_at": null, "ttl": null}"#,
),
(
"empty_strings",
r#"{"user_id": "", "csrf_token": "", "expires_at": "", "ttl": 0}"#,
),
];
for (attack_type, invalid_data) in invalid_data_attempts {
let attack_session_id = format!("attack_{attack_type}");
let cache_data = CacheData {
value: invalid_data.to_string(),
};
let cache_prefix = CachePrefix::new("session".to_string()).unwrap();
let cache_key = CacheKey::new(attack_session_id.clone()).unwrap();
let store_result = GENERIC_CACHE_STORE
.lock()
.await
.put_with_ttl(cache_prefix, cache_key, cache_data, 3600)
.await;
if store_result.is_ok() {
let attack_headers =
create_header_map_with_cookie(&cookie_name, &attack_session_id);
let auth_result = is_authenticated_basic(&attack_headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Auth check for {attack_type} should not error"
);
assert!(
!auth_result.unwrap().0,
"Auth should fail for invalid session data: {attack_type}"
);
}
}
let legit_headers = create_header_map_with_cookie(&cookie_name, legitimate_session_id);
let auth_result = is_authenticated_basic(&legit_headers, &Method::GET).await;
assert!(
auth_result.is_ok(),
"Legitimate session should still work after attacks"
);
assert!(
auth_result.unwrap().0,
"Legitimate session should authenticate after attacks"
);
let collision_attempts = vec![
"../session/legitimate_session", "session/../legitimate_session", "legitimate_session\x00", "legitimate_session\n", "LEGITIMATE_SESSION", "legitimate_session ", " legitimate_session", ];
for collision_key in collision_attempts {
let collision_session =
create_security_test_session("collision_csrf", "collision_user", 3600);
let store_result = store_session_in_cache(collision_key, collision_session, 3600).await;
if store_result.is_ok() {
let legit_check = is_authenticated_basic(&legit_headers, &Method::GET).await;
assert!(
legit_check.is_ok(),
"Legitimate session should be unaffected by collision attempt"
);
assert!(
legit_check.unwrap().0,
"Legitimate session should still authenticate"
);
}
}
let large_data_session = serde_json::json!({
"user_id": "a".repeat(1000000), "csrf_token": "b".repeat(1000000), "expires_at": (Utc::now() + Duration::seconds(3600)).to_rfc3339(),
"ttl": 3600_u64,
});
let _large_data_result =
store_session_in_cache("large_data_session", large_data_session, 3600).await;
let legit_check = is_authenticated_basic(&legit_headers, &Method::GET).await;
assert!(
legit_check.is_ok(),
"Legitimate session should be unaffected by large data injection"
);
assert!(
legit_check.unwrap().0,
"Legitimate session should still authenticate after large data attempt"
);
}
}