actix_session/
config.rs

1//! Configuration options to tune the behaviour of [`SessionMiddleware`].
2
3use actix_web::cookie::{time::Duration, Key, SameSite};
4use derive_more::derive::From;
5
6use crate::{storage::SessionStore, SessionMiddleware};
7
8/// Determines what type of session cookie should be used and how its lifecycle should be managed.
9///
10/// Used by [`SessionMiddlewareBuilder::session_lifecycle`].
11#[derive(Debug, Clone, From)]
12#[non_exhaustive]
13pub enum SessionLifecycle {
14    /// The session cookie will expire when the current browser session ends.
15    ///
16    /// When does a browser session end? It depends on the browser! Chrome, for example, will often
17    /// continue running in the background when the browser is closed—session cookies are not
18    /// deleted and they will still be available when the browser is opened again.
19    /// Check the documentation of the browsers you are targeting for up-to-date information.
20    BrowserSession(BrowserSession),
21
22    /// The session cookie will be a [persistent cookie].
23    ///
24    /// Persistent cookies have a pre-determined lifetime, specified via the `Max-Age` or `Expires`
25    /// attribute. They do not disappear when the current browser session ends.
26    ///
27    /// [persistent cookie]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
28    PersistentSession(PersistentSession),
29}
30
31/// A [session lifecycle](SessionLifecycle) strategy where the session cookie expires when the
32/// browser's current session ends.
33///
34/// When does a browser session end? It depends on the browser. Chrome, for example, will often
35/// continue running in the background when the browser is closed—session cookies are not deleted
36/// and they will still be available when the browser is opened again. Check the documentation of
37/// the browsers you are targeting for up-to-date information.
38///
39/// Due to its `Into<SessionLifecycle>` implementation, a `BrowserSession` can be passed directly
40/// to [`SessionMiddlewareBuilder::session_lifecycle()`].
41#[derive(Debug, Clone)]
42pub struct BrowserSession {
43    state_ttl: Duration,
44    state_ttl_extension_policy: TtlExtensionPolicy,
45}
46
47impl BrowserSession {
48    /// Sets a time-to-live (TTL) when storing the session state in the storage backend.
49    ///
50    /// We do not want to store session states indefinitely, otherwise we will inevitably run out of
51    /// storage by holding on to the state of countless abandoned or expired sessions!
52    ///
53    /// We are dealing with the lifecycle of two uncorrelated object here: the session cookie
54    /// and the session state. It is not a big issue if the session state outlives the cookie—
55    /// we are wasting some space in the backend storage, but it will be cleaned up eventually.
56    /// What happens, instead, if the cookie outlives the session state? A new session starts—
57    /// e.g. if sessions are being used for authentication, the user is de-facto logged out.
58    ///
59    /// It is not possible to predict with certainty how long a browser session is going to
60    /// last—you need to provide a reasonable upper bound. You do so via `state_ttl`—it dictates
61    /// what TTL should be used for session state when the lifecycle of the session cookie is
62    /// tied to the browser session length. [`SessionMiddleware`] will default to 1 day if
63    /// `state_ttl` is left unspecified.
64    ///
65    /// You can mitigate the risk of the session cookie outliving the session state by
66    /// specifying a more aggressive state TTL extension policy - check out
67    /// [`BrowserSession::state_ttl_extension_policy`] for more details.
68    pub fn state_ttl(mut self, ttl: Duration) -> Self {
69        self.state_ttl = ttl;
70        self
71    }
72
73    /// Determine under what circumstances the TTL of your session state should be extended.
74    ///
75    /// Defaults to [`TtlExtensionPolicy::OnStateChanges`] if left unspecified.
76    ///
77    /// See [`TtlExtensionPolicy`] for more details.
78    pub fn state_ttl_extension_policy(mut self, ttl_extension_policy: TtlExtensionPolicy) -> Self {
79        self.state_ttl_extension_policy = ttl_extension_policy;
80        self
81    }
82}
83
84impl Default for BrowserSession {
85    fn default() -> Self {
86        Self {
87            state_ttl: default_ttl(),
88            state_ttl_extension_policy: default_ttl_extension_policy(),
89        }
90    }
91}
92
93/// A [session lifecycle](SessionLifecycle) strategy where the session cookie will be [persistent].
94///
95/// Persistent cookies have a pre-determined expiration, specified via the `Max-Age` or `Expires`
96/// attribute. They do not disappear when the current browser session ends.
97///
98/// Due to its `Into<SessionLifecycle>` implementation, a `PersistentSession` can be passed directly
99/// to [`SessionMiddlewareBuilder::session_lifecycle()`].
100///
101/// # Examples
102/// ```
103/// use actix_web::cookie::time::Duration;
104/// use actix_session::SessionMiddleware;
105/// use actix_session::config::{PersistentSession, TtlExtensionPolicy};
106///
107/// const SECS_IN_WEEK: i64 = 60 * 60 * 24 * 7;
108///
109/// // a session lifecycle with a time-to-live (expiry) of 1 week and default extension policy
110/// PersistentSession::default().session_ttl(Duration::seconds(SECS_IN_WEEK));
111///
112/// // a session lifecycle with the default time-to-live (expiry) and a custom extension policy
113/// PersistentSession::default()
114///     // this policy causes the session state's TTL to be refreshed on every request
115///     .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest);
116/// ```
117///
118/// [persistent]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
119#[derive(Debug, Clone)]
120pub struct PersistentSession {
121    session_ttl: Duration,
122    ttl_extension_policy: TtlExtensionPolicy,
123}
124
125impl PersistentSession {
126    /// Specifies how long the session cookie should live.
127    ///
128    /// The session TTL is also used as the TTL for the session state in the storage backend.
129    ///
130    /// Defaults to 1 day.
131    ///
132    /// A persistent session can live more than the specified TTL if the TTL is extended.
133    /// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
134    #[doc(alias = "max_age", alias = "max age", alias = "expires")]
135    pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
136        self.session_ttl = session_ttl;
137        self
138    }
139
140    /// Determines under what circumstances the TTL of your session should be extended.
141    /// See [`TtlExtensionPolicy`] for more details.
142    ///
143    /// Defaults to [`TtlExtensionPolicy::OnStateChanges`].
144    pub fn session_ttl_extension_policy(
145        mut self,
146        ttl_extension_policy: TtlExtensionPolicy,
147    ) -> Self {
148        self.ttl_extension_policy = ttl_extension_policy;
149        self
150    }
151}
152
153impl Default for PersistentSession {
154    fn default() -> Self {
155        Self {
156            session_ttl: default_ttl(),
157            ttl_extension_policy: default_ttl_extension_policy(),
158        }
159    }
160}
161
162/// Configuration for which events should trigger an extension of the time-to-live for your session.
163///
164/// If you are using a [`BrowserSession`], `TtlExtensionPolicy` controls how often the TTL of the
165/// session state should be refreshed. The browser is in control of the lifecycle of the session
166/// cookie.
167///
168/// If you are using a [`PersistentSession`], `TtlExtensionPolicy` controls both the expiration of
169/// the session cookie and the TTL of the session state on the storage backend.
170#[derive(Debug, Clone)]
171#[non_exhaustive]
172pub enum TtlExtensionPolicy {
173    /// The TTL is refreshed every time the server receives a request associated with a session.
174    ///
175    /// # Performance impact
176    /// Refreshing the TTL on every request is not free. It implies a refresh of the TTL on the
177    /// session state. This translates into a request over the network if you are using a remote
178    /// system as storage backend (e.g. Redis). This impacts both the total load on your storage
179    /// backend (i.e. number of queries it has to handle) and the latency of the requests served by
180    /// your server.
181    OnEveryRequest,
182
183    /// The TTL is refreshed every time the session state changes or the session key is renewed.
184    OnStateChanges,
185}
186
187/// Determines how to secure the content of the session cookie.
188///
189/// Used by [`SessionMiddlewareBuilder::cookie_content_security`].
190#[derive(Debug, Clone, Copy)]
191pub enum CookieContentSecurity {
192    /// The cookie content is encrypted when using `CookieContentSecurity::Private`.
193    ///
194    /// Encryption guarantees confidentiality and integrity: the client cannot tamper with the
195    /// cookie content nor decode it, as long as the encryption key remains confidential.
196    Private,
197
198    /// The cookie content is signed when using `CookieContentSecurity::Signed`.
199    ///
200    /// Signing guarantees integrity, but it doesn't ensure confidentiality: the client cannot
201    /// tamper with the cookie content, but they can read it.
202    Signed,
203}
204
205pub(crate) const fn default_ttl() -> Duration {
206    Duration::days(1)
207}
208
209pub(crate) const fn default_ttl_extension_policy() -> TtlExtensionPolicy {
210    TtlExtensionPolicy::OnStateChanges
211}
212
213/// A fluent, customized [`SessionMiddleware`] builder.
214#[must_use]
215pub struct SessionMiddlewareBuilder<Store: SessionStore> {
216    storage_backend: Store,
217    configuration: Configuration,
218}
219
220impl<Store: SessionStore> SessionMiddlewareBuilder<Store> {
221    pub(crate) fn new(store: Store, configuration: Configuration) -> Self {
222        Self {
223            storage_backend: store,
224            configuration,
225        }
226    }
227
228    /// Set the name of the cookie used to store the session ID.
229    ///
230    /// Defaults to `id`.
231    pub fn cookie_name(mut self, name: String) -> Self {
232        self.configuration.cookie.name = name;
233        self
234    }
235
236    /// Set the `Secure` attribute for the cookie used to store the session ID.
237    ///
238    /// If the cookie is set as secure, it will only be transmitted when the connection is secure
239    /// (using `https`).
240    ///
241    /// Default is `true`.
242    pub fn cookie_secure(mut self, secure: bool) -> Self {
243        self.configuration.cookie.secure = secure;
244        self
245    }
246
247    /// Determines what type of session cookie should be used and how its lifecycle should be managed.
248    /// Check out [`SessionLifecycle`]'s documentation for more details on the available options.
249    ///
250    /// Default is [`SessionLifecycle::BrowserSession`].
251    ///
252    /// # Examples
253    /// ```
254    /// use actix_web::cookie::{Key, time::Duration};
255    /// use actix_session::{SessionMiddleware, config::PersistentSession};
256    /// use actix_session::storage::CookieSessionStore;
257    ///
258    /// const SECS_IN_WEEK: i64 = 60 * 60 * 24 * 7;
259    ///
260    /// // creates a session middleware with a time-to-live (expiry) of 1 week
261    /// SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64]))
262    ///     .session_lifecycle(
263    ///         PersistentSession::default().session_ttl(Duration::seconds(SECS_IN_WEEK))
264    ///     )
265    ///     .build();
266    /// ```
267    pub fn session_lifecycle<S: Into<SessionLifecycle>>(mut self, session_lifecycle: S) -> Self {
268        match session_lifecycle.into() {
269            SessionLifecycle::BrowserSession(BrowserSession {
270                state_ttl,
271                state_ttl_extension_policy,
272            }) => {
273                self.configuration.cookie.max_age = None;
274                self.configuration.session.state_ttl = state_ttl;
275                self.configuration.ttl_extension_policy = state_ttl_extension_policy;
276            }
277            SessionLifecycle::PersistentSession(PersistentSession {
278                session_ttl,
279                ttl_extension_policy,
280            }) => {
281                self.configuration.cookie.max_age = Some(session_ttl);
282                self.configuration.session.state_ttl = session_ttl;
283                self.configuration.ttl_extension_policy = ttl_extension_policy;
284            }
285        }
286
287        self
288    }
289
290    /// Set the `SameSite` attribute for the cookie used to store the session ID.
291    ///
292    /// By default, the attribute is set to `Lax`.
293    pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
294        self.configuration.cookie.same_site = same_site;
295        self
296    }
297
298    /// Set the `Path` attribute for the cookie used to store the session ID.
299    ///
300    /// By default, the attribute is set to `/`.
301    pub fn cookie_path(mut self, path: String) -> Self {
302        self.configuration.cookie.path = path;
303        self
304    }
305
306    /// Set the `Domain` attribute for the cookie used to store the session ID.
307    ///
308    /// Use `None` to leave the attribute unspecified. If unspecified, the attribute defaults
309    /// to the same host that set the cookie, excluding subdomains.
310    ///
311    /// By default, the attribute is left unspecified.
312    pub fn cookie_domain(mut self, domain: Option<String>) -> Self {
313        self.configuration.cookie.domain = domain;
314        self
315    }
316
317    /// Choose how the session cookie content should be secured.
318    ///
319    /// - [`CookieContentSecurity::Private`] selects encrypted cookie content.
320    /// - [`CookieContentSecurity::Signed`] selects signed cookie content.
321    ///
322    /// # Default
323    /// By default, the cookie content is encrypted. Encrypted was chosen instead of signed as
324    /// default because it reduces the chances of sensitive information being exposed in the session
325    /// key by accident, regardless of [`SessionStore`] implementation you chose to use.
326    ///
327    /// For example, if you are using cookie-based storage, you definitely want the cookie content
328    /// to be encrypted—the whole session state is embedded in the cookie! If you are using
329    /// Redis-based storage, signed is more than enough - the cookie content is just a unique
330    /// tamper-proof session key.
331    pub fn cookie_content_security(mut self, content_security: CookieContentSecurity) -> Self {
332        self.configuration.cookie.content_security = content_security;
333        self
334    }
335
336    /// Set the `HttpOnly` attribute for the cookie used to store the session ID.
337    ///
338    /// If the cookie is set as `HttpOnly`, it will not be visible to any JavaScript snippets
339    /// running in the browser.
340    ///
341    /// Default is `true`.
342    pub fn cookie_http_only(mut self, http_only: bool) -> Self {
343        self.configuration.cookie.http_only = http_only;
344        self
345    }
346
347    /// Finalise the builder and return a [`SessionMiddleware`] instance.
348    #[must_use]
349    pub fn build(self) -> SessionMiddleware<Store> {
350        SessionMiddleware::from_parts(self.storage_backend, self.configuration)
351    }
352}
353
354#[derive(Clone)]
355pub(crate) struct Configuration {
356    pub(crate) cookie: CookieConfiguration,
357    pub(crate) session: SessionConfiguration,
358    pub(crate) ttl_extension_policy: TtlExtensionPolicy,
359}
360
361#[derive(Clone)]
362pub(crate) struct SessionConfiguration {
363    pub(crate) state_ttl: Duration,
364}
365
366#[derive(Clone)]
367pub(crate) struct CookieConfiguration {
368    pub(crate) secure: bool,
369    pub(crate) http_only: bool,
370    pub(crate) name: String,
371    pub(crate) same_site: SameSite,
372    pub(crate) path: String,
373    pub(crate) domain: Option<String>,
374    pub(crate) max_age: Option<Duration>,
375    pub(crate) content_security: CookieContentSecurity,
376    pub(crate) key: Key,
377}
378
379pub(crate) fn default_configuration(key: Key) -> Configuration {
380    Configuration {
381        cookie: CookieConfiguration {
382            secure: true,
383            http_only: true,
384            name: "id".into(),
385            same_site: SameSite::Lax,
386            path: "/".into(),
387            domain: None,
388            max_age: None,
389            content_security: CookieContentSecurity::Private,
390            key,
391        },
392        session: SessionConfiguration {
393            state_ttl: default_ttl(),
394        },
395        ttl_extension_policy: default_ttl_extension_policy(),
396    }
397}