secure_identity/
session.rs1use std::collections::HashMap;
4
5use ring::rand::{SecureRandom, SystemRandom};
6use security_core::identity::AuthenticatedIdentity;
7use security_core::types::{ActorId, TenantId};
8use time::OffsetDateTime;
9use tokio::sync::Mutex;
10
11use crate::error::IdentityError;
12
13#[derive(Clone, serde::Serialize, serde::Deserialize)]
23pub struct Session {
24 pub id: String,
26 pub actor_id: ActorId,
28 pub tenant_id: Option<TenantId>,
30 pub roles: Vec<String>,
32 pub created_at: OffsetDateTime,
34 pub expires_at: OffsetDateTime,
36 pub last_accessed: OffsetDateTime,
38}
39
40#[allow(async_fn_in_trait)]
51pub trait SessionManager {
52 async fn create_session(
54 &self,
55 identity: &AuthenticatedIdentity,
56 lifetime_secs: u64,
57 ) -> Result<Session, IdentityError>;
58
59 async fn validate_session(&self, id: &str) -> Result<Session, IdentityError>;
61
62 async fn refresh_session(&self, id: &str, extra_secs: u64) -> Result<Session, IdentityError>;
64
65 async fn revoke_session(&self, id: &str) -> Result<(), IdentityError>;
67}
68
69fn generate_session_id() -> Result<String, IdentityError> {
70 let rng = SystemRandom::new();
71 let mut bytes = [0u8; 16];
72 rng.fill(&mut bytes)
73 .map_err(|_| IdentityError::ProviderUnavailable)?;
74 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
75}
76
77pub struct InMemorySessionManager {
87 sessions: Mutex<HashMap<String, Session>>,
88}
89
90impl InMemorySessionManager {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 sessions: Mutex::new(HashMap::new()),
96 }
97 }
98}
99
100impl Default for InMemorySessionManager {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl SessionManager for InMemorySessionManager {
107 async fn create_session(
108 &self,
109 identity: &AuthenticatedIdentity,
110 lifetime_secs: u64,
111 ) -> Result<Session, IdentityError> {
112 let id = generate_session_id()?;
113 let now = OffsetDateTime::now_utc();
114 #[allow(clippy::cast_possible_truncation)]
115 let expires_at = now + time::Duration::seconds(lifetime_secs as i64);
116 let session = Session {
117 id: id.clone(),
118 actor_id: identity.actor_id.clone(),
119 tenant_id: identity.tenant_id.clone(),
120 roles: identity.roles.clone(),
121 created_at: now,
122 expires_at,
123 last_accessed: now,
124 };
125 self.sessions.lock().await.insert(id, session.clone());
126 Ok(session)
127 }
128
129 async fn validate_session(&self, id: &str) -> Result<Session, IdentityError> {
130 let now = OffsetDateTime::now_utc();
131 let mut guard = self.sessions.lock().await;
132 let session = guard
133 .get(id)
134 .cloned()
135 .ok_or(IdentityError::SessionExpired)?;
136 if now > session.expires_at {
137 guard.remove(id);
138 return Err(IdentityError::SessionExpired);
139 }
140 let mut session = session;
141 session.last_accessed = now;
142 guard.insert(id.to_owned(), session.clone());
143 Ok(session)
144 }
145
146 async fn refresh_session(&self, id: &str, extra_secs: u64) -> Result<Session, IdentityError> {
147 let now = OffsetDateTime::now_utc();
148 let mut guard = self.sessions.lock().await;
149 let session = guard.get_mut(id).ok_or(IdentityError::SessionExpired)?;
150 if now > session.expires_at {
151 return Err(IdentityError::SessionExpired);
152 }
153 #[allow(clippy::cast_possible_truncation)]
154 let extra = time::Duration::seconds(extra_secs as i64);
155 session.expires_at += extra;
156 session.last_accessed = now;
157 Ok(session.clone())
158 }
159
160 async fn revoke_session(&self, id: &str) -> Result<(), IdentityError> {
161 self.sessions.lock().await.remove(id);
162 Ok(())
163 }
164}