1use crate::{
6 providers::session::{SessionData, SessionId, SessionProvider},
7 traits::{Authenticatable, SessionStorage, UserContext},
8 AuthError, AuthResult,
9};
10
11#[derive(Debug, Clone)]
13pub struct SessionMiddlewareConfig {
14 pub cookie_name: String,
16
17 pub cookie_domain: Option<String>,
19
20 pub cookie_path: String,
22
23 pub cookie_http_only: bool,
25
26 pub cookie_secure: bool,
28
29 pub cookie_same_site: CookieSameSite,
31
32 pub require_csrf: bool,
34
35 pub skip_paths: Vec<String>,
37
38 pub optional: bool,
40}
41
42#[derive(Debug, Clone)]
44pub enum CookieSameSite {
45 Strict,
46 Lax,
47 None,
48}
49
50impl std::fmt::Display for CookieSameSite {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 CookieSameSite::Strict => write!(f, "Strict"),
54 CookieSameSite::Lax => write!(f, "Lax"),
55 CookieSameSite::None => write!(f, "None"),
56 }
57 }
58}
59
60impl Default for SessionMiddlewareConfig {
61 fn default() -> Self {
62 Self {
63 cookie_name: "session_id".to_string(),
64 cookie_domain: None,
65 cookie_path: "/".to_string(),
66 cookie_http_only: true,
67 cookie_secure: false, cookie_same_site: CookieSameSite::Lax,
69 require_csrf: true,
70 skip_paths: vec!["/health".to_string(), "/metrics".to_string()],
71 optional: false,
72 }
73 }
74}
75
76impl SessionMiddlewareConfig {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
84 self.cookie_name = name.into();
85 self
86 }
87
88 pub fn cookie_domain(mut self, domain: impl Into<String>) -> Self {
90 self.cookie_domain = Some(domain.into());
91 self
92 }
93
94 pub fn cookie_path(mut self, path: impl Into<String>) -> Self {
96 self.cookie_path = path.into();
97 self
98 }
99
100 pub fn cookie_http_only(mut self, http_only: bool) -> Self {
102 self.cookie_http_only = http_only;
103 self
104 }
105
106 pub fn cookie_secure(mut self, secure: bool) -> Self {
108 self.cookie_secure = secure;
109 self
110 }
111
112 pub fn cookie_same_site(mut self, same_site: CookieSameSite) -> Self {
114 self.cookie_same_site = same_site;
115 self
116 }
117
118 pub fn require_csrf(mut self, require: bool) -> Self {
120 self.require_csrf = require;
121 self
122 }
123
124 pub fn skip_paths(mut self, paths: Vec<String>) -> Self {
126 self.skip_paths = paths;
127 self
128 }
129
130 pub fn skip_path(mut self, path: impl Into<String>) -> Self {
132 self.skip_paths.push(path.into());
133 self
134 }
135
136 pub fn optional(mut self, optional: bool) -> Self {
138 self.optional = optional;
139 self
140 }
141
142 pub fn production() -> Self {
144 Self {
145 cookie_secure: true,
146 cookie_same_site: CookieSameSite::Strict,
147 require_csrf: true,
148 ..Default::default()
149 }
150 }
151
152 pub fn development() -> Self {
154 Self {
155 cookie_secure: false,
156 require_csrf: false, ..Default::default()
158 }
159 }
160}
161
162pub struct SessionMiddleware<S, U>
166where
167 S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
168 U: Authenticatable,
169{
170 provider: SessionProvider<S, U>,
172
173 config: SessionMiddlewareConfig,
175}
176
177impl<S, U> SessionMiddleware<S, U>
178where
179 S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
180 U: Authenticatable + Clone,
181{
182 pub fn new(provider: SessionProvider<S, U>, config: SessionMiddlewareConfig) -> Self {
184 Self { provider, config }
185 }
186
187 pub fn with_default_config(provider: SessionProvider<S, U>) -> Self {
189 Self::new(provider, SessionMiddlewareConfig::default())
190 }
191
192 pub fn name(&self) -> &str {
194 "session"
195 }
196
197 pub fn extract_session_id_from_cookie(&self, cookie_header: &str) -> Option<SessionId> {
199 for cookie in cookie_header.split(';') {
200 let cookie = cookie.trim();
201 if let Some(value) = cookie.strip_prefix(&format!("{}=", self.config.cookie_name)) {
202 if let Ok(session_id) = SessionId::from_string(value.to_string()) {
203 return Some(session_id);
204 }
205 }
206 }
207 None
208 }
209
210 pub fn should_skip_path(&self, path: &str) -> bool {
212 self.config
213 .skip_paths
214 .iter()
215 .any(|skip_path| path.starts_with(skip_path))
216 }
217
218 pub fn create_cookie_header(&self, session_id: &SessionId, max_age: Option<i64>) -> String {
220 let mut cookie = format!("{}={}", self.config.cookie_name, session_id);
221
222 if let Some(domain) = &self.config.cookie_domain {
223 cookie.push_str(&format!("; Domain={}", domain));
224 }
225
226 cookie.push_str(&format!("; Path={}", self.config.cookie_path));
227
228 if self.config.cookie_http_only {
229 cookie.push_str("; HttpOnly");
230 }
231
232 if self.config.cookie_secure {
233 cookie.push_str("; Secure");
234 }
235
236 cookie.push_str(&format!("; SameSite={}", self.config.cookie_same_site));
237
238 if let Some(max_age) = max_age {
239 cookie.push_str(&format!("; Max-Age={}", max_age));
240 }
241
242 cookie
243 }
244
245 pub async fn validate_session(&self, session_id: &SessionId) -> AuthResult<SessionData> {
247 self.provider.validate_session(session_id).await
248 }
249
250 pub fn create_user_context(&self, session_data: &SessionData) -> UserContext {
252 UserContext {
253 user_id: session_data.user_id.clone(),
254 username: session_data.username.clone(),
255 roles: session_data.roles.clone(),
256 permissions: session_data.permissions.clone(),
257 auth_provider: "session".to_string(),
258 authenticated_at: session_data.created_at,
259 expires_at: Some(session_data.expires_at),
260 additional_data: session_data.metadata.clone(),
261 }
262 }
263
264 pub fn provider(&self) -> &SessionProvider<S, U> {
266 &self.provider
267 }
268
269 pub fn config(&self) -> &SessionMiddlewareConfig {
271 &self.config
272 }
273}
274
275pub struct SessionMiddlewareBuilder<S, U>
277where
278 S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
279 U: Authenticatable,
280{
281 provider: Option<SessionProvider<S, U>>,
282 config: SessionMiddlewareConfig,
283}
284
285impl<S, U> SessionMiddlewareBuilder<S, U>
286where
287 S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
288 U: Authenticatable,
289{
290 pub fn new() -> Self {
292 Self {
293 provider: None,
294 config: SessionMiddlewareConfig::default(),
295 }
296 }
297
298 pub fn provider(mut self, provider: SessionProvider<S, U>) -> Self {
300 self.provider = Some(provider);
301 self
302 }
303
304 pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
306 self.config.cookie_name = name.into();
307 self
308 }
309
310 pub fn optional(mut self) -> Self {
312 self.config.optional = true;
313 self
314 }
315
316 pub fn skip_paths(mut self, paths: Vec<String>) -> Self {
318 self.config.skip_paths = paths;
319 self
320 }
321
322 pub fn skip_path(mut self, path: impl Into<String>) -> Self {
324 self.config.skip_paths.push(path.into());
325 self
326 }
327
328 pub fn production(mut self) -> Self {
330 self.config = SessionMiddlewareConfig::production();
331 self
332 }
333
334 pub fn development(mut self) -> Self {
336 self.config = SessionMiddlewareConfig::development();
337 self
338 }
339
340 pub fn build(self) -> AuthResult<SessionMiddleware<S, U>>
342 where
343 U: Clone,
344 {
345 let provider = self
346 .provider
347 .ok_or_else(|| AuthError::generic_error("Session provider is required"))?;
348
349 Ok(SessionMiddleware::new(provider, self.config))
350 }
351}
352
353impl<S, U> Default for SessionMiddlewareBuilder<S, U>
354where
355 S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
356 U: Authenticatable,
357{
358 fn default() -> Self {
359 Self::new()
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::providers::session::MemorySessionStorage;
367
368 #[derive(Debug, Clone)]
370 struct MockUser {
371 id: String,
372 username: String,
373 roles: Vec<String>,
374 permissions: Vec<String>,
375 }
376
377 #[async_trait::async_trait]
378 impl Authenticatable for MockUser {
379 type Id = String;
380 type Credentials = String;
381
382 fn id(&self) -> &Self::Id {
383 &self.id
384 }
385
386 fn username(&self) -> &str {
387 &self.username
388 }
389
390 fn roles(&self) -> Vec<String> {
391 self.roles.clone()
392 }
393
394 fn permissions(&self) -> Vec<String> {
395 self.permissions.clone()
396 }
397
398 async fn verify_credentials(&self, _credentials: &Self::Credentials) -> AuthResult<bool> {
399 Ok(true)
400 }
401 }
402
403 #[test]
404 fn test_session_middleware_config() {
405 let config = SessionMiddlewareConfig::new()
406 .cookie_name("test_session")
407 .cookie_domain("example.com")
408 .cookie_secure(true)
409 .require_csrf(false)
410 .optional(true);
411
412 assert_eq!(config.cookie_name, "test_session");
413 assert_eq!(config.cookie_domain, Some("example.com".to_string()));
414 assert!(config.cookie_secure);
415 assert!(!config.require_csrf);
416 assert!(config.optional);
417 }
418
419 #[test]
420 fn test_cookie_same_site_display() {
421 assert_eq!(CookieSameSite::Strict.to_string(), "Strict");
422 assert_eq!(CookieSameSite::Lax.to_string(), "Lax");
423 assert_eq!(CookieSameSite::None.to_string(), "None");
424 }
425
426 #[test]
427 fn test_session_middleware_builder() {
428 let storage = MemorySessionStorage::new();
429 let provider: SessionProvider<MemorySessionStorage, MockUser> =
430 SessionProvider::with_default_config(storage);
431
432 let builder = SessionMiddlewareBuilder::new()
433 .provider(provider)
434 .cookie_name("test_session")
435 .optional()
436 .skip_path("/health");
437
438 let middleware = builder.build().unwrap();
439 assert_eq!(middleware.name(), "session");
440 }
441
442 #[tokio::test]
443 async fn test_cookie_header_creation() {
444 let storage = MemorySessionStorage::new();
445 let provider: SessionProvider<MemorySessionStorage, MockUser> =
446 SessionProvider::with_default_config(storage);
447 let config = SessionMiddlewareConfig::production();
448 let middleware = SessionMiddleware::new(provider, config);
449
450 let session_id = SessionId::generate();
451 let cookie = middleware.create_cookie_header(&session_id, Some(3600));
452
453 assert!(cookie.contains(&format!("session_id={}", session_id)));
454 assert!(cookie.contains("HttpOnly"));
455 assert!(cookie.contains("Secure"));
456 assert!(cookie.contains("SameSite=Strict"));
457 assert!(cookie.contains("Max-Age=3600"));
458 }
459
460 #[test]
461 fn test_session_id_extraction() {
462 let storage = MemorySessionStorage::new();
463 let provider: SessionProvider<MemorySessionStorage, MockUser> =
464 SessionProvider::with_default_config(storage);
465 let middleware = SessionMiddleware::with_default_config(provider);
466
467 let cookie_header = "other_cookie=value; session_id=short; another=value";
468 let extracted = middleware.extract_session_id_from_cookie(cookie_header);
469
470 assert!(extracted.is_none()); let valid_session_id = "a".repeat(32);
475 let cookie_header = format!("session_id={}", valid_session_id);
476 let extracted = middleware.extract_session_id_from_cookie(&cookie_header);
477 assert!(extracted.is_some());
478
479 let complex_cookie = format!("first=value1; session_id={}; last=value2", valid_session_id);
481 let extracted = middleware.extract_session_id_from_cookie(&complex_cookie);
482 assert!(extracted.is_some());
483 }
484
485 #[test]
486 fn test_path_skipping() {
487 let storage = MemorySessionStorage::new();
488 let provider: SessionProvider<MemorySessionStorage, MockUser> =
489 SessionProvider::with_default_config(storage);
490 let middleware = SessionMiddleware::with_default_config(provider);
491
492 assert!(middleware.should_skip_path("/health"));
493 assert!(middleware.should_skip_path("/metrics"));
494 assert!(!middleware.should_skip_path("/api/users"));
495 }
496}