Skip to main content

better_auth_core/
session.rs

1use chrono::Utc;
2use std::sync::Arc;
3
4use crate::config::AuthConfig;
5use crate::entity::{AuthSession, AuthUser};
6use crate::error::AuthResult;
7use crate::schema::AuthSchema;
8use crate::store::{AuthStore, JoinValue};
9use crate::types::CreateSession;
10use crate::utils::cookie_utils::{
11    get_cookie, related_cookie_name, session_cookie_headers, verify_cookie_value,
12};
13use crate::wire::{SessionView, UserView};
14use crate::{AuthError, AuthRequest, HttpMethod};
15use serde::{Deserialize, Serialize};
16
17#[cfg(test)]
18mod cache_tests;
19mod cookie_cache;
20mod native;
21mod response;
22mod signer;
23pub use native::NativeSessionData;
24pub use signer::{SessionCookieContext, SessionCookieSigner};
25#[cfg(test)]
26mod response_tests;
27mod view;
28
29/// A session and its selected User value, independent of the application's storage models.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SessionData<U = UserView> {
32    /// Session visible to the caller.
33    pub session: SessionView,
34    /// User visible to the caller.
35    pub user: U,
36}
37
38/// Session read result, including the deferred-refresh signal.
39pub struct SessionResolution<T = SessionData> {
40    /// Authenticated session, when a valid credential was supplied.
41    pub data: Option<T>,
42    /// Whether a deferred GET requires a subsequent POST refresh.
43    pub needs_refresh: Option<bool>,
44}
45
46/// Select whether an authentication check may use a cookie cache.
47#[derive(Clone, Copy)]
48pub enum SessionRead {
49    /// Allow a valid signed cookie cache.
50    Cached,
51    /// Read the session from the server store, for sensitive operations.
52    Authoritative,
53}
54
55/// Session manager handles session creation, validation, and cleanup
56pub struct SessionManager<S: AuthSchema> {
57    capabilities: crate::store::StoreCapabilities,
58    user_metadata: crate::plugin::MetadataMap,
59    adapter_user_fields: crate::user_fields::UserConfig,
60    config: Arc<AuthConfig>,
61    database: Arc<dyn AuthStore<S>>,
62    signer: Option<Arc<dyn SessionCookieSigner<S>>>,
63}
64
65impl<S: AuthSchema> Clone for SessionManager<S> {
66    fn clone(&self) -> Self {
67        Self {
68            capabilities: self.capabilities,
69            user_metadata: self.user_metadata.clone(),
70            adapter_user_fields: self.adapter_user_fields.clone(),
71            config: self.config.clone(),
72            database: self.database.clone(),
73            signer: self.signer.clone(),
74        }
75    }
76}
77
78impl<S: AuthSchema> SessionManager<S> {
79    pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
80        Self {
81            capabilities: Default::default(),
82            adapter_user_fields: config.user.clone(),
83            config,
84            database,
85            user_metadata: Default::default(),
86            signer: None,
87        }
88    }
89
90    /// Select adapter transforms separately from public field visibility.
91    pub fn with_adapter_user_fields(mut self, fields: crate::user_fields::UserConfig) -> Self {
92        self.adapter_user_fields = fields;
93        self
94    }
95
96    /// Attach enabled user plugin schemas for database and cache projections.
97    pub fn with_user_metadata(mut self, metadata: crate::plugin::MetadataMap) -> Self {
98        self.user_metadata = metadata;
99        self
100    }
101
102    pub(crate) fn with_store_capabilities(
103        mut self,
104        capabilities: crate::store::StoreCapabilities,
105    ) -> Self {
106        self.capabilities = capabilities;
107        self
108    }
109
110    async fn user_view(&self, user: &impl AuthUser) -> AuthResult<UserView> {
111        UserView::with_field_policies(
112            user,
113            &self.adapter_user_fields,
114            &self.config.user,
115            &self.user_metadata,
116            self.database.supports_native_json(),
117        )
118        .await
119    }
120
121    pub async fn session_view(&self, session: &impl AuthSession) -> AuthResult<SessionView> {
122        let mut view = self.internal_session_view(session).await?;
123        view.filter_returned_fields(&self.config.session);
124        Ok(view)
125    }
126
127    /// Include hidden session fields for trusted callbacks before public output filtering.
128    pub async fn internal_session_view(
129        &self,
130        session: &impl AuthSession,
131    ) -> AuthResult<SessionView> {
132        let mut view = SessionView::with_internal_fields_for_adapter(
133            session,
134            &self.config.session,
135            self.database.supports_native_json(),
136        )
137        .await?;
138        view.visible_fields = Some(
139            SessionView::active_plugin_fields(&self.user_metadata)
140                .filter(|name| {
141                    session
142                        .field_presence()
143                        .is_none_or(|fields| fields.contains(*name))
144                })
145                .map(str::to_owned)
146                .collect(),
147        );
148        Ok(view)
149    }
150
151    async fn internal_user_view(&self, user: &impl AuthUser) -> AuthResult<UserView> {
152        UserView::with_internal_fields_for_adapter(
153            user,
154            &self.adapter_user_fields,
155            &self.user_metadata,
156            self.database.supports_native_json(),
157        )
158        .await
159    }
160
161    /// Project an adapter result without removing fields available to trusted callbacks.
162    pub async fn internal_data(
163        &self,
164        user: &impl AuthUser,
165        session: &impl AuthSession,
166    ) -> AuthResult<SessionData> {
167        Ok(SessionData {
168            user: self.internal_user_view(user).await?,
169            session: self.internal_session_view(session).await?,
170        })
171    }
172
173    /// Write session credentials and retain the supplied identity for endpoint after hooks.
174    pub async fn set_session_cookie(
175        &self,
176        req: &AuthRequest,
177        data: SessionData,
178        dont_remember: Option<bool>,
179    ) -> AuthResult<()> {
180        self.set_native_session_cookie(req, data.into(), dont_remember)
181            .await
182    }
183
184    /// Write credentials while preserving the native User value for endpoint after hooks.
185    pub async fn set_native_session_cookie(
186        &self,
187        req: &AuthRequest,
188        data: NativeSessionData,
189        dont_remember: Option<bool>,
190    ) -> AuthResult<()> {
191        self.issue_session_cookie(req, data, dont_remember, None)
192            .await
193    }
194
195    /// Issue credentials while keeping signing-key reads and writes in the active transaction.
196    pub async fn set_session_cookie_in_transaction(
197        &self,
198        req: &AuthRequest,
199        data: SessionData,
200        dont_remember: Option<bool>,
201        transaction: &dyn crate::store::AuthTransaction<S>,
202    ) -> AuthResult<()> {
203        self.issue_session_cookie(req, data.into(), dont_remember, Some(transaction))
204            .await
205    }
206
207    async fn issue_session_cookie(
208        &self,
209        req: &AuthRequest,
210        data: NativeSessionData,
211        dont_remember: Option<bool>,
212        transaction: Option<&dyn crate::store::AuthTransaction<S>>,
213    ) -> AuthResult<()> {
214        let dont_remember = dont_remember.unwrap_or_else(|| self.dont_remember(req));
215        for cookie in session_cookie_headers(&data.session.token, dont_remember, &self.config) {
216            req.append_response_header("Set-Cookie", cookie?)?;
217        }
218        self.write_cache_with_response(req, &data, dont_remember, None, transaction)
219            .await?;
220        req.set_new_session(data)
221    }
222
223    fn public_data(
224        &self,
225        mut data: SessionData<JoinValue<UserView>>,
226    ) -> SessionData<JoinValue<UserView>> {
227        if let JoinValue::One(Some(user)) = &mut data.user {
228            user.filter_cached_fields(&self.config.user);
229        }
230        data.session.filter_returned_fields(&self.config.session);
231        data
232    }
233
234    /// Install a plugin-provided signer without coupling core to the plugin implementation.
235    pub fn with_cookie_signer(mut self, signer: Option<Arc<dyn SessionCookieSigner<S>>>) -> Self {
236        self.signer = signer;
237        self
238    }
239
240    /// Create a new session for a user
241    pub async fn create_session(
242        &self,
243        user: &impl AuthUser,
244        ip_address: Option<String>,
245        user_agent: Option<String>,
246    ) -> AuthResult<crate::wire::SessionView> {
247        self.create_session_for_id(user.id().into_owned(), ip_address, user_agent)
248            .await
249    }
250
251    /// Preserve the selected runtime User ID, including an omitted ID, during session creation.
252    pub async fn create_session_for_id(
253        &self,
254        user_id: crate::SchemaValue<String>,
255        ip_address: Option<String>,
256        user_agent: Option<String>,
257    ) -> AuthResult<SessionView> {
258        self.create_session_for_id_with_lifetime(
259            user_id,
260            ip_address,
261            user_agent,
262            self.config.session.expires_in(),
263        )
264        .await
265    }
266
267    /// Create a session with a lifetime that overrides the configured default.
268    pub async fn create_session_with_lifetime(
269        &self,
270        user: &impl AuthUser,
271        ip_address: Option<String>,
272        user_agent: Option<String>,
273        expires_in: chrono::Duration,
274    ) -> AuthResult<crate::wire::SessionView> {
275        self.create_session_for_id_with_lifetime(
276            user.id().into_owned(),
277            ip_address,
278            user_agent,
279            expires_in,
280        )
281        .await
282    }
283
284    /// Create a session from a native User ID with an explicit lifetime.
285    pub async fn create_session_for_id_with_lifetime(
286        &self,
287        user_id: crate::SchemaValue<String>,
288        ip_address: Option<String>,
289        user_agent: Option<String>,
290        expires_in: chrono::Duration,
291    ) -> AuthResult<SessionView> {
292        let expires_at = Utc::now() + expires_in;
293
294        let create_session = CreateSession {
295            additional_fields: Default::default(),
296            user_id,
297            expires_at: expires_at.into(),
298            ip_address,
299            user_agent,
300            impersonated_by: None,
301            active_organization_id: None,
302        };
303
304        let session = self.database.create_session(create_session).await?;
305        Ok(session)
306    }
307
308    /// Read a session directly from the server store and refresh its expiry.
309    pub async fn get_session(&self, token: &str) -> AuthResult<Option<crate::wire::SessionView>> {
310        let Some(session) = self.database.get_session(token).await? else {
311            return Ok(None);
312        };
313        if session.expires_at().milliseconds() < Utc::now().timestamp_millis() as f64
314            || !session.active()
315        {
316            self.database.delete_session(token).await?;
317            return Ok(None);
318        }
319        if self.needs_refresh(&session) {
320            let new_expires_at = Utc::now() + self.config.session.expires_in();
321            return self
322                .database
323                .update_session_expiry(token, new_expires_at)
324                .await
325                .map(Some);
326        }
327        Ok(Some(session))
328    }
329
330    fn needs_refresh(&self, session: &impl AuthSession) -> bool {
331        !self.config.session.disable_session_refresh()
332            && session.expires_at().milliseconds()
333                - self.config.session.expires_in().num_milliseconds() as f64
334                + self.config.session.update_age().num_milliseconds() as f64
335                <= Utc::now().timestamp_millis() as f64
336    }
337
338    /// Resolve an HTTP session and queue any cookie updates on the request.
339    pub async fn resolve(
340        &self,
341        req: &AuthRequest,
342        read: SessionRead,
343    ) -> AuthResult<SessionResolution> {
344        let resolved = self.resolve_relations(req, read, true).await?;
345        Ok(SessionResolution {
346            data: resolved
347                .data
348                .map(SessionData::into_typed)
349                .transpose()?
350                .flatten(),
351            needs_refresh: resolved.needs_refresh,
352        })
353    }
354
355    /// Resolve a session while preserving the User value selected by the adapter.
356    pub async fn resolve_native(
357        &self,
358        req: &AuthRequest,
359        read: SessionRead,
360    ) -> AuthResult<SessionResolution<NativeSessionData>> {
361        let resolved = self.resolve_relations(req, read, false).await?;
362        Ok(SessionResolution {
363            data: resolved.data.map(Into::into),
364            needs_refresh: resolved.needs_refresh,
365        })
366    }
367
368    async fn resolve_relations(
369        &self,
370        req: &AuthRequest,
371        read: SessionRead,
372        typed_user: bool,
373    ) -> AuthResult<SessionResolution<SessionData<JoinValue<UserView>>>> {
374        let raw = if req.path() == "/get-session" {
375            req.query.clone()
376        } else {
377            Some(
378                req.query
379                    .as_ref()
380                    .filter(|value| value.is_object())
381                    .cloned()
382                    .unwrap_or_else(|| serde_json::json!({})),
383            )
384        };
385        let query = crate::query::session_query(raw)?;
386        crate::query::with_validated_query(query, self.resolve_inner(req, read, typed_user)).await
387    }
388
389    async fn resolve_inner(
390        &self,
391        req: &AuthRequest,
392        read: SessionRead,
393        typed_user: bool,
394    ) -> AuthResult<SessionResolution<SessionData<JoinValue<UserView>>>> {
395        let none = || SessionResolution {
396            data: None,
397            needs_refresh: None,
398        };
399        if let Some(session) = req.virtual_session() {
400            let Some(user_id) = session.user_id.as_str() else {
401                return Ok(none());
402            };
403            let user = self
404                .database
405                .get_user_by_id(user_id)
406                .await?
407                .ok_or(AuthError::UserNotFound)?;
408            let data = SessionData {
409                session: session.clone(),
410                user: self.user_view(&user).await?,
411            };
412            req.set_session_snapshot(Some(data.clone().into()))?;
413            return Ok(SessionResolution {
414                data: Some(data.into()),
415                needs_refresh: None,
416            });
417        }
418        let cache = self
419            .config
420            .session
421            .cookie_cache
422            .as_ref()
423            .filter(|cache| cache.enabled());
424        let token = self.extract_session_token(req);
425        if token.is_none() && cache.is_some() {
426            return Ok(none());
427        }
428        let cache_value =
429            cookie_cache::read(req, &related_cookie_name(&self.config, "session_data"));
430        if cache.is_none() && cache_value.is_some() {
431            cookie_cache::clear_existing(req, &self.config)?;
432        }
433        let Some(token) = token else {
434            return Ok(none());
435        };
436        let disable_cache = (matches!(read, SessionRead::Authoritative)
437            && self.capabilities.server_sessions())
438            || query_flag(req, "disableCookieCache")?;
439        if !disable_cache && let (Some(cache), Some(value)) = (cache, cache_value.as_deref()) {
440            let decoded = if let Some(signer) = &self.signer {
441                signer
442                    .verify(
443                        value,
444                        SessionCookieContext {
445                            request: req,
446                            config: &self.config,
447                            transaction: None,
448                        },
449                    )
450                    .await?
451                    .and_then(cookie_cache::parse_jwt)
452            } else {
453                cookie_cache::decode(value, &self.config, cache)
454            };
455            if let Some((mut payload, expires)) = decoded
456                && payload.data.session.token == token
457                && (if payload.version.is_empty() {
458                    "1"
459                } else {
460                    &payload.version
461                }) == cache.version.resolve(&payload.data.clone().into()).await?
462                && expires >= Utc::now().timestamp_millis()
463                && payload.data.session.expires_at.milliseconds()
464                    >= Utc::now().timestamp_millis() as f64
465            {
466                if !self.capabilities.server_sessions()
467                    && let Some(update_age) = cache.refresh_age()
468                    && expires - Utc::now().timestamp_millis() < update_age.num_milliseconds()
469                {
470                    self.write_cache(req, &payload.data, false).await?;
471                    let max_age = (!self.dont_remember(req))
472                        .then_some(self.config.session.expires_in().as_seconds_f64());
473                    req.append_response_header(
474                        "Set-Cookie",
475                        crate::utils::cookie_utils::create_session_cookie_with_max_age(
476                            Some(&payload.data.session.token),
477                            max_age,
478                            &self.config,
479                        )?,
480                    )?;
481                }
482                let raw_snapshot =
483                    self.capabilities.server_sessions() || cache.refresh_age().is_none();
484                if raw_snapshot {
485                    req.set_session_snapshot(Some(payload.data.clone().into()))?;
486                }
487                payload.data.user.filter_cached_fields(&self.config.user);
488                payload
489                    .data
490                    .session
491                    .filter_returned_fields(&self.config.session);
492                if !raw_snapshot {
493                    req.set_session_snapshot(Some(payload.data.clone().into()))?;
494                }
495                return Ok(SessionResolution {
496                    data: Some(payload.data.into()),
497                    needs_refresh: None,
498                });
499            }
500            cookie_cache::expire(req, &self.config)?;
501        }
502        let is_post = req.path().ends_with("/get-session") && req.method() == &HttpMethod::Post;
503        let stored = self.database.get_session_snapshot(&token).await?;
504        let Some((session, cached_data)) = stored else {
505            req.set_session_snapshot(None)?;
506            self.clear_cookies(req)?;
507            return Ok(none());
508        };
509        let mut data = if let Some(mut data) = cached_data {
510            match &mut data.user {
511                JoinValue::One(Some(user)) => *user = self.user_view(user).await?,
512                JoinValue::One(None) => {
513                    req.set_session_snapshot(None)?;
514                    self.clear_cookies(req)?;
515                    return Ok(none());
516                }
517                JoinValue::Many(_) if typed_user => return Err(relationship_array_error()),
518                JoinValue::Many(_) => {}
519            }
520            data.session.filter_returned_fields(&self.config.session);
521            data
522        } else {
523            let Some(user_id) = session.user_id.as_str() else {
524                return Ok(none());
525            };
526            let Some(user) = self.database.get_user_by_id(user_id).await? else {
527                req.set_session_snapshot(None)?;
528                self.clear_cookies(req)?;
529                return Ok(none());
530            };
531            SessionData {
532                session: self.session_view(&session).await?,
533                user: JoinValue::One(Some(self.user_view(&user).await?)),
534            }
535        };
536        req.set_session_snapshot(Some(data.clone()))?;
537        if session.expires_at().milliseconds() < Utc::now().timestamp_millis() as f64
538            || !session.active()
539        {
540            self.clear_cookies(req)?;
541            if !self.config.session.defer_session_refresh || is_post {
542                self.database.delete_session(&token).await?;
543            }
544            return Ok(none());
545        }
546        let dont_remember = self.dont_remember(req);
547        if dont_remember || query_flag(req, "disableRefresh")? {
548            return Ok(SessionResolution {
549                data: Some(self.public_data(data)),
550                needs_refresh: None,
551            });
552        }
553        let needs_refresh = self.needs_refresh(&session);
554        if self.config.session.defer_session_refresh && !is_post {
555            self.write_cache_with_response(req, &data.clone().into(), false, None, None)
556                .await?;
557            return Ok(SessionResolution {
558                data: Some(self.public_data(data)),
559                needs_refresh: Some(needs_refresh),
560            });
561        }
562        if needs_refresh {
563            let updated = match self
564                .database
565                .update_session_expiry(&token, Utc::now() + self.config.session.expires_in())
566                .await
567            {
568                Err(AuthError::SessionNotFound) => {
569                    self.clear_cookies(req)?;
570                    return Err(failed_session_update());
571                }
572                result => result?,
573            };
574            data.session = self.internal_session_view(&updated).await?;
575            self.set_native_session_cookie(req, data.clone().into(), Some(false))
576                .await?;
577        } else {
578            self.write_cache_with_response(req, &data.clone().into(), false, None, None)
579                .await?;
580        }
581        Ok(SessionResolution {
582            data: Some(self.public_data(data)),
583            needs_refresh: None,
584        })
585    }
586
587    /// Write the configured cache from authenticated session data.
588    pub async fn write_cache(
589        &self,
590        req: &AuthRequest,
591        data: &SessionData,
592        dont_remember: bool,
593    ) -> AuthResult<()> {
594        self.write_cache_with_response(req, &data.clone().into(), dont_remember, None, None)
595            .await
596    }
597
598    async fn write_cache_with_response(
599        &self,
600        req: &AuthRequest,
601        data: &NativeSessionData,
602        dont_remember: bool,
603        response_headers: Option<&crate::Headers>,
604        transaction: Option<&dyn crate::store::AuthTransaction<S>>,
605    ) -> AuthResult<()> {
606        let signed = if let (Some(signer), Some(cache)) = (
607            &self.signer,
608            self.config
609                .session
610                .cookie_cache
611                .as_ref()
612                .filter(|cache| cache.enabled()),
613        ) {
614            let (payload, max_age) =
615                cookie_cache::payload(data, &self.config, cache, dont_remember).await?;
616            Some(
617                signer
618                    .sign(
619                        payload,
620                        max_age,
621                        SessionCookieContext {
622                            request: req,
623                            config: &self.config,
624                            transaction,
625                        },
626                    )
627                    .await?,
628            )
629        } else {
630            None
631        };
632        cookie_cache::write(
633            req,
634            data,
635            &self.config,
636            dont_remember,
637            signed,
638            self.capabilities.database,
639            response_headers,
640        )
641        .await
642    }
643
644    /// Read the signed marker for a browser-session-only login.
645    pub fn dont_remember(&self, req: &AuthRequest) -> bool {
646        get_cookie(req, &related_cookie_name(&self.config, "dont_remember"))
647            .and_then(|value| verify_cookie_value(&value, self.config.signing_secret()))
648            .is_some()
649    }
650
651    /// Expire session credentials and every cache chunk received on the request.
652    pub fn clear_cookies(&self, req: &AuthRequest) -> AuthResult<()> {
653        crate::utils::cookie_utils::delete_session_cookies(req, &self.config, false, None)
654    }
655
656    /// Delete a session
657    pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
658        self.database.delete_session(token).await?;
659        Ok(())
660    }
661
662    /// Delete all sessions for a user
663    pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
664        self.database.delete_user_sessions(user_id.as_ref()).await?;
665        Ok(())
666    }
667
668    /// Get all active sessions for a user
669    pub async fn list_user_sessions(
670        &self,
671        user_id: impl AsRef<str>,
672    ) -> AuthResult<Vec<crate::wire::SessionView>> {
673        let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
674        let now = Utc::now();
675
676        // Filter out expired sessions
677        let active_sessions = sessions
678            .into_iter()
679            .filter(|session| {
680                session.expires_at().milliseconds() > now.timestamp_millis() as f64
681                    && session.active()
682            })
683            .collect();
684
685        Ok(active_sessions)
686    }
687
688    /// Project active sessions while preserving cached undefined application fields.
689    pub async fn list_user_session_views(
690        &self,
691        user_id: impl AsRef<str>,
692    ) -> AuthResult<Vec<SessionView>> {
693        let snapshots = self
694            .database
695            .get_user_session_snapshots(user_id.as_ref())
696            .await?;
697        let mut views = Vec::new();
698        for (session, cached) in snapshots.into_iter().filter(|(session, _)| {
699            session.expires_at().milliseconds() > Utc::now().timestamp_millis() as f64
700                && session.active()
701        }) {
702            let view = if let Some(mut view) = cached {
703                view.filter_returned_fields(&self.config.session);
704                view
705            } else {
706                SessionView::with_fields_for_adapter(
707                    &session,
708                    &self.config.session,
709                    self.database.supports_native_json(),
710                )
711                .await?
712            };
713            views.push(view);
714        }
715        Ok(views)
716    }
717
718    /// Revoke a specific session by token
719    pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
720        // Check if session exists before trying to delete
721        let session_exists = self.get_session(token).await?.is_some();
722
723        if session_exists {
724            self.delete_session(token).await?;
725            Ok(true)
726        } else {
727            Ok(false)
728        }
729    }
730
731    /// Revoke all sessions for a user
732    pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
733        // Get count of sessions before deletion for return value
734        let user_id = user_id.as_ref();
735        let sessions = self.list_user_sessions(user_id).await?;
736        let count = sessions.len();
737
738        self.delete_user_sessions(user_id).await?;
739        Ok(count)
740    }
741
742    /// Revoke all sessions for a user except the current one
743    pub async fn revoke_other_user_sessions(
744        &self,
745        user_id: impl AsRef<str>,
746        current_token: &str,
747    ) -> AuthResult<usize> {
748        let sessions = self.list_user_sessions(user_id).await?;
749        let mut count = 0;
750
751        for session in sessions {
752            if session.token() != current_token {
753                self.delete_session(session.token()).await?;
754                count += 1;
755            }
756        }
757
758        Ok(count)
759    }
760
761    /// Cleanup expired sessions
762    pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
763        let count = self.database.delete_expired_sessions().await?;
764        Ok(count)
765    }
766
767    /// Check whether a session is "fresh" (created recently enough for
768    /// sensitive operations like password change or account deletion).
769    ///
770    /// Returns `true` when `fresh_age` is set and
771    /// `session.created_at() + fresh_age > now`.
772    /// If `fresh_age` is `None`, the session is never considered fresh.
773    pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
774        match self.config.session.fresh_age {
775            Some(fresh_age) => {
776                session.created_at().milliseconds() + fresh_age.num_milliseconds() as f64
777                    > Utc::now().timestamp_millis() as f64
778            }
779            None => false,
780        }
781    }
782
783    /// Validate session token format
784    pub fn validate_token_format(&self, token: &str) -> bool {
785        token.starts_with("session_") && token.len() > 40
786    }
787
788    /// Extract and verify a session cookie or an explicitly enabled Bearer token.
789    pub fn extract_session_token(&self, req: &AuthRequest) -> Option<String> {
790        if let Some(bearer) = &self.config.session.bearer
791            && let Some(header) = req.headers.get("authorization")
792            && header
793                .get(..7)
794                .is_some_and(|scheme| scheme.eq_ignore_ascii_case("bearer "))
795        {
796            let token = header.get(7..)?.trim();
797            if !token.is_empty() {
798                if token.contains('.') {
799                    if let Some(value) = verify_cookie_value(token, self.config.signing_secret()) {
800                        return Some(value);
801                    }
802                } else if !bearer.require_signature {
803                    return Some(token.to_string());
804                }
805            }
806        }
807        get_cookie(
808            req,
809            &self
810                .config
811                .auth_cookie("session_token", Default::default())
812                .name,
813        )
814        .and_then(|value| verify_cookie_value(&value, self.config.signing_secret()))
815    }
816}
817
818fn query_flag(req: &AuthRequest, name: &str) -> AuthResult<bool> {
819    req.query
820        .as_ref()
821        .and_then(|query| query.get(name))
822        .map(|value| crate::FieldValue::from_json(value.clone()).map(|value| value.is_truthy()))
823        .transpose()
824        .map(|value| value.unwrap_or(false))
825}
826
827fn relationship_array_error() -> AuthError {
828    AuthError::internal("A User relationship array cannot authenticate a typed User")
829}
830
831fn failed_session_update() -> AuthError {
832    AuthError::Upstream {
833        status: 401,
834        code: "FAILED_TO_GET_SESSION",
835        message: "Failed to get session",
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842    use crate::entity::AuthSession;
843    use crate::test_store::{BundledSchema, test_config, test_database};
844    use crate::types::AuthRequest;
845    use crate::types::HttpMethod;
846    use crate::utils::cookie_utils::sign_cookie_value;
847    use crate::wire::SessionView;
848    use chrono::Duration;
849
850    fn test_manager() -> SessionManager<BundledSchema> {
851        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
852        SessionManager::new(test_config(), runtime.block_on(test_database()))
853    }
854
855    // ── validate_token_format ───────────────────────────────────────────
856
857    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
858    #[test]
859    fn valid_token_format() {
860        let mgr = test_manager();
861        let token = "session_abcdefghijklmnopqrstuvwxyz1234567890";
862        assert!(mgr.validate_token_format(token));
863    }
864
865    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
866    #[test]
867    fn invalid_token_no_prefix() {
868        let mgr = test_manager();
869        assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxyz1234567890"));
870    }
871
872    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
873    #[test]
874    fn invalid_token_too_short() {
875        let mgr = test_manager();
876        assert!(!mgr.validate_token_format("session_short"));
877    }
878
879    // ── extract_session_token ───────────────────────────────────────────
880
881    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
882    #[test]
883    fn extract_from_bearer() {
884        let mgr = test_manager();
885        let mut req = AuthRequest::new(HttpMethod::Get, "/test");
886        let _ = req
887            .headers
888            .insert("authorization".into(), "Bearer my-token".into());
889        assert_eq!(mgr.extract_session_token(&req), Some("my-token".into()));
890    }
891
892    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
893    #[test]
894    fn extract_from_cookie() {
895        let mgr = test_manager();
896        let mut req = AuthRequest::new(HttpMethod::Get, "/test");
897        let _ = req.headers.insert(
898            "cookie".into(),
899            format!(
900                "better-auth.session_token={}; other=val",
901                sign_cookie_value("tok123", &mgr.config.secret)
902            ),
903        );
904        assert_eq!(mgr.extract_session_token(&req), Some("tok123".into()));
905    }
906
907    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
908    #[test]
909    fn extract_bearer_takes_precedence_over_cookie() {
910        let mgr = test_manager();
911        let mut req = AuthRequest::new(HttpMethod::Get, "/test");
912        let _ = req
913            .headers
914            .insert("authorization".into(), "Bearer bearer-tok".into());
915        let _ = req.headers.insert(
916            "cookie".into(),
917            format!(
918                "better-auth.session_token={}",
919                sign_cookie_value("cookie-tok", &mgr.config.secret)
920            ),
921        );
922        assert_eq!(mgr.extract_session_token(&req), Some("bearer-tok".into()));
923    }
924
925    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
926    #[test]
927    fn extract_returns_none_without_auth() {
928        let mgr = test_manager();
929        let req = AuthRequest::new(HttpMethod::Get, "/test");
930        assert_eq!(mgr.extract_session_token(&req), None);
931    }
932
933    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
934    #[test]
935    fn extract_skips_empty_cookie_value() {
936        let mgr = test_manager();
937        let mut req = AuthRequest::new(HttpMethod::Get, "/test");
938        let _ = req
939            .headers
940            .insert("cookie".into(), "better-auth.session_token=".into());
941        assert_eq!(mgr.extract_session_token(&req), None);
942    }
943
944    // ── is_session_fresh ────────────────────────────────────────────────
945
946    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
947    #[test]
948    fn session_fresh_when_within_window() {
949        let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
950        config.session.fresh_age = Some(Duration::minutes(10));
951        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
952        let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
953
954        // A session created "now" is fresh within a 10-minute window.
955        let session = SessionView {
956            visible_fields: None,
957            id: "s1".into(),
958            expires_at: (Utc::now() + Duration::hours(1)).into(),
959            token: "tok".into(),
960            created_at: Utc::now().into(),
961            updated_at: Utc::now().into(),
962            ip_address: None,
963            user_agent: None,
964            user_id: "u1".into(),
965            impersonated_by: None,
966            active_organization_id: None,
967            active_team_id: None,
968            active: true,
969            additional_fields: Default::default(),
970        };
971        assert!(mgr.is_session_fresh(&session));
972    }
973
974    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
975    #[test]
976    fn session_not_fresh_when_old() {
977        let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
978        config.session.fresh_age = Some(Duration::minutes(10));
979        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
980        let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
981
982        let session = SessionView {
983            visible_fields: None,
984            id: "s1".into(),
985            expires_at: (Utc::now() + Duration::hours(1)).into(),
986            token: "tok".into(),
987            created_at: (Utc::now() - Duration::minutes(20)).into(),
988            updated_at: Utc::now().into(),
989            ip_address: None,
990            user_agent: None,
991            user_id: "u1".into(),
992            impersonated_by: None,
993            active_organization_id: None,
994            active_team_id: None,
995            active: true,
996            additional_fields: Default::default(),
997        };
998        assert!(!mgr.is_session_fresh(&session));
999    }
1000
1001    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1002    #[test]
1003    fn session_never_fresh_when_no_fresh_age() {
1004        let mgr = test_manager(); // default: fresh_age = None
1005        let session = SessionView {
1006            visible_fields: None,
1007            id: "s1".into(),
1008            expires_at: (Utc::now() + Duration::hours(1)).into(),
1009            token: "tok".into(),
1010            created_at: Utc::now().into(),
1011            updated_at: Utc::now().into(),
1012            ip_address: None,
1013            user_agent: None,
1014            user_id: "u1".into(),
1015            impersonated_by: None,
1016            active_organization_id: None,
1017            active_team_id: None,
1018            active: true,
1019            additional_fields: Default::default(),
1020        };
1021        assert!(!mgr.is_session_fresh(&session));
1022    }
1023
1024    // ── async operations ────────────────────────────────────────────────
1025
1026    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1027    #[tokio::test]
1028    async fn create_and_get_session() {
1029        let db = test_database().await;
1030        let mgr = SessionManager::new(test_config(), db.clone());
1031
1032        // Create a user first
1033        let user = db
1034            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1035            .await
1036            .unwrap();
1037
1038        let session = mgr.create_session(&user, None, None).await.unwrap();
1039        let token = session.token().to_string();
1040
1041        let retrieved = mgr.get_session(&token).await.unwrap();
1042        assert!(retrieved.is_some());
1043    }
1044
1045    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1046    #[tokio::test]
1047    async fn refresh_returns_the_persisted_expiry() {
1048        let db = test_database().await;
1049        let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
1050        // Refresh on every access so a single `get_session` exercises the path.
1051        config.session.update_age = Some(Duration::zero());
1052        let mgr = SessionManager::new(Arc::new(config), db.clone());
1053
1054        let user = db
1055            .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
1056            .await
1057            .unwrap();
1058        let session = mgr.create_session(&user, None, None).await.unwrap();
1059        let token = session.token().to_string();
1060
1061        // Move the stored expiry back so the refresh is observable.
1062        let stale = session.expires_at().to_datetime().unwrap().unwrap() - Duration::minutes(30);
1063        let _ = db.update_session_expiry(&token, stale).await.unwrap();
1064
1065        let returned = mgr
1066            .get_session(&token)
1067            .await
1068            .unwrap()
1069            .expect("session should still be live");
1070        let stored = db
1071            .get_session(&token)
1072            .await
1073            .unwrap()
1074            .expect("session should still be stored");
1075
1076        assert!(
1077            returned.expires_at().milliseconds() > stale.timestamp_millis() as f64,
1078            "refresh should have extended the expiry"
1079        );
1080        assert_eq!(
1081            returned.expires_at(),
1082            stored.expires_at(),
1083            "returned session must reflect the persisted expiry, not the pre-refresh value"
1084        );
1085    }
1086
1087    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1088    #[tokio::test]
1089    async fn create_session_without_metadata_uses_empty_strings() {
1090        let db = test_database().await;
1091        let mgr = SessionManager::new(test_config(), db.clone());
1092
1093        let user = db
1094            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1095            .await
1096            .unwrap();
1097
1098        let session = mgr.create_session(&user, None, None).await.unwrap();
1099        assert_eq!(session.ip_address.as_deref(), Some(""));
1100        assert_eq!(session.user_agent.as_deref(), Some(""));
1101    }
1102
1103    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1104    #[tokio::test]
1105    async fn delete_session_removes_it() {
1106        let db = test_database().await;
1107        let mgr = SessionManager::new(test_config(), db.clone());
1108
1109        let user = db
1110            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1111            .await
1112            .unwrap();
1113
1114        let session = mgr.create_session(&user, None, None).await.unwrap();
1115        let token = session.token().to_string();
1116
1117        mgr.delete_session(&token).await.unwrap();
1118        let retrieved = mgr.get_session(&token).await.unwrap();
1119        assert!(retrieved.is_none());
1120    }
1121
1122    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1123    #[tokio::test]
1124    async fn revoke_session_returns_true_when_found() {
1125        let db = test_database().await;
1126        let mgr = SessionManager::new(test_config(), db.clone());
1127
1128        let user = db
1129            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1130            .await
1131            .unwrap();
1132
1133        let session = mgr.create_session(&user, None, None).await.unwrap();
1134        let result = mgr.revoke_session(session.token()).await.unwrap();
1135        assert!(result);
1136    }
1137
1138    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1139    #[tokio::test]
1140    async fn revoke_session_returns_false_when_not_found() {
1141        let mgr = SessionManager::new(test_config(), test_database().await);
1142        let result = mgr.revoke_session("nonexistent-token").await.unwrap();
1143        assert!(!result);
1144    }
1145
1146    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1147    #[tokio::test]
1148    async fn list_user_sessions_excludes_expired() {
1149        let db = test_database().await;
1150        let mgr = SessionManager::new(test_config(), db.clone());
1151
1152        let user = db
1153            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1154            .await
1155            .unwrap();
1156
1157        // Create two sessions
1158        let _ = mgr.create_session(&user, None, None).await.unwrap();
1159        let _ = mgr.create_session(&user, None, None).await.unwrap();
1160
1161        let sessions = mgr
1162            .list_user_sessions(user.id().typed().unwrap())
1163            .await
1164            .unwrap();
1165        assert_eq!(sessions.len(), 2);
1166    }
1167
1168    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1169    #[tokio::test]
1170    async fn revoke_all_user_sessions() {
1171        let db = test_database().await;
1172        let mgr = SessionManager::new(test_config(), db.clone());
1173
1174        let user = db
1175            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1176            .await
1177            .unwrap();
1178
1179        let _ = mgr.create_session(&user, None, None).await.unwrap();
1180        let _ = mgr.create_session(&user, None, None).await.unwrap();
1181
1182        let count = mgr
1183            .revoke_all_user_sessions(user.id().typed().unwrap())
1184            .await
1185            .unwrap();
1186        assert_eq!(count, 2);
1187
1188        let sessions = mgr
1189            .list_user_sessions(user.id().typed().unwrap())
1190            .await
1191            .unwrap();
1192        assert!(sessions.is_empty());
1193    }
1194
1195    // Rust-specific surface: `SessionManager` and its token/session helper APIs are public Rust APIs with no direct TS analogue.
1196    #[tokio::test]
1197    async fn revoke_other_sessions_keeps_current() {
1198        let db = test_database().await;
1199        let mgr = SessionManager::new(test_config(), db.clone());
1200
1201        let user = db
1202            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
1203            .await
1204            .unwrap();
1205
1206        let current = mgr.create_session(&user, None, None).await.unwrap();
1207        let _ = mgr.create_session(&user, None, None).await.unwrap();
1208        let _ = mgr.create_session(&user, None, None).await.unwrap();
1209
1210        let count = mgr
1211            .revoke_other_user_sessions(user.id().typed().unwrap(), current.token())
1212            .await
1213            .unwrap();
1214        assert_eq!(count, 2);
1215
1216        let remaining = mgr
1217            .list_user_sessions(user.id().typed().unwrap())
1218            .await
1219            .unwrap();
1220        assert_eq!(remaining.len(), 1);
1221        assert_eq!(remaining[0].token(), current.token());
1222    }
1223}