Skip to main content

elif_auth/middleware/
session.rs

1//! Session authentication middleware
2//!
3//! This module provides session-based authentication middleware similar to JWT middleware.
4
5use crate::{
6    providers::session::{SessionData, SessionId, SessionProvider},
7    traits::{Authenticatable, SessionStorage, UserContext},
8    AuthError, AuthResult,
9};
10
11/// Session middleware configuration
12#[derive(Debug, Clone)]
13pub struct SessionMiddlewareConfig {
14    /// Cookie name for session ID
15    pub cookie_name: String,
16
17    /// Session cookie domain
18    pub cookie_domain: Option<String>,
19
20    /// Session cookie path
21    pub cookie_path: String,
22
23    /// Whether session cookie is HTTP-only
24    pub cookie_http_only: bool,
25
26    /// Whether session cookie is secure (HTTPS only)
27    pub cookie_secure: bool,
28
29    /// Session cookie SameSite attribute
30    pub cookie_same_site: CookieSameSite,
31
32    /// Require CSRF protection for session
33    pub require_csrf: bool,
34
35    /// Paths to skip authentication
36    pub skip_paths: Vec<String>,
37
38    /// Whether authentication is optional (doesn't fail if no session)
39    pub optional: bool,
40}
41
42/// Cookie SameSite attribute values
43#[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, // Set to true in production
68            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    /// Create a new configuration
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Set cookie name
83    pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
84        self.cookie_name = name.into();
85        self
86    }
87
88    /// Set cookie domain
89    pub fn cookie_domain(mut self, domain: impl Into<String>) -> Self {
90        self.cookie_domain = Some(domain.into());
91        self
92    }
93
94    /// Set cookie path
95    pub fn cookie_path(mut self, path: impl Into<String>) -> Self {
96        self.cookie_path = path.into();
97        self
98    }
99
100    /// Set whether cookie is HTTP-only
101    pub fn cookie_http_only(mut self, http_only: bool) -> Self {
102        self.cookie_http_only = http_only;
103        self
104    }
105
106    /// Set whether cookie is secure
107    pub fn cookie_secure(mut self, secure: bool) -> Self {
108        self.cookie_secure = secure;
109        self
110    }
111
112    /// Set cookie SameSite attribute
113    pub fn cookie_same_site(mut self, same_site: CookieSameSite) -> Self {
114        self.cookie_same_site = same_site;
115        self
116    }
117
118    /// Set whether CSRF protection is required
119    pub fn require_csrf(mut self, require: bool) -> Self {
120        self.require_csrf = require;
121        self
122    }
123
124    /// Add paths to skip authentication
125    pub fn skip_paths(mut self, paths: Vec<String>) -> Self {
126        self.skip_paths = paths;
127        self
128    }
129
130    /// Add a path to skip authentication
131    pub fn skip_path(mut self, path: impl Into<String>) -> Self {
132        self.skip_paths.push(path.into());
133        self
134    }
135
136    /// Set whether authentication is optional
137    pub fn optional(mut self, optional: bool) -> Self {
138        self.optional = optional;
139        self
140    }
141
142    /// Production-ready configuration
143    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    /// Development configuration  
153    pub fn development() -> Self {
154        Self {
155            cookie_secure: false,
156            require_csrf: false, // Relaxed for development
157            ..Default::default()
158        }
159    }
160}
161
162/// Session authentication middleware (placeholder for Phase 5.4)
163///
164/// This will be implemented in Phase 5.4: User Authentication Middleware
165pub struct SessionMiddleware<S, U>
166where
167    S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
168    U: Authenticatable,
169{
170    /// Session provider for session operations
171    provider: SessionProvider<S, U>,
172
173    /// Session extraction configuration
174    config: SessionMiddlewareConfig,
175}
176
177impl<S, U> SessionMiddleware<S, U>
178where
179    S: SessionStorage<SessionId = SessionId, SessionData = SessionData>,
180    U: Authenticatable + Clone,
181{
182    /// Create new session middleware
183    pub fn new(provider: SessionProvider<S, U>, config: SessionMiddlewareConfig) -> Self {
184        Self { provider, config }
185    }
186
187    /// Create session middleware with default config
188    pub fn with_default_config(provider: SessionProvider<S, U>) -> Self {
189        Self::new(provider, SessionMiddlewareConfig::default())
190    }
191
192    /// Get middleware name
193    pub fn name(&self) -> &str {
194        "session"
195    }
196
197    /// Extract session ID from cookie header string
198    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    /// Check if path should be skipped
211    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    /// Create session cookie string
219    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    /// Validate session and get session data
246    pub async fn validate_session(&self, session_id: &SessionId) -> AuthResult<SessionData> {
247        self.provider.validate_session(session_id).await
248    }
249
250    /// Create user context from session data
251    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    /// Get the session provider
265    pub fn provider(&self) -> &SessionProvider<S, U> {
266        &self.provider
267    }
268
269    /// Get the configuration
270    pub fn config(&self) -> &SessionMiddlewareConfig {
271        &self.config
272    }
273}
274
275/// Builder for session middleware
276pub 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    /// Create new builder
291    pub fn new() -> Self {
292        Self {
293            provider: None,
294            config: SessionMiddlewareConfig::default(),
295        }
296    }
297
298    /// Set session provider
299    pub fn provider(mut self, provider: SessionProvider<S, U>) -> Self {
300        self.provider = Some(provider);
301        self
302    }
303
304    /// Set cookie name
305    pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
306        self.config.cookie_name = name.into();
307        self
308    }
309
310    /// Make authentication optional
311    pub fn optional(mut self) -> Self {
312        self.config.optional = true;
313        self
314    }
315
316    /// Skip authentication for specific paths
317    pub fn skip_paths(mut self, paths: Vec<String>) -> Self {
318        self.config.skip_paths = paths;
319        self
320    }
321
322    /// Add a path to skip
323    pub fn skip_path(mut self, path: impl Into<String>) -> Self {
324        self.config.skip_paths.push(path.into());
325        self
326    }
327
328    /// Use production configuration
329    pub fn production(mut self) -> Self {
330        self.config = SessionMiddlewareConfig::production();
331        self
332    }
333
334    /// Use development configuration
335    pub fn development(mut self) -> Self {
336        self.config = SessionMiddlewareConfig::development();
337        self
338    }
339
340    /// Build the middleware
341    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    // Mock user for testing
369    #[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        // This will fail since "short" is too short for validation
471        assert!(extracted.is_none()); // Because it fails validation
472
473        // Test with a valid length session ID
474        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        // Test extraction from complex cookie header
480        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}