actix_security_core/http/security/session.rs
1//! Session-based Authentication.
2//!
3//! # Spring Security Equivalent
4//! Similar to Spring Security's session-based authentication with `HttpSession`.
5//!
6//! # Features
7//! - Store user in session after login
8//! - Session fixation protection (migrate, new session, or none)
9//! - Configurable session timeout
10//! - Maximum sessions per user support
11//! - Integration with actix-session
12//!
13//! # Example
14//! ```rust,ignore
15//! use actix_security_core::http::security::session::{
16//! SessionAuthenticator, SessionConfig, SessionFixationStrategy
17//! };
18//! use actix_session::SessionMiddleware;
19//! use actix_session::storage::CookieSessionStore;
20//!
21//! // Configure session middleware (required)
22//! let session_middleware = SessionMiddleware::new(
23//! CookieSessionStore::default(),
24//! cookie_key.clone()
25//! );
26//!
27//! // Configure session authenticator with fixation protection
28//! let config = SessionConfig::new()
29//! .fixation_strategy(SessionFixationStrategy::MigrateSession);
30//!
31//! let authenticator = SessionAuthenticator::new(config);
32//!
33//! App::new()
34//! .wrap(session_middleware)
35//! .wrap(SecurityTransform::new()
36//! .config_authenticator(move || authenticator.clone())
37//! .config_authorizer(|| /* ... */))
38//! ```
39
40use crate::http::security::config::Authenticator;
41use crate::http::security::User;
42use actix_session::SessionExt;
43use actix_web::dev::ServiceRequest;
44use serde::{Deserialize, Serialize};
45use std::time::Duration;
46
47// =============================================================================
48// Session Fixation Strategy
49// =============================================================================
50
51/// Strategy for session fixation protection.
52///
53/// # Spring Security Equivalent
54/// Similar to `SessionFixationProtectionStrategy` in Spring Security.
55///
56/// Session fixation attacks occur when an attacker sets a user's session ID
57/// before they authenticate. After authentication, the attacker can hijack
58/// the session using the known session ID.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum SessionFixationStrategy {
61 /// Create a new session and migrate all attributes from the old session.
62 /// This is the safest option that preserves user data.
63 ///
64 /// # Spring Equivalent
65 /// `SessionFixationProtectionStrategy.MIGRATE_SESSION`
66 #[default]
67 MigrateSession,
68
69 /// Create a new session without migrating attributes.
70 /// Use this when you want a completely fresh session after login.
71 ///
72 /// # Spring Equivalent
73 /// `SessionFixationProtectionStrategy.NEW_SESSION`
74 NewSession,
75
76 /// No session fixation protection.
77 /// **WARNING**: This is insecure and should only be used for testing.
78 ///
79 /// # Spring Equivalent
80 /// `SessionFixationProtectionStrategy.NONE`
81 None,
82}
83
84// =============================================================================
85// Session User Data
86// =============================================================================
87
88/// Serializable user data stored in session.
89///
90/// This is the data structure stored in the session.
91/// It's separate from `User` to ensure clean serialization.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SessionUser {
94 /// Username
95 pub username: String,
96 /// User roles
97 pub roles: Vec<String>,
98 /// User authorities/permissions
99 pub authorities: Vec<String>,
100}
101
102impl SessionUser {
103 /// Create from a User.
104 pub fn from_user(user: &User) -> Self {
105 Self {
106 username: user.get_username().to_string(),
107 roles: user.get_roles().to_vec(),
108 authorities: user.get_authorities().to_vec(),
109 }
110 }
111
112 /// Convert to User.
113 pub fn to_user(&self) -> User {
114 User::new(self.username.clone(), String::new())
115 .roles(&self.roles)
116 .authorities(&self.authorities)
117 }
118}
119
120impl From<&User> for SessionUser {
121 fn from(user: &User) -> Self {
122 Self::from_user(user)
123 }
124}
125
126impl From<SessionUser> for User {
127 fn from(session_user: SessionUser) -> Self {
128 session_user.to_user()
129 }
130}
131
132// =============================================================================
133// Session Configuration
134// =============================================================================
135
136/// Session authentication configuration.
137///
138/// # Spring Security Equivalent
139/// Combines `SessionManagementConfigurer` and `SessionFixationConfigurer`.
140///
141/// # Example
142/// ```rust,ignore
143/// let config = SessionConfig::new()
144/// .user_key("user")
145/// .fixation_strategy(SessionFixationStrategy::MigrateSession)
146/// .maximum_sessions(1); // Only one session per user
147/// ```
148#[derive(Clone)]
149pub struct SessionConfig {
150 /// Session key for storing user data
151 user_key: String,
152 /// Session key for authentication flag
153 authenticated_key: String,
154 /// Session key for storing the original request URL (for redirect after login)
155 saved_request_key: String,
156 /// Session fixation protection strategy
157 fixation_strategy: SessionFixationStrategy,
158 /// Maximum number of concurrent sessions per user (None = unlimited)
159 maximum_sessions: Option<usize>,
160 /// Session timeout duration (used for reference, actual timeout configured in SessionMiddleware)
161 timeout: Option<Duration>,
162 /// Whether to expire the oldest session when max sessions exceeded
163 expire_oldest_session: bool,
164}
165
166impl Default for SessionConfig {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172impl SessionConfig {
173 /// Create a new session configuration with default keys.
174 pub fn new() -> Self {
175 Self {
176 user_key: "security_user".to_string(),
177 authenticated_key: "security_authenticated".to_string(),
178 saved_request_key: "security_saved_request".to_string(),
179 fixation_strategy: SessionFixationStrategy::MigrateSession,
180 maximum_sessions: None,
181 timeout: None,
182 expire_oldest_session: false,
183 }
184 }
185
186 /// Set the session key for user data.
187 pub fn user_key(mut self, key: &str) -> Self {
188 self.user_key = key.to_string();
189 self
190 }
191
192 /// Set the session key for authentication flag.
193 pub fn authenticated_key(mut self, key: &str) -> Self {
194 self.authenticated_key = key.to_string();
195 self
196 }
197
198 /// Set the session key for saved request URL.
199 pub fn saved_request_key(mut self, key: &str) -> Self {
200 self.saved_request_key = key.to_string();
201 self
202 }
203
204 /// Set the session fixation protection strategy.
205 ///
206 /// # Spring Equivalent
207 /// `sessionManagement().sessionFixation().migrateSession()`
208 pub fn fixation_strategy(mut self, strategy: SessionFixationStrategy) -> Self {
209 self.fixation_strategy = strategy;
210 self
211 }
212
213 /// Set the maximum number of concurrent sessions per user.
214 ///
215 /// # Spring Equivalent
216 /// `sessionManagement().maximumSessions(1)`
217 pub fn maximum_sessions(mut self, max: usize) -> Self {
218 self.maximum_sessions = Some(max);
219 self
220 }
221
222 /// Set the session timeout duration.
223 /// Note: This is informational; actual timeout is configured in SessionMiddleware.
224 pub fn timeout(mut self, duration: Duration) -> Self {
225 self.timeout = Some(duration);
226 self
227 }
228
229 /// Whether to expire the oldest session when maximum sessions exceeded.
230 ///
231 /// # Spring Equivalent
232 /// `sessionManagement().maximumSessions(1).expiredSessionStrategy(...)`
233 pub fn expire_oldest_session(mut self, expire: bool) -> Self {
234 self.expire_oldest_session = expire;
235 self
236 }
237
238 /// Get the user key.
239 pub fn get_user_key(&self) -> &str {
240 &self.user_key
241 }
242
243 /// Get the authenticated key.
244 pub fn get_authenticated_key(&self) -> &str {
245 &self.authenticated_key
246 }
247
248 /// Get the saved request key.
249 pub fn get_saved_request_key(&self) -> &str {
250 &self.saved_request_key
251 }
252
253 /// Get the fixation strategy.
254 pub fn get_fixation_strategy(&self) -> SessionFixationStrategy {
255 self.fixation_strategy
256 }
257
258 /// Get maximum sessions.
259 pub fn get_maximum_sessions(&self) -> Option<usize> {
260 self.maximum_sessions
261 }
262
263 /// Get timeout.
264 pub fn get_timeout(&self) -> Option<Duration> {
265 self.timeout
266 }
267
268 /// Check if oldest session should be expired.
269 pub fn should_expire_oldest(&self) -> bool {
270 self.expire_oldest_session
271 }
272}
273
274// =============================================================================
275// Session Authenticator
276// =============================================================================
277
278/// Session-based authenticator.
279///
280/// Reads user information from the actix-session.
281///
282/// # Spring Security Equivalent
283/// Similar to Spring's session-based authentication where `SecurityContext`
284/// is stored in the `HttpSession`.
285///
286/// # Requirements
287/// - `SessionMiddleware` must be configured in your application
288/// - User must be logged in via `SessionAuthenticator::login()`
289///
290/// # Example
291/// ```rust,ignore
292/// use actix_security_core::http::security::session::{SessionAuthenticator, SessionConfig};
293///
294/// let config = SessionConfig::new()
295/// .fixation_strategy(SessionFixationStrategy::MigrateSession);
296/// let authenticator = SessionAuthenticator::new(config);
297///
298/// // In login handler
299/// async fn login(session: Session, form: Form<LoginForm>) -> impl Responder {
300/// // Validate credentials...
301/// let user = validate_user(&form.username, &form.password)?;
302///
303/// // Store user in session (with fixation protection)
304/// SessionAuthenticator::login(&session, &user, &config)?;
305///
306/// HttpResponse::Ok().body("Logged in")
307/// }
308/// ```
309#[derive(Clone)]
310pub struct SessionAuthenticator {
311 config: SessionConfig,
312}
313
314impl SessionAuthenticator {
315 /// Create a new session authenticator.
316 pub fn new(config: SessionConfig) -> Self {
317 Self { config }
318 }
319
320 /// Create with default configuration.
321 pub fn default_config() -> Self {
322 Self::new(SessionConfig::default())
323 }
324
325 /// Store user in session (login) with session fixation protection.
326 ///
327 /// This method:
328 /// 1. Applies session fixation protection based on configuration
329 /// 2. Stores user data in the session
330 /// 3. Sets the authenticated flag
331 ///
332 /// # Spring Equivalent
333 /// Similar to `SecurityContextHolder.getContext().setAuthentication(...)`
334 /// combined with session fixation protection.
335 ///
336 /// # Example
337 /// ```rust,ignore
338 /// async fn login_handler(
339 /// session: Session,
340 /// form: Form<LoginForm>,
341 /// config: Data<SessionConfig>,
342 /// ) -> impl Responder {
343 /// // Validate credentials
344 /// let user = validate_user(&form.username, &form.password)?;
345 ///
346 /// // Login with session fixation protection
347 /// SessionAuthenticator::login(&session, &user, &config)?;
348 ///
349 /// HttpResponse::Ok().body("Logged in")
350 /// }
351 /// ```
352 pub fn login(
353 session: &actix_session::Session,
354 user: &User,
355 config: &SessionConfig,
356 ) -> Result<(), SessionError> {
357 // Apply session fixation protection
358 Self::apply_fixation_protection(session, config)?;
359
360 // Store user in session
361 let session_user = SessionUser::from_user(user);
362
363 session
364 .insert(&config.user_key, &session_user)
365 .map_err(|e| SessionError::InsertError(e.to_string()))?;
366
367 session
368 .insert(&config.authenticated_key, true)
369 .map_err(|e| SessionError::InsertError(e.to_string()))?;
370
371 Ok(())
372 }
373
374 /// Apply session fixation protection based on configuration.
375 fn apply_fixation_protection(
376 session: &actix_session::Session,
377 config: &SessionConfig,
378 ) -> Result<(), SessionError> {
379 match config.fixation_strategy {
380 SessionFixationStrategy::MigrateSession => {
381 // Regenerate session ID but keep data
382 // Note: actix-session's renew() regenerates the session ID
383 session.renew();
384 }
385 SessionFixationStrategy::NewSession => {
386 // Clear all session data and regenerate
387 session.purge();
388 }
389 SessionFixationStrategy::None => {
390 // No protection - do nothing
391 }
392 }
393 Ok(())
394 }
395
396 /// Remove user from session (logout).
397 ///
398 /// # Example
399 /// ```rust,ignore
400 /// async fn logout_handler(session: Session, config: Data<SessionConfig>) -> impl Responder {
401 /// SessionAuthenticator::logout(&session, &config);
402 /// HttpResponse::Ok().body("Logged out")
403 /// }
404 /// ```
405 pub fn logout(session: &actix_session::Session, config: &SessionConfig) {
406 session.remove(&config.user_key);
407 session.remove(&config.authenticated_key);
408 session.remove(&config.saved_request_key);
409 }
410
411 /// Clear entire session (logout + clear all data).
412 pub fn clear_session(session: &actix_session::Session) {
413 session.purge();
414 }
415
416 /// Check if session is authenticated.
417 pub fn is_authenticated(session: &actix_session::Session, config: &SessionConfig) -> bool {
418 session
419 .get::<bool>(&config.authenticated_key)
420 .ok()
421 .flatten()
422 .unwrap_or(false)
423 }
424
425 /// Get user from session.
426 pub fn get_session_user(
427 session: &actix_session::Session,
428 config: &SessionConfig,
429 ) -> Option<User> {
430 session
431 .get::<SessionUser>(&config.user_key)
432 .ok()
433 .flatten()
434 .map(|su| su.to_user())
435 }
436
437 /// Save the current request URL for redirect after login.
438 ///
439 /// # Spring Equivalent
440 /// Similar to `SavedRequest` in Spring Security.
441 pub fn save_request(
442 session: &actix_session::Session,
443 url: &str,
444 config: &SessionConfig,
445 ) -> Result<(), SessionError> {
446 session
447 .insert(&config.saved_request_key, url)
448 .map_err(|e| SessionError::InsertError(e.to_string()))
449 }
450
451 /// Get the saved request URL and remove it from session.
452 ///
453 /// Returns the saved URL or the default URL if none was saved.
454 pub fn get_saved_request(
455 session: &actix_session::Session,
456 config: &SessionConfig,
457 default_url: &str,
458 ) -> String {
459 let saved = session
460 .get::<String>(&config.saved_request_key)
461 .ok()
462 .flatten();
463
464 if saved.is_some() {
465 session.remove(&config.saved_request_key);
466 }
467
468 saved.unwrap_or_else(|| default_url.to_string())
469 }
470
471 /// Get the configuration.
472 pub fn config(&self) -> &SessionConfig {
473 &self.config
474 }
475}
476
477impl Authenticator for SessionAuthenticator {
478 fn get_user(&self, req: &ServiceRequest) -> Option<User> {
479 let session = req.get_session();
480
481 // Check if authenticated
482 if !Self::is_authenticated(&session, &self.config) {
483 return None;
484 }
485
486 // Get user from session
487 Self::get_session_user(&session, &self.config)
488 }
489}
490
491// =============================================================================
492// Session Error
493// =============================================================================
494
495/// Session-related errors.
496#[derive(Debug)]
497pub enum SessionError {
498 /// Error inserting data into session
499 InsertError(String),
500 /// Error reading data from session
501 ReadError(String),
502 /// Session not found
503 NotFound,
504 /// Maximum sessions exceeded
505 MaxSessionsExceeded,
506 /// Session expired
507 Expired,
508}
509
510impl std::fmt::Display for SessionError {
511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512 match self {
513 SessionError::InsertError(e) => write!(f, "Session insert error: {}", e),
514 SessionError::ReadError(e) => write!(f, "Session read error: {}", e),
515 SessionError::NotFound => write!(f, "Session not found"),
516 SessionError::MaxSessionsExceeded => write!(f, "Maximum sessions exceeded"),
517 SessionError::Expired => write!(f, "Session expired"),
518 }
519 }
520}
521
522impl std::error::Error for SessionError {}
523
524// =============================================================================
525// Session Login Service
526// =============================================================================
527
528/// Service for handling login/logout with sessions.
529///
530/// Combines authentication validation with session management.
531///
532/// # Example
533/// ```rust,ignore
534/// let login_service = SessionLoginService::new(
535/// memory_authenticator, // For validating credentials
536/// session_config,
537/// );
538///
539/// // In login handler
540/// async fn login(
541/// session: Session,
542/// form: Form<LoginForm>,
543/// login_service: Data<SessionLoginService<MemoryAuthenticator>>,
544/// ) -> impl Responder {
545/// match login_service.login(&session, &form.username, &form.password) {
546/// Ok(user) => HttpResponse::Ok().body(format!("Welcome, {}!", user.get_username())),
547/// Err(_) => HttpResponse::Unauthorized().body("Invalid credentials"),
548/// }
549/// }
550/// ```
551#[derive(Clone)]
552pub struct SessionLoginService<A>
553where
554 A: Authenticator,
555{
556 /// Authenticator for validating credentials
557 #[allow(dead_code)]
558 credential_authenticator: A,
559 /// Session configuration
560 config: SessionConfig,
561}
562
563impl<A> SessionLoginService<A>
564where
565 A: Authenticator,
566{
567 /// Create a new login service.
568 pub fn new(credential_authenticator: A, config: SessionConfig) -> Self {
569 Self {
570 credential_authenticator,
571 config,
572 }
573 }
574
575 /// Login with a validated user.
576 ///
577 /// This method:
578 /// 1. Applies session fixation protection
579 /// 2. Stores user in session
580 pub fn login_with_user(
581 &self,
582 session: &actix_session::Session,
583 user: &User,
584 ) -> Result<(), SessionError> {
585 SessionAuthenticator::login(session, user, &self.config)
586 }
587
588 /// Logout - remove user from session.
589 pub fn logout(&self, session: &actix_session::Session) {
590 SessionAuthenticator::logout(session, &self.config);
591 }
592
593 /// Get the session configuration.
594 pub fn config(&self) -> &SessionConfig {
595 &self.config
596 }
597
598 /// Save the current request URL for redirect after login.
599 pub fn save_request(
600 &self,
601 session: &actix_session::Session,
602 url: &str,
603 ) -> Result<(), SessionError> {
604 SessionAuthenticator::save_request(session, url, &self.config)
605 }
606
607 /// Get the saved request URL.
608 pub fn get_saved_request(&self, session: &actix_session::Session, default_url: &str) -> String {
609 SessionAuthenticator::get_saved_request(session, &self.config, default_url)
610 }
611}
612
613// =============================================================================
614// Credential Authenticator Trait
615// =============================================================================
616
617/// Trait for authenticators that can validate username/password credentials.
618///
619/// # Spring Equivalent
620/// Similar to `AuthenticationProvider` that handles username/password authentication.
621///
622/// This trait is separate from `Authenticator` because it validates credentials
623/// directly rather than extracting them from a request.
624pub trait CredentialAuthenticator: Send + Sync {
625 /// Validate username and password, returning the user if valid.
626 fn authenticate(&self, username: &str, password: &str) -> Option<User>;
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 fn test_user() -> User {
634 User::new("testuser".to_string(), "password".to_string())
635 .roles(&["USER".into()])
636 .authorities(&["read".into()])
637 }
638
639 #[test]
640 fn test_session_user_conversion() {
641 let user = test_user();
642 let session_user = SessionUser::from_user(&user);
643
644 assert_eq!(session_user.username, "testuser");
645 assert!(session_user.roles.contains(&"USER".to_string()));
646 assert!(session_user.authorities.contains(&"read".to_string()));
647
648 let converted = session_user.to_user();
649 assert_eq!(converted.get_username(), "testuser");
650 assert!(converted.has_role("USER"));
651 }
652
653 #[test]
654 fn test_session_config() {
655 let config = SessionConfig::new()
656 .user_key("my_user")
657 .authenticated_key("my_auth")
658 .fixation_strategy(SessionFixationStrategy::NewSession)
659 .maximum_sessions(2);
660
661 assert_eq!(config.get_user_key(), "my_user");
662 assert_eq!(config.get_authenticated_key(), "my_auth");
663 assert_eq!(
664 config.get_fixation_strategy(),
665 SessionFixationStrategy::NewSession
666 );
667 assert_eq!(config.get_maximum_sessions(), Some(2));
668 }
669
670 #[test]
671 fn test_session_fixation_strategy_default() {
672 let strategy = SessionFixationStrategy::default();
673 assert_eq!(strategy, SessionFixationStrategy::MigrateSession);
674 }
675
676 #[test]
677 fn test_session_user_serialization() {
678 let user = test_user();
679 let session_user = SessionUser::from_user(&user);
680
681 // Serialize to JSON
682 let json = serde_json::to_string(&session_user).unwrap();
683 assert!(json.contains("testuser"));
684
685 // Deserialize back
686 let deserialized: SessionUser = serde_json::from_str(&json).unwrap();
687 assert_eq!(deserialized.username, "testuser");
688 }
689
690 #[test]
691 fn test_session_config_builder() {
692 use std::time::Duration;
693
694 let config = SessionConfig::new()
695 .user_key("user")
696 .authenticated_key("auth")
697 .saved_request_key("saved")
698 .fixation_strategy(SessionFixationStrategy::MigrateSession)
699 .maximum_sessions(1)
700 .timeout(Duration::from_secs(3600))
701 .expire_oldest_session(true);
702
703 assert_eq!(config.get_user_key(), "user");
704 assert_eq!(config.get_authenticated_key(), "auth");
705 assert_eq!(config.get_saved_request_key(), "saved");
706 assert_eq!(
707 config.get_fixation_strategy(),
708 SessionFixationStrategy::MigrateSession
709 );
710 assert_eq!(config.get_maximum_sessions(), Some(1));
711 assert_eq!(config.get_timeout(), Some(Duration::from_secs(3600)));
712 assert!(config.should_expire_oldest());
713 }
714}