Skip to main content

secure_identity/
session.rs

1//! Session management trait and types.
2
3use 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/// A user session.
14///
15/// # Examples
16///
17/// ```
18/// use secure_identity::session::InMemorySessionManager;
19///
20/// let mgr = InMemorySessionManager::new();
21/// ```
22#[derive(Clone, serde::Serialize, serde::Deserialize)]
23pub struct Session {
24    /// Cryptographically random session ID (128 bits, hex-encoded).
25    pub id: String,
26    /// The actor this session belongs to.
27    pub actor_id: ActorId,
28    /// The tenant this session belongs to, if applicable.
29    pub tenant_id: Option<TenantId>,
30    /// The roles assigned to this actor for this session.
31    pub roles: Vec<String>,
32    /// When the session was created.
33    pub created_at: OffsetDateTime,
34    /// When the session expires.
35    pub expires_at: OffsetDateTime,
36    /// When the session was last accessed.
37    pub last_accessed: OffsetDateTime,
38}
39
40/// A trait for managing sessions.
41///
42/// # Examples
43///
44/// ```
45/// use secure_identity::session::InMemorySessionManager;
46///
47/// // InMemorySessionManager implements SessionManager.
48/// let mgr = InMemorySessionManager::new();
49/// ```
50#[allow(async_fn_in_trait)]
51pub trait SessionManager {
52    /// Creates a new session for the given identity with the given lifetime in seconds.
53    async fn create_session(
54        &self,
55        identity: &AuthenticatedIdentity,
56        lifetime_secs: u64,
57    ) -> Result<Session, IdentityError>;
58
59    /// Validates a session by ID, returning it if valid and not expired.
60    async fn validate_session(&self, id: &str) -> Result<Session, IdentityError>;
61
62    /// Refreshes a session by ID, extending it by `extra_secs` seconds.
63    async fn refresh_session(&self, id: &str, extra_secs: u64) -> Result<Session, IdentityError>;
64
65    /// Revokes a session by ID.
66    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
77/// An in-memory session manager backed by a `tokio::sync::Mutex<HashMap>`.
78///
79/// # Examples
80///
81/// ```
82/// use secure_identity::session::InMemorySessionManager;
83///
84/// let mgr = InMemorySessionManager::new();
85/// ```
86pub struct InMemorySessionManager {
87    sessions: Mutex<HashMap<String, Session>>,
88}
89
90impl InMemorySessionManager {
91    /// Creates a new empty [`InMemorySessionManager`].
92    #[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}