Skip to main content

better_auth_core/
session.rs

1use chrono::Utc;
2use std::sync::Arc;
3
4use crate::adapters::DatabaseAdapter;
5use crate::config::AuthConfig;
6use crate::entity::{AuthSession, AuthUser};
7use crate::error::AuthResult;
8use crate::types::CreateSession;
9
10/// Session manager handles session creation, validation, and cleanup
11pub struct SessionManager<DB: DatabaseAdapter> {
12    config: Arc<AuthConfig>,
13    database: Arc<DB>,
14}
15
16impl<DB: DatabaseAdapter> Clone for SessionManager<DB> {
17    fn clone(&self) -> Self {
18        Self {
19            config: self.config.clone(),
20            database: self.database.clone(),
21        }
22    }
23}
24
25impl<DB: DatabaseAdapter> SessionManager<DB> {
26    pub fn new(config: Arc<AuthConfig>, database: Arc<DB>) -> Self {
27        Self { config, database }
28    }
29
30    /// Create a new session for a user
31    pub async fn create_session(
32        &self,
33        user: &impl AuthUser,
34        ip_address: Option<String>,
35        user_agent: Option<String>,
36    ) -> AuthResult<DB::Session> {
37        let expires_at = Utc::now() + self.config.session.expires_in;
38
39        let create_session = CreateSession {
40            user_id: user.id().to_string(),
41            expires_at,
42            ip_address,
43            user_agent,
44            impersonated_by: None,
45            active_organization_id: None,
46        };
47
48        let session = self.database.create_session(create_session).await?;
49        Ok(session)
50    }
51
52    /// Get session by token
53    pub async fn get_session(&self, token: &str) -> AuthResult<Option<DB::Session>> {
54        let session = self.database.get_session(token).await?;
55
56        // Check if session exists and is not expired
57        if let Some(ref session) = session {
58            let now = Utc::now();
59
60            if session.expires_at() < now || !session.active() {
61                // Session expired or inactive - delete it
62                self.database.delete_session(token).await?;
63                return Ok(None);
64            }
65
66            // Update session if configured to do so
67            if !self.config.session.disable_session_refresh {
68                let should_refresh = match self.config.session.update_age {
69                    Some(age) => {
70                        // Only refresh if the session was last updated more than
71                        // `update_age` ago.
72                        let updated = session.updated_at();
73                        Utc::now() - updated >= age
74                    }
75                    // No update_age set → refresh on every access.
76                    None => true,
77                };
78
79                if should_refresh {
80                    let new_expires_at = Utc::now() + self.config.session.expires_in;
81                    let _ = self
82                        .database
83                        .update_session_expiry(token, new_expires_at)
84                        .await;
85                }
86            }
87        }
88
89        Ok(session)
90    }
91
92    /// Delete a session
93    pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
94        self.database.delete_session(token).await?;
95        Ok(())
96    }
97
98    /// Delete all sessions for a user
99    pub async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()> {
100        self.database.delete_user_sessions(user_id).await?;
101        Ok(())
102    }
103
104    /// Get all active sessions for a user
105    pub async fn list_user_sessions(&self, user_id: &str) -> AuthResult<Vec<DB::Session>> {
106        let sessions = self.database.get_user_sessions(user_id).await?;
107        let now = Utc::now();
108
109        // Filter out expired sessions
110        let active_sessions: Vec<DB::Session> = sessions
111            .into_iter()
112            .filter(|session| session.expires_at() > now && session.active())
113            .collect();
114
115        Ok(active_sessions)
116    }
117
118    /// Revoke a specific session by token
119    pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
120        // Check if session exists before trying to delete
121        let session_exists = self.get_session(token).await?.is_some();
122
123        if session_exists {
124            self.delete_session(token).await?;
125            Ok(true)
126        } else {
127            Ok(false)
128        }
129    }
130
131    /// Revoke all sessions for a user
132    pub async fn revoke_all_user_sessions(&self, user_id: &str) -> AuthResult<usize> {
133        // Get count of sessions before deletion for return value
134        let sessions = self.list_user_sessions(user_id).await?;
135        let count = sessions.len();
136
137        self.delete_user_sessions(user_id).await?;
138        Ok(count)
139    }
140
141    /// Revoke all sessions for a user except the current one
142    pub async fn revoke_other_user_sessions(
143        &self,
144        user_id: &str,
145        current_token: &str,
146    ) -> AuthResult<usize> {
147        let sessions = self.list_user_sessions(user_id).await?;
148        let mut count = 0;
149
150        for session in sessions {
151            if session.token() != current_token {
152                self.delete_session(session.token()).await?;
153                count += 1;
154            }
155        }
156
157        Ok(count)
158    }
159
160    /// Cleanup expired sessions
161    pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
162        let count = self.database.delete_expired_sessions().await?;
163        Ok(count)
164    }
165
166    /// Check whether a session is "fresh" (created recently enough for
167    /// sensitive operations like password change or account deletion).
168    ///
169    /// Returns `true` when `fresh_age` is set and
170    /// `session.created_at() + fresh_age > now`.
171    /// If `fresh_age` is `None`, the session is never considered fresh.
172    pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
173        match self.config.session.fresh_age {
174            Some(fresh_age) => session.created_at() + fresh_age > Utc::now(),
175            None => false,
176        }
177    }
178
179    /// Validate session token format
180    pub fn validate_token_format(&self, token: &str) -> bool {
181        token.starts_with("session_") && token.len() > 40
182    }
183
184    /// Extract session token from a request.
185    ///
186    /// Tries Bearer token from Authorization header first, then falls back
187    /// to parsing the configured cookie from the Cookie header.
188    pub fn extract_session_token(&self, req: &crate::types::AuthRequest) -> Option<String> {
189        // Try Bearer token first
190        if let Some(auth_header) = req.headers.get("authorization")
191            && let Some(token) = auth_header.strip_prefix("Bearer ")
192        {
193            return Some(token.to_string());
194        }
195
196        // Fall back to cookie (using the `cookie` crate for correct parsing)
197        if let Some(cookie_header) = req.headers.get("cookie") {
198            let cookie_name = &self.config.session.cookie_name;
199            for c in cookie::Cookie::split_parse(cookie_header).flatten() {
200                if c.name() == cookie_name && !c.value().is_empty() {
201                    return Some(c.value().to_string());
202                }
203            }
204        }
205
206        None
207    }
208}