use crate::http::security::config::Authenticator;
use crate::http::security::User;
use actix_session::SessionExt;
use actix_web::dev::ServiceRequest;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SessionFixationStrategy {
#[default]
MigrateSession,
NewSession,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionUser {
pub username: String,
pub roles: Vec<String>,
pub authorities: Vec<String>,
}
impl SessionUser {
pub fn from_user(user: &User) -> Self {
Self {
username: user.get_username().to_string(),
roles: user.get_roles().to_vec(),
authorities: user.get_authorities().to_vec(),
}
}
pub fn to_user(&self) -> User {
User::new(self.username.clone(), String::new())
.roles(&self.roles)
.authorities(&self.authorities)
}
}
impl From<&User> for SessionUser {
fn from(user: &User) -> Self {
Self::from_user(user)
}
}
impl From<SessionUser> for User {
fn from(session_user: SessionUser) -> Self {
session_user.to_user()
}
}
#[derive(Clone)]
pub struct SessionConfig {
user_key: String,
authenticated_key: String,
saved_request_key: String,
fixation_strategy: SessionFixationStrategy,
maximum_sessions: Option<usize>,
timeout: Option<Duration>,
expire_oldest_session: bool,
}
impl Default for SessionConfig {
fn default() -> Self {
Self::new()
}
}
impl SessionConfig {
pub fn new() -> Self {
Self {
user_key: "security_user".to_string(),
authenticated_key: "security_authenticated".to_string(),
saved_request_key: "security_saved_request".to_string(),
fixation_strategy: SessionFixationStrategy::MigrateSession,
maximum_sessions: None,
timeout: None,
expire_oldest_session: false,
}
}
pub fn user_key(mut self, key: &str) -> Self {
self.user_key = key.to_string();
self
}
pub fn authenticated_key(mut self, key: &str) -> Self {
self.authenticated_key = key.to_string();
self
}
pub fn saved_request_key(mut self, key: &str) -> Self {
self.saved_request_key = key.to_string();
self
}
pub fn fixation_strategy(mut self, strategy: SessionFixationStrategy) -> Self {
self.fixation_strategy = strategy;
self
}
pub fn maximum_sessions(mut self, max: usize) -> Self {
self.maximum_sessions = Some(max);
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
pub fn expire_oldest_session(mut self, expire: bool) -> Self {
self.expire_oldest_session = expire;
self
}
pub fn get_user_key(&self) -> &str {
&self.user_key
}
pub fn get_authenticated_key(&self) -> &str {
&self.authenticated_key
}
pub fn get_saved_request_key(&self) -> &str {
&self.saved_request_key
}
pub fn get_fixation_strategy(&self) -> SessionFixationStrategy {
self.fixation_strategy
}
pub fn get_maximum_sessions(&self) -> Option<usize> {
self.maximum_sessions
}
pub fn get_timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn should_expire_oldest(&self) -> bool {
self.expire_oldest_session
}
}
#[derive(Clone)]
pub struct SessionAuthenticator {
config: SessionConfig,
}
impl SessionAuthenticator {
pub fn new(config: SessionConfig) -> Self {
Self { config }
}
pub fn default_config() -> Self {
Self::new(SessionConfig::default())
}
pub fn login(
session: &actix_session::Session,
user: &User,
config: &SessionConfig,
) -> Result<(), SessionError> {
Self::apply_fixation_protection(session, config)?;
let session_user = SessionUser::from_user(user);
session
.insert(&config.user_key, &session_user)
.map_err(|e| SessionError::InsertError(e.to_string()))?;
session
.insert(&config.authenticated_key, true)
.map_err(|e| SessionError::InsertError(e.to_string()))?;
Ok(())
}
fn apply_fixation_protection(
session: &actix_session::Session,
config: &SessionConfig,
) -> Result<(), SessionError> {
match config.fixation_strategy {
SessionFixationStrategy::MigrateSession => {
session.renew();
}
SessionFixationStrategy::NewSession => {
session.purge();
}
SessionFixationStrategy::None => {
}
}
Ok(())
}
pub fn logout(session: &actix_session::Session, config: &SessionConfig) {
session.remove(&config.user_key);
session.remove(&config.authenticated_key);
session.remove(&config.saved_request_key);
}
pub fn clear_session(session: &actix_session::Session) {
session.purge();
}
pub fn is_authenticated(session: &actix_session::Session, config: &SessionConfig) -> bool {
session
.get::<bool>(&config.authenticated_key)
.ok()
.flatten()
.unwrap_or(false)
}
pub fn get_session_user(
session: &actix_session::Session,
config: &SessionConfig,
) -> Option<User> {
session
.get::<SessionUser>(&config.user_key)
.ok()
.flatten()
.map(|su| su.to_user())
}
pub fn save_request(
session: &actix_session::Session,
url: &str,
config: &SessionConfig,
) -> Result<(), SessionError> {
session
.insert(&config.saved_request_key, url)
.map_err(|e| SessionError::InsertError(e.to_string()))
}
pub fn get_saved_request(
session: &actix_session::Session,
config: &SessionConfig,
default_url: &str,
) -> String {
let saved = session
.get::<String>(&config.saved_request_key)
.ok()
.flatten();
if saved.is_some() {
session.remove(&config.saved_request_key);
}
saved.unwrap_or_else(|| default_url.to_string())
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
}
impl Authenticator for SessionAuthenticator {
fn get_user(&self, req: &ServiceRequest) -> Option<User> {
let session = req.get_session();
if !Self::is_authenticated(&session, &self.config) {
return None;
}
Self::get_session_user(&session, &self.config)
}
}
#[derive(Debug)]
pub enum SessionError {
InsertError(String),
ReadError(String),
NotFound,
MaxSessionsExceeded,
Expired,
}
impl std::fmt::Display for SessionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionError::InsertError(e) => write!(f, "Session insert error: {}", e),
SessionError::ReadError(e) => write!(f, "Session read error: {}", e),
SessionError::NotFound => write!(f, "Session not found"),
SessionError::MaxSessionsExceeded => write!(f, "Maximum sessions exceeded"),
SessionError::Expired => write!(f, "Session expired"),
}
}
}
impl std::error::Error for SessionError {}
#[derive(Clone)]
pub struct SessionLoginService<A>
where
A: Authenticator,
{
#[allow(dead_code)]
credential_authenticator: A,
config: SessionConfig,
}
impl<A> SessionLoginService<A>
where
A: Authenticator,
{
pub fn new(credential_authenticator: A, config: SessionConfig) -> Self {
Self {
credential_authenticator,
config,
}
}
pub fn login_with_user(
&self,
session: &actix_session::Session,
user: &User,
) -> Result<(), SessionError> {
SessionAuthenticator::login(session, user, &self.config)
}
pub fn logout(&self, session: &actix_session::Session) {
SessionAuthenticator::logout(session, &self.config);
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
pub fn save_request(
&self,
session: &actix_session::Session,
url: &str,
) -> Result<(), SessionError> {
SessionAuthenticator::save_request(session, url, &self.config)
}
pub fn get_saved_request(&self, session: &actix_session::Session, default_url: &str) -> String {
SessionAuthenticator::get_saved_request(session, &self.config, default_url)
}
}
pub trait CredentialAuthenticator: Send + Sync {
fn authenticate(&self, username: &str, password: &str) -> Option<User>;
}
#[cfg(test)]
mod tests {
use super::*;
fn test_user() -> User {
User::new("testuser".to_string(), "password".to_string())
.roles(&["USER".into()])
.authorities(&["read".into()])
}
#[test]
fn test_session_user_conversion() {
let user = test_user();
let session_user = SessionUser::from_user(&user);
assert_eq!(session_user.username, "testuser");
assert!(session_user.roles.contains(&"USER".to_string()));
assert!(session_user.authorities.contains(&"read".to_string()));
let converted = session_user.to_user();
assert_eq!(converted.get_username(), "testuser");
assert!(converted.has_role("USER"));
}
#[test]
fn test_session_config() {
let config = SessionConfig::new()
.user_key("my_user")
.authenticated_key("my_auth")
.fixation_strategy(SessionFixationStrategy::NewSession)
.maximum_sessions(2);
assert_eq!(config.get_user_key(), "my_user");
assert_eq!(config.get_authenticated_key(), "my_auth");
assert_eq!(
config.get_fixation_strategy(),
SessionFixationStrategy::NewSession
);
assert_eq!(config.get_maximum_sessions(), Some(2));
}
#[test]
fn test_session_fixation_strategy_default() {
let strategy = SessionFixationStrategy::default();
assert_eq!(strategy, SessionFixationStrategy::MigrateSession);
}
#[test]
fn test_session_user_serialization() {
let user = test_user();
let session_user = SessionUser::from_user(&user);
let json = serde_json::to_string(&session_user).unwrap();
assert!(json.contains("testuser"));
let deserialized: SessionUser = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.username, "testuser");
}
#[test]
fn test_session_config_builder() {
use std::time::Duration;
let config = SessionConfig::new()
.user_key("user")
.authenticated_key("auth")
.saved_request_key("saved")
.fixation_strategy(SessionFixationStrategy::MigrateSession)
.maximum_sessions(1)
.timeout(Duration::from_secs(3600))
.expire_oldest_session(true);
assert_eq!(config.get_user_key(), "user");
assert_eq!(config.get_authenticated_key(), "auth");
assert_eq!(config.get_saved_request_key(), "saved");
assert_eq!(
config.get_fixation_strategy(),
SessionFixationStrategy::MigrateSession
);
assert_eq!(config.get_maximum_sessions(), Some(1));
assert_eq!(config.get_timeout(), Some(Duration::from_secs(3600)));
assert!(config.should_expire_oldest());
}
}