auth_framework/server/oidc/
oidc_session_management.rs

1//! OpenID Connect Session Management Implementation
2//!
3//! This module provides comprehensive session management capabilities for OpenID Connect,
4//! serving as the foundation for RP-Initiated Logout, Front-Channel Logout,
5//! Back-Channel Logout, and other session-related specifications.
6//!
7//! # Features
8//!
9//! - Session state monitoring
10//! - Session management endpoints
11//! - iframe-based session checking
12//! - Session change notifications
13//! - Multi-tab session coordination
14
15use crate::errors::{AuthError, Result};
16use base64::{Engine as _, engine::general_purpose::STANDARD};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::time::{SystemTime, UNIX_EPOCH};
20use uuid::Uuid;
21
22/// Session state enumeration
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub enum SessionState {
25    /// User is authenticated
26    Authenticated,
27    /// User is not authenticated
28    Unauthenticated,
29    /// Session state changed
30    Changed,
31    /// Session state unknown/error
32    Unknown,
33}
34
35/// OpenID Connect session information
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct OidcSession {
38    /// Unique session identifier
39    pub session_id: String,
40    /// Subject (user) identifier
41    pub sub: String,
42    /// Client ID for this session
43    pub client_id: String,
44    /// Session creation timestamp
45    pub created_at: u64,
46    /// Last activity timestamp
47    pub last_activity: u64,
48    /// Session expiration timestamp
49    pub expires_at: u64,
50    /// Session state
51    pub state: SessionState,
52    /// Browser session identifier (session_state parameter)
53    pub browser_session_id: String,
54    /// Associated logout tokens for backchannel logout
55    pub logout_tokens: Vec<String>,
56    /// Session metadata
57    pub metadata: HashMap<String, String>,
58}
59
60/// Session Management configuration
61#[derive(Debug, Clone)]
62pub struct SessionManagementConfig {
63    /// Enable session management features
64    pub enabled: bool,
65    /// Session timeout in seconds
66    pub session_timeout: u64,
67    /// Check session interval in seconds
68    pub check_session_interval: u64,
69    /// Enable iframe session checking
70    pub enable_iframe_checking: bool,
71    /// Session management endpoints
72    pub check_session_iframe_endpoint: String,
73    /// End session endpoint
74    pub end_session_endpoint: String,
75}
76
77impl Default for SessionManagementConfig {
78    fn default() -> Self {
79        Self {
80            enabled: true,
81            session_timeout: 3600,      // 1 hour
82            check_session_interval: 30, // 30 seconds
83            enable_iframe_checking: true,
84            check_session_iframe_endpoint: "/connect/checksession".to_string(),
85            end_session_endpoint: "/connect/endsession".to_string(),
86        }
87    }
88}
89
90/// Session Management provider
91#[derive(Debug, Clone)]
92pub struct SessionManager {
93    /// Configuration
94    config: SessionManagementConfig,
95    /// Active sessions storage
96    sessions: HashMap<String, OidcSession>,
97}
98
99/// Session check request parameters
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SessionCheckRequest {
102    /// Client ID
103    pub client_id: String,
104    /// Session state value
105    pub session_state: String,
106    /// Origin for CORS
107    pub origin: String,
108}
109
110/// Session check response
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct SessionCheckResponse {
113    /// Session state
114    pub state: SessionState,
115    /// New session state value if changed
116    pub session_state: Option<String>,
117}
118
119impl SessionManager {
120    /// Create new session manager
121    pub fn new(config: SessionManagementConfig) -> Self {
122        Self {
123            config,
124            sessions: HashMap::new(),
125        }
126    }
127
128    /// Create new session
129    pub fn create_session(
130        &mut self,
131        sub: String,
132        client_id: String,
133        metadata: HashMap<String, String>,
134    ) -> Result<OidcSession> {
135        let now = SystemTime::now()
136            .duration_since(UNIX_EPOCH)
137            .unwrap()
138            .as_secs();
139
140        let session = OidcSession {
141            session_id: Uuid::new_v4().to_string(),
142            sub: sub.clone(),
143            client_id: client_id.clone(),
144            created_at: now,
145            last_activity: now,
146            expires_at: now + 3600, // Default 1 hour expiration
147            state: SessionState::Authenticated,
148            browser_session_id: self.generate_browser_session_id(&sub, &client_id)?,
149            logout_tokens: Vec::new(),
150            metadata,
151        };
152
153        self.sessions
154            .insert(session.session_id.clone(), session.clone());
155        Ok(session)
156    }
157
158    /// Get session by ID
159    pub fn get_session(&self, session_id: &str) -> Option<&OidcSession> {
160        self.sessions.get(session_id)
161    }
162
163    /// Update session activity
164    pub fn update_session_activity(&mut self, session_id: &str) -> Result<()> {
165        if let Some(session) = self.sessions.get_mut(session_id) {
166            let now = SystemTime::now()
167                .duration_since(UNIX_EPOCH)
168                .unwrap()
169                .as_secs();
170            session.last_activity = now;
171            Ok(())
172        } else {
173            Err(AuthError::validation("Session not found"))
174        }
175    }
176
177    /// Check if session is valid (not expired)
178    pub fn is_session_valid(&self, session_id: &str) -> bool {
179        if let Some(session) = self.get_session(session_id) {
180            let now = SystemTime::now()
181                .duration_since(UNIX_EPOCH)
182                .unwrap()
183                .as_secs();
184
185            now - session.last_activity < self.config.session_timeout
186        } else {
187            false
188        }
189    }
190
191    /// Generate browser session ID for session_state parameter
192    fn generate_browser_session_id(&self, sub: &str, client_id: &str) -> Result<String> {
193        // In a real implementation, this would be a cryptographically secure
194        // hash of session data + client salt
195        let data = format!("{}:{}:{}", sub, client_id, Uuid::new_v4());
196
197        // Simple base64 encoding for demo - use proper crypto in production
198        Ok(STANDARD.encode(data))
199    }
200
201    /// Check session state for iframe polling
202    pub fn check_session_state(
203        &self,
204        request: SessionCheckRequest,
205    ) -> Result<SessionCheckResponse> {
206        // Find session by browser session ID
207        let session = self.sessions.values().find(|s| {
208            s.browser_session_id == request.session_state && s.client_id == request.client_id
209        });
210
211        if let Some(session) = session {
212            if self.is_session_valid(&session.session_id) {
213                Ok(SessionCheckResponse {
214                    state: SessionState::Authenticated,
215                    session_state: None, // No change
216                })
217            } else {
218                Ok(SessionCheckResponse {
219                    state: SessionState::Unauthenticated,
220                    session_state: None,
221                })
222            }
223        } else {
224            Ok(SessionCheckResponse {
225                state: SessionState::Unauthenticated,
226                session_state: None,
227            })
228        }
229    }
230
231    /// End session (logout)
232    pub fn end_session(&mut self, session_id: &str) -> Result<OidcSession> {
233        if let Some(mut session) = self.sessions.remove(session_id) {
234            session.state = SessionState::Unauthenticated;
235            Ok(session)
236        } else {
237            Err(AuthError::validation("Session not found"))
238        }
239    }
240
241    /// Get check session iframe HTML
242    pub fn get_check_session_iframe(&self, client_id: &str) -> String {
243        format!(
244            r#"<!DOCTYPE html>
245<html>
246<head>
247    <title>Session Check</title>
248    <script>
249        (function() {{
250            var client_id = "{}";
251            var check_interval = {} * 1000; // Convert to milliseconds
252
253            function getCookie(name) {{
254                var cookies = document.cookie.split(';');
255                for (var i = 0; i < cookies.length; i++) {{
256                    var cookie = cookies[i].trim();
257                    if (cookie.indexOf(name + '=') === 0) {{
258                        return cookie.substring(name.length + 1);
259                    }}
260                }}
261                return null;
262            }}
263
264            function checkSession() {{
265                var sessionState = getCookie('session_state');
266                if (sessionState) {{
267                    // Notify parent window of session check
268                    window.parent.postMessage({{
269                        type: 'session_check',
270                        client_id: client_id,
271                        session_state: sessionState,
272                        state: 'unchanged'
273                    }}, '*');
274                }} else {{
275                    window.parent.postMessage({{
276                        type: 'session_check',
277                        client_id: client_id,
278                        state: 'unauthenticated'
279                    }}, '*');
280                }}
281            }}
282
283            // Initial check
284            checkSession();
285
286            // Periodic checking
287            setInterval(checkSession, check_interval);
288
289            // Listen for messages from parent
290            window.addEventListener('message', function(e) {{
291                if (e.data && e.data.type === 'check_session') {{
292                    checkSession();
293                }}
294            }});
295        }})();
296    </script>
297</head>
298<body>
299    <p>Session monitoring active...</p>
300</body>
301</html>"#,
302            client_id, self.config.check_session_interval
303        )
304    }
305
306    /// Clean up expired sessions
307    pub fn cleanup_expired_sessions(&mut self) -> usize {
308        let now = SystemTime::now()
309            .duration_since(UNIX_EPOCH)
310            .unwrap()
311            .as_secs();
312
313        let initial_count = self.sessions.len();
314
315        self.sessions
316            .retain(|_, session| now - session.last_activity < self.config.session_timeout);
317
318        initial_count - self.sessions.len()
319    }
320
321    /// Get all sessions for a subject
322    pub fn get_sessions_for_subject(&self, sub: &str) -> Vec<&OidcSession> {
323        self.sessions
324            .values()
325            .filter(|session| session.sub == sub)
326            .collect()
327    }
328
329    /// Add logout token to session (for backchannel logout)
330    pub fn add_logout_token(&mut self, session_id: &str, logout_token: String) -> Result<()> {
331        if let Some(session) = self.sessions.get_mut(session_id) {
332            session.logout_tokens.push(logout_token);
333            Ok(())
334        } else {
335            Err(AuthError::validation("Session not found"))
336        }
337    }
338
339    /// Get session management discovery metadata
340    pub fn get_discovery_metadata(&self) -> HashMap<String, serde_json::Value> {
341        let mut metadata = HashMap::new();
342
343        if self.config.enabled {
344            metadata.insert(
345                "check_session_iframe".to_string(),
346                serde_json::Value::String(self.config.check_session_iframe_endpoint.clone()),
347            );
348            metadata.insert(
349                "end_session_endpoint".to_string(),
350                serde_json::Value::String(self.config.end_session_endpoint.clone()),
351            );
352            metadata.insert(
353                "frontchannel_logout_supported".to_string(),
354                serde_json::Value::Bool(true),
355            );
356            metadata.insert(
357                "frontchannel_logout_session_supported".to_string(),
358                serde_json::Value::Bool(true),
359            );
360            metadata.insert(
361                "backchannel_logout_supported".to_string(),
362                serde_json::Value::Bool(true),
363            );
364            metadata.insert(
365                "backchannel_logout_session_supported".to_string(),
366                serde_json::Value::Bool(true),
367            );
368        }
369
370        metadata
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_session_creation() {
380        let mut manager = SessionManager::new(SessionManagementConfig::default());
381
382        let mut metadata = HashMap::new();
383        metadata.insert("ip_address".to_string(), "192.168.1.1".to_string());
384
385        let session = manager
386            .create_session("user123".to_string(), "client456".to_string(), metadata)
387            .unwrap();
388
389        assert_eq!(session.sub, "user123");
390        assert_eq!(session.client_id, "client456");
391        assert_eq!(session.state, SessionState::Authenticated);
392        assert!(!session.browser_session_id.is_empty());
393    }
394
395    #[test]
396    fn test_session_validity() {
397        let mut manager = SessionManager::new(SessionManagementConfig {
398            session_timeout: 1, // 1 second timeout for testing
399            ..SessionManagementConfig::default()
400        });
401
402        let session = manager
403            .create_session(
404                "user123".to_string(),
405                "client456".to_string(),
406                HashMap::new(),
407            )
408            .unwrap();
409
410        assert!(manager.is_session_valid(&session.session_id));
411
412        // Wait for timeout
413        std::thread::sleep(std::time::Duration::from_secs(2));
414
415        assert!(!manager.is_session_valid(&session.session_id));
416    }
417
418    #[test]
419    fn test_check_session_iframe_generation() {
420        let manager = SessionManager::new(SessionManagementConfig::default());
421
422        let html = manager.get_check_session_iframe("test_client");
423
424        assert!(html.contains("test_client"));
425        assert!(html.contains("session_check"));
426        assert!(html.contains("30 * 1000")); // Default interval
427    }
428}
429
430