auth_framework/server/oidc/
oidc_session_management.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub enum SessionState {
25 Authenticated,
27 Unauthenticated,
29 Changed,
31 Unknown,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct OidcSession {
38 pub session_id: String,
40 pub sub: String,
42 pub client_id: String,
44 pub created_at: u64,
46 pub last_activity: u64,
48 pub expires_at: u64,
50 pub state: SessionState,
52 pub browser_session_id: String,
54 pub logout_tokens: Vec<String>,
56 pub metadata: HashMap<String, String>,
58}
59
60#[derive(Debug, Clone)]
62pub struct SessionManagementConfig {
63 pub enabled: bool,
65 pub session_timeout: u64,
67 pub check_session_interval: u64,
69 pub enable_iframe_checking: bool,
71 pub check_session_iframe_endpoint: String,
73 pub end_session_endpoint: String,
75}
76
77impl Default for SessionManagementConfig {
78 fn default() -> Self {
79 Self {
80 enabled: true,
81 session_timeout: 3600, check_session_interval: 30, 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#[derive(Debug, Clone)]
92pub struct SessionManager {
93 config: SessionManagementConfig,
95 sessions: HashMap<String, OidcSession>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SessionCheckRequest {
102 pub client_id: String,
104 pub session_state: String,
106 pub origin: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct SessionCheckResponse {
113 pub state: SessionState,
115 pub session_state: Option<String>,
117}
118
119impl SessionManager {
120 pub fn new(config: SessionManagementConfig) -> Self {
122 Self {
123 config,
124 sessions: HashMap::new(),
125 }
126 }
127
128 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, 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 pub fn get_session(&self, session_id: &str) -> Option<&OidcSession> {
160 self.sessions.get(session_id)
161 }
162
163 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 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 fn generate_browser_session_id(&self, sub: &str, client_id: &str) -> Result<String> {
193 let data = format!("{}:{}:{}", sub, client_id, Uuid::new_v4());
196
197 Ok(STANDARD.encode(data))
199 }
200
201 pub fn check_session_state(
203 &self,
204 request: SessionCheckRequest,
205 ) -> Result<SessionCheckResponse> {
206 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, })
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 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 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 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 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 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 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, ..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 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")); }
428}
429
430