Skip to main content

reinhardt_auth/sessions/
rotation.rs

1//! Session key rotation for enhanced security
2//!
3//! This module provides functionality to rotate session keys automatically
4//! to prevent session fixation attacks and improve security.
5//!
6//! ## Example
7//!
8//! ```rust
9//! use reinhardt_auth::sessions::rotation::{RotationPolicy, SessionRotator};
10//! use reinhardt_auth::sessions::Session;
11//! use reinhardt_auth::sessions::backends::InMemorySessionBackend;
12//!
13//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
14//! let backend = InMemorySessionBackend::new();
15//! let mut session = Session::new(backend.clone());
16//!
17//! // Set some data
18//! session.set("user_id", 42)?;
19//!
20//! // Create rotator with policy
21//! let rotator = SessionRotator::new(RotationPolicy::OnLogin);
22//!
23//! // Rotate session key
24//! rotator.rotate(&mut session).await?;
25//!
26//! // Data is preserved, but key has changed
27//! let user_id: i32 = session.get("user_id")?.unwrap();
28//! assert_eq!(user_id, 42);
29//! # Ok(())
30//! # }
31//! ```
32
33#![allow(clippy::field_reassign_with_default)]
34use super::backends::{SessionBackend, SessionError};
35use super::session::Session;
36use chrono::{DateTime, Duration as ChronoDuration, Utc};
37use std::time::Duration;
38
39/// Session rotation policy
40///
41/// Defines when session keys should be rotated.
42///
43/// # Example
44///
45/// ```rust
46/// use reinhardt_auth::sessions::rotation::RotationPolicy;
47/// use std::time::Duration;
48///
49/// // Rotate on every login
50/// let on_login = RotationPolicy::OnLogin;
51///
52/// // Rotate every hour
53/// let periodic = RotationPolicy::Periodic(Duration::from_secs(3600));
54///
55/// // Rotate after specific number of requests
56/// let after_requests = RotationPolicy::AfterRequests(100);
57/// ```
58#[derive(Debug, Clone)]
59pub enum RotationPolicy {
60	/// Rotate session key on user login
61	OnLogin,
62	/// Rotate session key periodically
63	Periodic(Duration),
64	/// Rotate session key after N requests
65	AfterRequests(usize),
66	/// Rotate on privilege escalation
67	OnPrivilegeEscalation,
68	/// Never rotate (not recommended for production)
69	Never,
70}
71
72impl Default for RotationPolicy {
73	/// Create default rotation policy
74	///
75	/// # Example
76	///
77	/// ```rust
78	/// use reinhardt_auth::sessions::rotation::RotationPolicy;
79	///
80	/// let policy = RotationPolicy::default();
81	/// // Default is OnLogin
82	/// ```
83	fn default() -> Self {
84		Self::OnLogin
85	}
86}
87
88/// Session rotation metadata
89#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct RotationMetadata {
91	/// When the session key was last rotated
92	pub last_rotation: DateTime<Utc>,
93	/// Number of requests since last rotation
94	pub request_count: usize,
95}
96
97impl Default for RotationMetadata {
98	fn default() -> Self {
99		Self {
100			last_rotation: Utc::now(),
101			request_count: 0,
102		}
103	}
104}
105
106/// Session rotator
107///
108/// Handles session key rotation based on configured policy.
109///
110/// # Example
111///
112/// ```rust
113/// use reinhardt_auth::sessions::rotation::{SessionRotator, RotationPolicy};
114/// use reinhardt_auth::sessions::Session;
115/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
116///
117/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
118/// let backend = InMemorySessionBackend::new();
119/// let mut session = Session::new(backend);
120///
121/// session.set("user_id", 123)?;
122///
123/// let rotator = SessionRotator::new(RotationPolicy::OnLogin);
124/// rotator.rotate(&mut session).await?;
125///
126/// // Session key has changed but data is preserved
127/// let user_id: i32 = session.get("user_id")?.unwrap();
128/// assert_eq!(user_id, 123);
129/// # Ok(())
130/// # }
131/// ```
132pub struct SessionRotator {
133	policy: RotationPolicy,
134}
135
136impl SessionRotator {
137	/// Create a new session rotator with the given policy
138	///
139	/// # Example
140	///
141	/// ```rust
142	/// use reinhardt_auth::sessions::rotation::{SessionRotator, RotationPolicy};
143	/// use std::time::Duration;
144	///
145	/// let rotator = SessionRotator::new(RotationPolicy::Periodic(Duration::from_secs(3600)));
146	/// ```
147	pub fn new(policy: RotationPolicy) -> Self {
148		Self { policy }
149	}
150
151	/// Rotate session key
152	///
153	/// This preserves all session data while changing the session key.
154	///
155	/// # Example
156	///
157	/// ```rust
158	/// use reinhardt_auth::sessions::rotation::{SessionRotator, RotationPolicy};
159	/// use reinhardt_auth::sessions::Session;
160	/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
161	///
162	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
163	/// let backend = InMemorySessionBackend::new();
164	/// let mut session = Session::new(backend);
165	///
166	/// session.set("data", "value")?;
167	/// let old_key = session.get_or_create_key().to_string();
168	///
169	/// let rotator = SessionRotator::new(RotationPolicy::OnLogin);
170	/// rotator.rotate(&mut session).await?;
171	///
172	/// // Key has changed
173	/// assert_ne!(session.get_or_create_key(), old_key);
174	///
175	/// // Data is preserved
176	/// let data: String = session.get("data")?.unwrap();
177	/// assert_eq!(data, "value");
178	/// # Ok(())
179	/// # }
180	/// ```
181	pub async fn rotate<B: SessionBackend>(
182		&self,
183		session: &mut Session<B>,
184	) -> Result<(), SessionError> {
185		// Use the existing cycle_key method which preserves data
186		session.cycle_key().await?;
187
188		// Update rotation metadata
189		let metadata = RotationMetadata::default();
190		session
191			.set("_rotation_metadata", metadata)
192			.map_err(|e| SessionError::SerializationError(e.to_string()))?;
193
194		Ok(())
195	}
196
197	/// Check if rotation is needed based on policy
198	///
199	/// # Example
200	///
201	/// ```rust
202	/// use reinhardt_auth::sessions::rotation::{SessionRotator, RotationPolicy, RotationMetadata};
203	/// use std::time::Duration;
204	///
205	/// let rotator = SessionRotator::new(RotationPolicy::AfterRequests(100));
206	///
207	/// let mut metadata = RotationMetadata::default();
208	/// metadata.request_count = 99;
209	/// assert!(!rotator.should_rotate(&metadata));
210	///
211	/// metadata.request_count = 100;
212	/// assert!(rotator.should_rotate(&metadata));
213	/// ```
214	pub fn should_rotate(&self, metadata: &RotationMetadata) -> bool {
215		match &self.policy {
216			RotationPolicy::OnLogin => false, // Handled externally
217			RotationPolicy::Periodic(duration) => {
218				let elapsed = Utc::now() - metadata.last_rotation;
219				elapsed > ChronoDuration::from_std(*duration).unwrap()
220			}
221			RotationPolicy::AfterRequests(max_requests) => metadata.request_count >= *max_requests,
222			RotationPolicy::OnPrivilegeEscalation => false, // Handled externally
223			RotationPolicy::Never => false,
224		}
225	}
226
227	/// Increment request count in metadata
228	///
229	/// # Example
230	///
231	/// ```rust
232	/// use reinhardt_auth::sessions::rotation::{SessionRotator, RotationMetadata};
233	///
234	/// let rotator = SessionRotator::default();
235	/// let mut metadata = RotationMetadata::default();
236	///
237	/// assert_eq!(metadata.request_count, 0);
238	/// rotator.increment_request_count(&mut metadata);
239	/// assert_eq!(metadata.request_count, 1);
240	/// ```
241	pub fn increment_request_count(&self, metadata: &mut RotationMetadata) {
242		metadata.request_count += 1;
243	}
244}
245
246impl Default for SessionRotator {
247	/// Create session rotator with default policy
248	///
249	/// # Example
250	///
251	/// ```rust
252	/// use reinhardt_auth::sessions::rotation::SessionRotator;
253	///
254	/// let rotator = SessionRotator::default();
255	/// ```
256	fn default() -> Self {
257		Self::new(RotationPolicy::default())
258	}
259}
260
261#[cfg(test)]
262mod tests {
263	use super::*;
264	use crate::sessions::InMemorySessionBackend;
265	use rstest::rstest;
266
267	#[rstest]
268	#[tokio::test]
269	async fn test_rotation_policy_default() {
270		let policy = RotationPolicy::default();
271		match policy {
272			RotationPolicy::OnLogin => {}
273			_ => panic!("Expected OnLogin policy"),
274		}
275	}
276
277	#[rstest]
278	#[tokio::test]
279	async fn test_rotation_metadata_default() {
280		let metadata = RotationMetadata::default();
281		assert_eq!(metadata.request_count, 0);
282		assert!(metadata.last_rotation <= Utc::now());
283	}
284
285	#[rstest]
286	#[tokio::test]
287	async fn test_session_rotator_creation() {
288		let _rotator = SessionRotator::new(RotationPolicy::OnLogin);
289	}
290
291	#[rstest]
292	#[tokio::test]
293	async fn test_session_rotator_default() {
294		let _rotator = SessionRotator::default();
295	}
296
297	#[rstest]
298	#[tokio::test]
299	async fn test_rotate_session() {
300		let backend = InMemorySessionBackend::new();
301		let mut session = Session::new(backend);
302
303		session.set("user_id", 123).unwrap();
304		let old_key = session.get_or_create_key().to_string();
305
306		let rotator = SessionRotator::new(RotationPolicy::OnLogin);
307		rotator.rotate(&mut session).await.unwrap();
308
309		// Key has changed
310		assert_ne!(session.get_or_create_key(), old_key);
311
312		// Data is preserved
313		let user_id: i32 = session.get("user_id").unwrap().unwrap();
314		assert_eq!(user_id, 123);
315	}
316
317	#[rstest]
318	#[tokio::test]
319	async fn test_should_rotate_periodic() {
320		let rotator = SessionRotator::new(RotationPolicy::Periodic(Duration::from_secs(3600)));
321
322		let metadata = RotationMetadata::default();
323		// Just created, should not rotate
324		assert!(!rotator.should_rotate(&metadata));
325
326		// Old metadata, should rotate
327		let old_metadata = RotationMetadata {
328			last_rotation: Utc::now() - ChronoDuration::hours(2),
329			request_count: 0,
330		};
331		assert!(rotator.should_rotate(&old_metadata));
332	}
333
334	#[rstest]
335	#[tokio::test]
336	async fn test_should_rotate_after_requests() {
337		let rotator = SessionRotator::new(RotationPolicy::AfterRequests(100));
338
339		let mut metadata = RotationMetadata::default();
340		metadata.request_count = 99;
341		assert!(!rotator.should_rotate(&metadata));
342
343		metadata.request_count = 100;
344		assert!(rotator.should_rotate(&metadata));
345
346		metadata.request_count = 150;
347		assert!(rotator.should_rotate(&metadata));
348	}
349
350	#[rstest]
351	#[tokio::test]
352	async fn test_should_rotate_never() {
353		let rotator = SessionRotator::new(RotationPolicy::Never);
354
355		let metadata = RotationMetadata::default();
356		assert!(!rotator.should_rotate(&metadata));
357
358		let old_metadata = RotationMetadata {
359			last_rotation: Utc::now() - ChronoDuration::days(365),
360			request_count: 1000000,
361		};
362		assert!(!rotator.should_rotate(&old_metadata));
363	}
364
365	#[rstest]
366	#[tokio::test]
367	async fn test_increment_request_count() {
368		let rotator = SessionRotator::default();
369		let mut metadata = RotationMetadata::default();
370
371		assert_eq!(metadata.request_count, 0);
372		rotator.increment_request_count(&mut metadata);
373		assert_eq!(metadata.request_count, 1);
374		rotator.increment_request_count(&mut metadata);
375		assert_eq!(metadata.request_count, 2);
376	}
377
378	#[rstest]
379	#[tokio::test]
380	async fn test_rotate_preserves_all_session_data() {
381		// Arrange
382		let backend = InMemorySessionBackend::new();
383		let mut session = Session::new(backend);
384		session.set("user_id", 42).unwrap();
385		session.set("role", "admin").unwrap();
386		session.set("theme", "dark").unwrap();
387		let old_key = session.get_or_create_key().to_string();
388		let rotator = SessionRotator::new(RotationPolicy::OnLogin);
389
390		// Act
391		rotator.rotate(&mut session).await.unwrap();
392
393		// Assert
394		assert_ne!(session.get_or_create_key(), old_key);
395		let user_id: i32 = session.get("user_id").unwrap().unwrap();
396		assert_eq!(user_id, 42);
397		let role: String = session.get("role").unwrap().unwrap();
398		assert_eq!(role, "admin");
399		let theme: String = session.get("theme").unwrap().unwrap();
400		assert_eq!(theme, "dark");
401	}
402
403	#[rstest]
404	#[tokio::test]
405	async fn test_rotation_metadata_stored() {
406		// Arrange
407		let backend = InMemorySessionBackend::new();
408		let mut session = Session::new(backend);
409		session.set("user_id", 1).unwrap();
410		let rotator = SessionRotator::new(RotationPolicy::OnLogin);
411
412		// Act
413		rotator.rotate(&mut session).await.unwrap();
414
415		// Assert
416		let metadata: RotationMetadata = session.get("_rotation_metadata").unwrap().unwrap();
417		assert_eq!(metadata.request_count, 0);
418		assert!(metadata.last_rotation <= Utc::now());
419	}
420
421	#[rstest]
422	#[tokio::test]
423	async fn test_never_policy_no_rotation() {
424		// Arrange
425		let rotator = SessionRotator::new(RotationPolicy::Never);
426		let metadata = RotationMetadata::default();
427
428		// Act
429		let should_rotate = rotator.should_rotate(&metadata);
430
431		// Assert
432		assert!(!should_rotate);
433	}
434}