auth_framework/server/oidc/
oidc_session_management.rs1use crate::errors::{AuthError, Result};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use std::collections::HashMap;
19use std::time::{SystemTime, UNIX_EPOCH};
20use uuid::Uuid;
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub enum OidcSessionState {
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: OidcSessionState,
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: OidcSessionState,
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_or_default()
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: OidcSessionState::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_or_default()
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_or_default()
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> {
196 let nonce = Uuid::new_v4().to_string();
197 let mut hasher = Sha256::new();
198 hasher.update(sub.as_bytes());
199 hasher.update(b":");
200 hasher.update(client_id.as_bytes());
201 hasher.update(b":");
202 hasher.update(nonce.as_bytes());
203 let hash = hasher.finalize();
204 Ok(hex::encode(hash))
205 }
206
207 pub fn check_session_state(
209 &self,
210 request: SessionCheckRequest,
211 ) -> Result<SessionCheckResponse> {
212 let session = self.sessions.values().find(|s| {
214 s.browser_session_id == request.session_state && s.client_id == request.client_id
215 });
216
217 if let Some(session) = session {
218 if self.is_session_valid(&session.session_id) {
219 Ok(SessionCheckResponse {
220 state: OidcSessionState::Authenticated,
221 session_state: None, })
223 } else {
224 Ok(SessionCheckResponse {
225 state: OidcSessionState::Unauthenticated,
226 session_state: None,
227 })
228 }
229 } else {
230 Ok(SessionCheckResponse {
231 state: OidcSessionState::Unauthenticated,
232 session_state: None,
233 })
234 }
235 }
236
237 pub fn end_session(&mut self, session_id: &str) -> Result<OidcSession> {
239 if let Some(mut session) = self.sessions.remove(session_id) {
240 session.state = OidcSessionState::Unauthenticated;
241 Ok(session)
242 } else {
243 Err(AuthError::validation("Session not found"))
244 }
245 }
246
247 pub fn get_check_session_iframe(&self, client_id: &str) -> String {
249 format!(
250 r#"<!DOCTYPE html>
251<html>
252<head>
253 <title>Session Check</title>
254 <script>
255 (function() {{
256 var client_id = "{}";
257 var check_interval = {} * 1000; // Convert to milliseconds
258
259 function getCookie(name) {{
260 var cookies = document.cookie.split(';');
261 for (var i = 0; i < cookies.length; i++) {{
262 var cookie = cookies[i].trim();
263 if (cookie.indexOf(name + '=') === 0) {{
264 return cookie.substring(name.length + 1);
265 }}
266 }}
267 return null;
268 }}
269
270 function checkSession() {{
271 var OidcSessionState = getCookie('session_state');
272 if (OidcSessionState) {{
273 // Notify parent window of session check
274 window.parent.postMessage({{
275 type: 'session_check',
276 client_id: client_id,
277 session_state: OidcSessionState,
278 state: 'unchanged'
279 }}, '*');
280 }} else {{
281 window.parent.postMessage({{
282 type: 'session_check',
283 client_id: client_id,
284 state: 'unauthenticated'
285 }}, '*');
286 }}
287 }}
288
289 // Initial check
290 checkSession();
291
292 // Periodic checking
293 setInterval(checkSession, check_interval);
294
295 // Listen for messages from parent
296 window.addEventListener('message', function(e) {{
297 if (e.data && e.data.type === 'check_session') {{
298 checkSession();
299 }}
300 }});
301 }})();
302 </script>
303</head>
304<body>
305 <p>Session monitoring active...</p>
306</body>
307</html>"#,
308 client_id, self.config.check_session_interval
309 )
310 }
311
312 pub fn cleanup_expired_sessions(&mut self) -> usize {
314 let now = SystemTime::now()
315 .duration_since(UNIX_EPOCH)
316 .unwrap_or_default()
317 .as_secs();
318
319 let initial_count = self.sessions.len();
320
321 self.sessions
322 .retain(|_, session| now - session.last_activity < self.config.session_timeout);
323
324 initial_count - self.sessions.len()
325 }
326
327 pub fn get_sessions_for_subject(&self, sub: &str) -> Vec<&OidcSession> {
329 self.sessions
330 .values()
331 .filter(|session| session.sub == sub)
332 .collect()
333 }
334
335 pub fn add_logout_token(&mut self, session_id: &str, logout_token: String) -> Result<()> {
337 if let Some(session) = self.sessions.get_mut(session_id) {
338 session.logout_tokens.push(logout_token);
339 Ok(())
340 } else {
341 Err(AuthError::validation("Session not found"))
342 }
343 }
344
345 pub fn get_discovery_metadata(&self) -> HashMap<String, serde_json::Value> {
347 let mut metadata = HashMap::new();
348
349 if self.config.enabled {
350 metadata.insert(
351 "check_session_iframe".to_string(),
352 serde_json::Value::String(self.config.check_session_iframe_endpoint.clone()),
353 );
354 metadata.insert(
355 "end_session_endpoint".to_string(),
356 serde_json::Value::String(self.config.end_session_endpoint.clone()),
357 );
358 metadata.insert(
359 "frontchannel_logout_supported".to_string(),
360 serde_json::Value::Bool(true),
361 );
362 metadata.insert(
363 "frontchannel_logout_session_supported".to_string(),
364 serde_json::Value::Bool(true),
365 );
366 metadata.insert(
367 "backchannel_logout_supported".to_string(),
368 serde_json::Value::Bool(true),
369 );
370 metadata.insert(
371 "backchannel_logout_session_supported".to_string(),
372 serde_json::Value::Bool(true),
373 );
374 }
375
376 metadata
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_session_creation() {
386 let mut manager = SessionManager::new(SessionManagementConfig::default());
387
388 let mut metadata = HashMap::new();
389 metadata.insert("ip_address".to_string(), "192.168.1.1".to_string());
390
391 let session = manager
392 .create_session("user123".to_string(), "client456".to_string(), metadata)
393 .unwrap();
394
395 assert_eq!(session.sub, "user123");
396 assert_eq!(session.client_id, "client456");
397 assert_eq!(session.state, OidcSessionState::Authenticated);
398 assert!(!session.browser_session_id.is_empty());
399 }
400
401 #[test]
402 fn test_session_validity() {
403 let mut manager = SessionManager::new(SessionManagementConfig {
404 session_timeout: 1, ..SessionManagementConfig::default()
406 });
407
408 let session = manager
409 .create_session(
410 "user123".to_string(),
411 "client456".to_string(),
412 HashMap::new(),
413 )
414 .unwrap();
415
416 assert!(manager.is_session_valid(&session.session_id));
417
418 std::thread::sleep(std::time::Duration::from_secs(2));
420
421 assert!(!manager.is_session_valid(&session.session_id));
422 }
423
424 #[test]
425 fn test_check_session_iframe_generation() {
426 let manager = SessionManager::new(SessionManagementConfig::default());
427
428 let html = manager.get_check_session_iframe("test_client");
429
430 assert!(html.contains("test_client"));
431 assert!(html.contains("session_check"));
432 assert!(html.contains("30 * 1000")); }
434}