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}