Skip to main content

alibi_core/config/
session.rs

1use super::*;
2/// Session-specific configuration
3#[derive(Clone)]
4#[expect(
5    clippy::struct_excessive_bools,
6    reason = "Independent configuration switches model distinct upstream behavior, rather than mutually exclusive states"
7)]
8pub struct SessionConfig {
9    /// No durable server session authority. Session records are instance-local
10    /// in memory; no SQL session rows are read or written. Captured caches remain
11    /// replayable until their embedded expiry or cache version/secret invalidation.
12    pub stateless: bool,
13    /// Stateless envelope renewal. Stateful deployments ignore this policy.
14    pub cookie_refresh_cache: CookieRefreshCache,
15    /// Shared secondary session backend for stateful deployments.
16    pub secondary_storage: Option<Arc<dyn crate::store::CacheAdapter>>,
17    /// Also persist session rows when a secondary backend is configured.
18    pub store_in_database: bool,
19    /// Retain ended database sessions for auditing; cache misses never fall back to them.
20    pub preserve_in_database: bool,
21    /// Additional fields accepted by session input and output policies.
22    pub additional_fields: indexmap::IndexMap<String, crate::field_policy::FieldConfig>,
23    /// Session expiration duration
24    pub expires_in: Duration,
25
26    /// How often to refresh the session expiry (as a Duration).
27    ///
28    /// A read refreshes when `expires_at - expires_in + update_age` is due.
29    /// This uses the stored expiry, including application overrides, rather
30    /// than the row's last update timestamp. `None` refreshes on every read
31    /// unless refresh is disabled or deferred.
32    pub update_age: Option<Duration>,
33
34    /// If `true`, sessions are never automatically refreshed on access.
35    pub disable_session_refresh: bool,
36
37    /// Defer refresh and expired-row deletion during `GET` session reads.
38    /// `POST /get-session` performs these writes when enabled; otherwise that
39    /// method is rejected with 405. Expired browser cookies are still cleared.
40    pub defer_session_refresh: bool,
41
42    /// Session freshness window, defaulting to one day. `None` or zero skips
43    /// the freshness restriction; a positive window checks creation time.
44    pub fresh_age: Option<Duration>,
45
46    /// Cookie name for session token
47    pub cookie_name: String,
48
49    /// Cookie settings
50    pub cookie_secure: bool,
51    pub cookie_http_only: bool,
52    pub cookie_same_site: SameSite,
53
54    /// Optional cookie-based session cache to avoid DB lookups.
55    ///
56    /// When enabled, session data is cached in a signed/encrypted cookie.
57    /// `SessionManager` checks the cookie cache before hitting the database.
58    pub cookie_cache: Option<CookieCacheConfig>,
59}
60
61impl std::fmt::Debug for SessionConfig {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("SessionConfig")
64            .field("secondary_storage", &self.secondary_storage.is_some())
65            .field("store_in_database", &self.store_in_database)
66            .field("preserve_in_database", &self.preserve_in_database)
67            .field("expires_in", &self.expires_in)
68            .finish_non_exhaustive()
69    }
70}
71
72/// JWT configuration
73#[derive(Debug, Clone)]
74pub struct JwtConfig {
75    /// JWT expiration duration
76    pub expires_in: Duration,
77
78    /// JWT algorithm
79    pub algorithm: String,
80
81    /// Issuer claim
82    pub issuer: Option<String>,
83
84    /// Audience claim
85    pub audience: Option<String>,
86}
87
88/// Configuration for cookie-based session caching.
89///
90/// When enabled, session data is stored in a signed or encrypted cookie so that
91/// subsequent requests can skip the database lookup.
92#[derive(Debug, Clone)]
93pub struct CookieCacheConfig {
94    /// Whether the cookie cache is active.
95    pub enabled: bool,
96
97    /// Maximum age in seconds before a fresh DB lookup is required.
98    ///
99    /// JavaScript falsy zero/NaN selects 300; other IEEE754 values are retained.
100    ///
101    /// Default: 5 minutes.
102    pub max_age: f64,
103
104    /// Strategy used to protect the cached cookie value.
105    ///
106    /// JWT uses the auth secret unless a locally managed JWT plugin signer is installed.
107    pub strategy: CookieCacheStrategy,
108
109    /// Literal or asynchronous application-owned version policy.
110    pub version: Option<crate::session::cookie_cache::CookieCacheVersion>,
111}
112
113/// Stateless cache renewal policy, corresponding to `cookieCache.refreshCache`.
114#[derive(Debug, Clone, Copy, Default)]
115pub enum CookieRefreshCache {
116    /// No cache renewal.
117    #[default]
118    Disabled,
119    /// Renew when remaining envelope lifetime is below floor(maxAge * 0.2).
120    Automatic,
121    /// Renew below this remaining lifetime in seconds (JavaScript Number).
122    UpdateAge(f64),
123}
124
125impl SessionConfig {
126    /// Select cookie-only sessions, installing the pinned no-store defaults.
127    /// Configure `cookie_cache` afterwards to override strategy, lifetime,
128    /// version, or renewal. Existing database defaults are unchanged.
129    #[must_use]
130    pub fn stateless(mut self) -> Self {
131        if !self.stateless {
132            self.cookie_refresh_cache = CookieRefreshCache::Automatic;
133        }
134        self.stateless = true;
135        if self.cookie_cache.is_none() {
136            self.cookie_cache = Some(CookieCacheConfig {
137                enabled: true,
138                strategy: CookieCacheStrategy::Jwe,
139                max_age: self.expires_in.num_seconds() as f64,
140                ..CookieCacheConfig::default()
141            });
142        }
143        self
144    }
145
146    /// Whether deployment has durable server session storage.
147    #[must_use]
148    pub fn has_server_session_store(&self) -> bool {
149        !self.stateless
150    }
151}
152
153/// Strategy for signing / encrypting the cookie cache.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum CookieCacheStrategy {
156    /// Base64url-encoded payload + HMAC-SHA256 signature.
157    Compact,
158    /// Standard JWT with HMAC signing.
159    Jwt,
160    /// JWE with direct-key AES-256-CBC/HMAC-SHA512 authenticated encryption.
161    Jwe,
162}
163
164impl Default for CookieCacheConfig {
165    fn default() -> Self {
166        Self {
167            enabled: false,
168            max_age: 300.0,
169            strategy: CookieCacheStrategy::Compact,
170            version: None,
171        }
172    }
173}
174
175impl Default for SessionConfig {
176    fn default() -> Self {
177        Self {
178            stateless: false,
179            cookie_refresh_cache: CookieRefreshCache::Disabled,
180            secondary_storage: None,
181            store_in_database: false,
182            preserve_in_database: false,
183            additional_fields: indexmap::IndexMap::default(),
184            expires_in: Duration::hours(24 * 7),   // 7 days
185            update_age: Some(Duration::hours(24)), // refresh once per day
186            disable_session_refresh: false,
187            defer_session_refresh: false,
188            fresh_age: Some(Duration::days(1)),
189            cookie_name: "better-auth.session_token".to_owned(),
190            // Secure flag is derived from base_url scheme (HTTPS → true).
191            // Default base_url is http://localhost:3000, so default is false.
192            cookie_secure: false,
193            cookie_http_only: true,
194            cookie_same_site: SameSite::Lax,
195            cookie_cache: None,
196        }
197    }
198}
199
200impl Default for JwtConfig {
201    fn default() -> Self {
202        Self {
203            expires_in: Duration::hours(24), // 1 day
204            algorithm: "HS256".to_owned(),
205            issuer: None,
206            audience: None,
207        }
208    }
209}