actix_cloud/session/config.rs
1//! Configuration options to tune the behaviour of [`SessionMiddleware`].
2
3use std::sync::Arc;
4
5use actix_web::cookie::{time::Duration, Key, SameSite};
6
7use crate::memorydb::MemoryDB;
8
9use super::{storage::SessionStore, SessionMiddleware};
10
11/// A [session lifecycle](SessionLifecycle) strategy where the session cookie will be [persistent].
12///
13/// Persistent cookies have a pre-determined expiration, specified via the `Max-Age` or `Expires`
14/// attribute. They do not disappear when the current browser session ends.
15///
16/// Due to its `Into<SessionLifecycle>` implementation, a `PersistentSession` can be passed directly
17/// to [`SessionMiddlewareBuilder::session_lifecycle()`].
18///
19/// # Examples
20/// ```
21/// use actix_cloud::actix_web::cookie::time::Duration;
22/// use actix_cloud::session::SessionMiddleware;
23/// use actix_cloud::session::config::{PersistentSession, TtlExtensionPolicy};
24///
25/// const SECS_IN_WEEK: i64 = 60 * 60 * 24 * 7;
26///
27/// // a session lifecycle with a time-to-live (expiry) of 1 week and default extension policy
28/// PersistentSession::default().session_ttl(Duration::seconds(SECS_IN_WEEK));
29///
30/// // a session lifecycle with the default time-to-live (expiry) and a custom extension policy
31/// PersistentSession::default()
32/// // this policy causes the session state's TTL to be refreshed on every request
33/// .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest);
34/// ```
35///
36/// [persistent]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
37#[derive(Debug, Clone)]
38pub struct PersistentSession {
39 session_ttl: Duration,
40 ttl_extension_policy: TtlExtensionPolicy,
41}
42
43impl PersistentSession {
44 /// Specifies how long the session cookie should live.
45 ///
46 /// The session TTL is also used as the TTL for the session state in the storage backend.
47 ///
48 /// Defaults to 1 day.
49 ///
50 /// A persistent session can live more than the specified TTL if the TTL is extended.
51 /// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
52 #[doc(alias = "max_age", alias = "max age", alias = "expires")]
53 pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
54 self.session_ttl = session_ttl;
55 self
56 }
57
58 /// Determines under what circumstances the TTL of your session should be extended.
59 /// See [`TtlExtensionPolicy`] for more details.
60 ///
61 /// Defaults to [`TtlExtensionPolicy::OnStateChanges`].
62 pub fn session_ttl_extension_policy(
63 mut self,
64 ttl_extension_policy: TtlExtensionPolicy,
65 ) -> Self {
66 self.ttl_extension_policy = ttl_extension_policy;
67 self
68 }
69}
70
71impl Default for PersistentSession {
72 fn default() -> Self {
73 Self {
74 session_ttl: default_ttl(),
75 ttl_extension_policy: default_ttl_extension_policy(),
76 }
77 }
78}
79
80/// Configuration for which events should trigger an extension of the time-to-live for your session.
81///
82/// If you are using a [`BrowserSession`], `TtlExtensionPolicy` controls how often the TTL of the
83/// session state should be refreshed. The browser is in control of the lifecycle of the session
84/// cookie.
85///
86/// If you are using a [`PersistentSession`], `TtlExtensionPolicy` controls both the expiration of
87/// the session cookie and the TTL of the session state on the storage backend.
88#[derive(Debug, Clone)]
89#[non_exhaustive]
90pub enum TtlExtensionPolicy {
91 /// The TTL is refreshed every time the server receives a request associated with a session.
92 ///
93 /// # Performance impact
94 /// Refreshing the TTL on every request is not free. It implies a refresh of the TTL on the
95 /// session state. This translates into a request over the network if you are using a remote
96 /// system as storage backend (e.g. Redis). This impacts both the total load on your storage
97 /// backend (i.e. number of queries it has to handle) and the latency of the requests served by
98 /// your server.
99 OnEveryRequest,
100
101 /// The TTL is refreshed every time the session state changes or the session key is renewed.
102 OnStateChanges,
103}
104
105/// Determines how to secure the content of the session cookie.
106///
107/// Used by [`SessionMiddlewareBuilder::cookie_content_security`].
108#[derive(Debug, Clone, Copy)]
109pub enum CookieContentSecurity {
110 /// The cookie content is encrypted when using `CookieContentSecurity::Private`.
111 ///
112 /// Encryption guarantees confidentiality and integrity: the client cannot tamper with the
113 /// cookie content nor decode it, as long as the encryption key remains confidential.
114 Private,
115
116 /// The cookie content is signed when using `CookieContentSecurity::Signed`.
117 ///
118 /// Signing guarantees integrity, but it doesn't ensure confidentiality: the client cannot
119 /// tamper with the cookie content, but they can read it.
120 Signed,
121}
122
123pub(crate) const fn default_ttl() -> Duration {
124 Duration::days(1)
125}
126
127pub(crate) const fn default_ttl_extension_policy() -> TtlExtensionPolicy {
128 TtlExtensionPolicy::OnStateChanges
129}
130
131/// A fluent, customized [`SessionMiddleware`] builder.
132#[must_use]
133pub struct SessionMiddlewareBuilder {
134 storage_backend: SessionStore,
135 configuration: Configuration,
136}
137
138impl SessionMiddlewareBuilder {
139 pub(crate) fn new(client: Arc<dyn MemoryDB>, configuration: Configuration) -> Self {
140 Self {
141 storage_backend: SessionStore::new(client),
142 configuration,
143 }
144 }
145
146 pub fn cache_keygen<F>(mut self, keygen: F) -> Self
147 where
148 F: Fn(&str) -> String + 'static + Send + Sync,
149 {
150 self.storage_backend.cache_keygen(keygen);
151 self
152 }
153
154 /// Set the name of the cookie used to store the session ID.
155 ///
156 /// Defaults to `id`.
157 pub fn cookie_name(mut self, name: String) -> Self {
158 self.configuration.cookie.name = name;
159 self
160 }
161
162 /// Set the `Secure` attribute for the cookie used to store the session ID.
163 ///
164 /// If the cookie is set as secure, it will only be transmitted when the connection is secure
165 /// (using `https`).
166 ///
167 /// Default is `true`.
168 pub fn cookie_secure(mut self, secure: bool) -> Self {
169 self.configuration.cookie.secure = secure;
170 self
171 }
172
173 /// Determines how session lifecycle should be managed.
174 pub fn session_lifecycle(mut self, session_lifecycle: PersistentSession) -> Self {
175 self.configuration.cookie.max_age = Some(session_lifecycle.session_ttl);
176 self.configuration.session.state_ttl = session_lifecycle.session_ttl;
177 self.configuration.ttl_extension_policy = session_lifecycle.ttl_extension_policy;
178
179 self
180 }
181
182 /// Set the `SameSite` attribute for the cookie used to store the session ID.
183 ///
184 /// By default, the attribute is set to `Lax`.
185 pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
186 self.configuration.cookie.same_site = same_site;
187 self
188 }
189
190 /// Set the `Path` attribute for the cookie used to store the session ID.
191 ///
192 /// By default, the attribute is set to `/`.
193 pub fn cookie_path(mut self, path: String) -> Self {
194 self.configuration.cookie.path = path;
195 self
196 }
197
198 /// Set the `Domain` attribute for the cookie used to store the session ID.
199 ///
200 /// Use `None` to leave the attribute unspecified. If unspecified, the attribute defaults
201 /// to the same host that set the cookie, excluding subdomains.
202 ///
203 /// By default, the attribute is left unspecified.
204 pub fn cookie_domain(mut self, domain: Option<String>) -> Self {
205 self.configuration.cookie.domain = domain;
206 self
207 }
208
209 /// Choose how the session cookie content should be secured.
210 ///
211 /// - [`CookieContentSecurity::Private`] selects encrypted cookie content.
212 /// - [`CookieContentSecurity::Signed`] selects signed cookie content.
213 ///
214 /// # Default
215 /// By default, the cookie content is encrypted. Encrypted was chosen instead of signed as
216 /// default because it reduces the chances of sensitive information being exposed in the session
217 /// key by accident, regardless of SessionStore implementation you chose to use.
218 ///
219 /// For example, if you are using cookie-based storage, you definitely want the cookie content
220 /// to be encrypted—the whole session state is embedded in the cookie! If you are using
221 /// Redis-based storage, signed is more than enough - the cookie content is just a unique
222 /// tamper-proof session key.
223 pub fn cookie_content_security(mut self, content_security: CookieContentSecurity) -> Self {
224 self.configuration.cookie.content_security = content_security;
225 self
226 }
227
228 /// Set the `HttpOnly` attribute for the cookie used to store the session ID.
229 ///
230 /// If the cookie is set as `HttpOnly`, it will not be visible to any JavaScript snippets
231 /// running in the browser.
232 ///
233 /// Default is `true`.
234 pub fn cookie_http_only(mut self, http_only: bool) -> Self {
235 self.configuration.cookie.http_only = http_only;
236 self
237 }
238
239 /// Finalise the builder and return a [`SessionMiddleware`] instance.
240 #[must_use]
241 pub fn build(self) -> SessionMiddleware {
242 SessionMiddleware::from_parts(self.storage_backend, self.configuration)
243 }
244}
245
246#[derive(Clone)]
247pub(crate) struct Configuration {
248 pub(crate) cookie: CookieConfiguration,
249 pub(crate) session: SessionConfiguration,
250 pub(crate) ttl_extension_policy: TtlExtensionPolicy,
251}
252
253#[derive(Clone)]
254pub(crate) struct SessionConfiguration {
255 pub(crate) state_ttl: Duration,
256}
257
258#[derive(Clone)]
259pub(crate) struct CookieConfiguration {
260 pub(crate) secure: bool,
261 pub(crate) http_only: bool,
262 pub(crate) name: String,
263 pub(crate) same_site: SameSite,
264 pub(crate) path: String,
265 pub(crate) domain: Option<String>,
266 pub(crate) max_age: Option<Duration>,
267 pub(crate) content_security: CookieContentSecurity,
268 pub(crate) key: Key,
269}
270
271pub(crate) fn default_configuration(key: Key) -> Configuration {
272 Configuration {
273 cookie: CookieConfiguration {
274 secure: true,
275 http_only: true,
276 name: "id".into(),
277 same_site: SameSite::Lax,
278 path: "/".into(),
279 domain: None,
280 max_age: None,
281 content_security: CookieContentSecurity::Private,
282 key,
283 },
284 session: SessionConfiguration {
285 state_ttl: default_ttl(),
286 },
287 ttl_extension_policy: default_ttl_extension_policy(),
288 }
289}