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