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