trillium_sessions/session_handler.rs
1const BASE64_DIGEST_LEN: usize = 44;
2use async_session::{
3 Session, SessionStore, base64,
4 hmac::{Hmac, Mac, NewMac},
5 sha2::Sha256,
6};
7use std::{
8 fmt::{self, Debug, Formatter},
9 iter,
10 time::{Duration, SystemTime},
11};
12use trillium::{Conn, Handler};
13use trillium_cookies::{
14 CookiesConnExt,
15 cookie::{Cookie, Key, SameSite},
16};
17
18/// # Handler to enable sessions.
19///
20/// See crate-level docs for an overview of this crate's approach to
21/// sessions and security.
22pub struct SessionHandler<Store> {
23 store: Store,
24 cookie_path: String,
25 cookie_name: String,
26 cookie_domain: Option<String>,
27 session_ttl: Option<Duration>,
28 save_unchanged: bool,
29 same_site_policy: SameSite,
30 key: Key,
31 older_keys: Vec<Key>,
32}
33
34impl<Store: SessionStore> Debug for SessionHandler<Store> {
35 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
36 f.debug_struct("SessionHandler")
37 .field("store", &self.store)
38 .field("cookie_path", &self.cookie_path)
39 .field("cookie_name", &self.cookie_name)
40 .field("cookie_domain", &self.cookie_domain)
41 .field("session_ttl", &self.session_ttl)
42 .field("save_unchanged", &self.save_unchanged)
43 .field("same_site_policy", &self.same_site_policy)
44 .field("key", &"<<secret>>")
45 .field("older_keys", &"<<secret>>")
46 .finish()
47 }
48}
49
50impl<Store: SessionStore> SessionHandler<Store> {
51 /// Constructs a SessionHandler from the given
52 /// [`async_session::SessionStore`] and secret.
53 ///
54 /// The `secret` MUST be at least 32 bytes long, and MUST be cryptographically random to be
55 /// secure. It is recommended to retrieve this at runtime from the environment instead of
56 /// compiling it into your application.
57 ///
58 /// # Panics
59 ///
60 /// `SessionHandler::new` will panic if the secret is fewer than 32 bytes.
61 ///
62 /// # Defaults
63 ///
64 /// The defaults for `SessionHandler` are:
65 ///
66 /// * cookie path: "/"
67 /// * cookie name: "trillium.sid"
68 /// * session ttl: one day
69 /// * same site: strict
70 /// * save unchanged: enabled
71 /// * older secrets: none
72 ///
73 /// # Customization
74 ///
75 /// Although the above defaults are appropriate for most applications, they can be
76 /// overridden. Please be careful changing these settings, as they can weaken your application's
77 /// security:
78 ///
79 /// ```rust
80 /// # use std::time::Duration;
81 /// # let secrets = concat!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ",
82 /// # "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
83 /// # unsafe { std::env::set_var("TRILLIUM_SESSION_SECRETS", secrets); }
84 ///
85 /// use trillium_cookies::{CookiesHandler, cookie::SameSite};
86 /// use trillium_sessions::{MemoryStore, SessionHandler};
87 ///
88 /// // this logic will be unique to your deployment
89 /// let secrets_var = std::env::var("TRILLIUM_SESSION_SECRETS").unwrap();
90 /// let session_secrets = secrets_var.split(' ').collect::<Vec<_>>();
91 ///
92 /// let handler = (
93 /// CookiesHandler::new(),
94 /// SessionHandler::new(MemoryStore::new(), session_secrets[0])
95 /// .with_cookie_name("custom.cookie.name")
96 /// .with_cookie_path("/some/path")
97 /// .with_cookie_domain("trillium.rs")
98 /// .with_same_site_policy(SameSite::Strict)
99 /// .with_session_ttl(Some(Duration::from_secs(1)))
100 /// .with_older_secrets(&session_secrets[1..])
101 /// .without_save_unchanged(),
102 /// );
103 /// ```
104 pub fn new(store: Store, secret: impl AsRef<[u8]>) -> Self {
105 Self {
106 store,
107 save_unchanged: true,
108 cookie_path: "/".into(),
109 cookie_name: "trillium.sid".into(),
110 cookie_domain: None,
111 same_site_policy: SameSite::Lax,
112 session_ttl: Some(Duration::from_secs(24 * 60 * 60)),
113 key: Key::derive_from(secret.as_ref()),
114 older_keys: vec![],
115 }
116 }
117
118 /// Sets a cookie path for this session handler.
119 /// The default for this value is "/"
120 pub fn with_cookie_path(mut self, cookie_path: impl AsRef<str>) -> Self {
121 cookie_path.as_ref().clone_into(&mut self.cookie_path);
122 self
123 }
124
125 /// Sets a session ttl.
126 ///
127 /// This will be used both for the cookie expiry and also for the session-internal expiry.
128 ///
129 /// The default for this value is one day. Set this to None to not set a cookie or session
130 /// expiry. This is not recommended.
131 pub fn with_session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
132 self.session_ttl = session_ttl;
133 self
134 }
135
136 /// Sets the name of the cookie that the session is stored with or in.
137 ///
138 /// If you are running multiple trillium applications on the same domain, you will need
139 /// different values for each application. The default value is "trillium.sid"
140 pub fn with_cookie_name(mut self, cookie_name: impl AsRef<str>) -> Self {
141 cookie_name.as_ref().clone_into(&mut self.cookie_name);
142 self
143 }
144
145 /// Disables the `save_unchanged` setting.
146 ///
147 /// When `save_unchanged` is enabled, a session will cookie will always be set. With
148 /// `save_unchanged` disabled, the session data must be modified from the `Default` value in
149 /// order for it to save. If a session already exists and its data unmodified in the course of a
150 /// request, the session will only be persisted if `save_unchanged` is enabled.
151 pub fn without_save_unchanged(mut self) -> Self {
152 self.save_unchanged = false;
153 self
154 }
155
156 /// Sets the same site policy for the session cookie. Defaults to SameSite::Strict. See
157 /// [incrementally better
158 /// cookies](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01) for more
159 /// information about this setting
160 pub fn with_same_site_policy(mut self, policy: SameSite) -> Self {
161 self.same_site_policy = policy;
162 self
163 }
164
165 /// Sets the domain of the cookie.
166 pub fn with_cookie_domain(mut self, cookie_domain: impl AsRef<str>) -> Self {
167 self.cookie_domain = Some(cookie_domain.as_ref().to_owned());
168 self
169 }
170
171 /// Sets optional older signing keys that will not be used to sign cookies, but can be used to
172 /// validate previously signed cookies.
173 pub fn with_older_secrets(mut self, secrets: &[impl AsRef<[u8]>]) -> Self {
174 self.older_keys = secrets
175 .iter()
176 .map(AsRef::as_ref)
177 .map(Key::derive_from)
178 .collect();
179 self
180 }
181
182 //--- methods below here are private ---
183
184 async fn load_or_create(&self, cookie_value: Option<&str>) -> Session {
185 let session = match cookie_value {
186 Some(cookie_value) => self
187 .store
188 .load_session(String::from(cookie_value))
189 .await
190 .ok()
191 .flatten(),
192 None => None,
193 };
194
195 session
196 .and_then(|session| session.validate())
197 .unwrap_or_default()
198 }
199
200 fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> {
201 let mut cookie: Cookie<'static> = Cookie::build((self.cookie_name.clone(), cookie_value))
202 .http_only(true)
203 .same_site(self.same_site_policy)
204 .secure(secure)
205 .path(self.cookie_path.clone())
206 .into();
207
208 if let Some(ttl) = self.session_ttl {
209 cookie.set_expires(Some((SystemTime::now() + ttl).into()));
210 }
211
212 if let Some(cookie_domain) = self.cookie_domain.clone() {
213 cookie.set_domain(cookie_domain)
214 }
215
216 self.sign_cookie(&mut cookie);
217
218 cookie
219 }
220
221 // the following is reused verbatim from
222 // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L37-46
223 /// Signs the cookie's value providing integrity and authenticity.
224 fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
225 // Compute HMAC-SHA256 of the cookie's value.
226 let mut mac = Hmac::<Sha256>::new_from_slice(self.key.signing()).expect("good key");
227 mac.update(cookie.value().as_bytes());
228
229 // Cookie's new value is [MAC | original-value].
230 let mut new_value = base64::encode(mac.finalize().into_bytes());
231 new_value.push_str(cookie.value());
232 cookie.set_value(new_value);
233 }
234
235 // the following is based on
236 // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L51-L66
237 /// Given a signed value `str` where the signature is prepended to `value`, verifies the signed
238 /// value and returns it. If there's a problem, returns an `Err` with a string describing the
239 /// issue.
240 fn verify_signature<'a>(&self, cookie_value: &'a str) -> Option<&'a str> {
241 if cookie_value.len() < BASE64_DIGEST_LEN {
242 log::trace!("length of value is <= BASE64_DIGEST_LEN");
243 return None;
244 }
245
246 // Split [MAC | original-value] into its two parts.
247 let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
248 let digest = match base64::decode(digest_str) {
249 Ok(digest) => digest,
250 Err(_) => {
251 log::trace!("bad base64 digest");
252 return None;
253 }
254 };
255
256 iter::once(&self.key)
257 .chain(self.older_keys.iter())
258 .find_map(|key| {
259 let mut mac = Hmac::<Sha256>::new_from_slice(key.signing()).expect("good key");
260 mac.update(value.as_bytes());
261 mac.verify(&digest).ok()
262 })
263 .map(|_| value)
264 }
265}
266
267impl<Store: SessionStore> Handler for SessionHandler<Store> {
268 async fn run(&self, mut conn: Conn) -> Conn {
269 let session = conn.take_state::<Session>();
270
271 let cookie_value = conn
272 .cookies()
273 .get(&self.cookie_name)
274 .and_then(|cookie| self.verify_signature(cookie.value()));
275
276 let mut session = match session {
277 Some(session) => session,
278 None => self.load_or_create(cookie_value).await,
279 };
280
281 if let Some(ttl) = self.session_ttl {
282 session.expire_in(ttl);
283 }
284
285 conn.with_state(session)
286 }
287
288 async fn before_send(&self, mut conn: Conn) -> Conn {
289 if let Some(session) = conn.take_state::<Session>() {
290 let session_to_keep = session.clone();
291 let secure = conn.is_secure();
292 if session.is_destroyed() {
293 self.store.destroy_session(session).await.ok();
294 conn.cookies_mut()
295 .remove(Cookie::from(self.cookie_name.clone()));
296 } else if self.save_unchanged || session.data_changed() {
297 match self.store.store_session(session).await {
298 Ok(Some(cookie_value)) => {
299 conn.cookies_mut()
300 .add(self.build_cookie(secure, cookie_value));
301 }
302
303 Ok(None) => {}
304
305 Err(e) => {
306 log::error!("could not store session:\n\n{e}")
307 }
308 }
309 }
310
311 conn.with_state(session_to_keep)
312 } else {
313 conn
314 }
315 }
316}
317
318/// Alias for [`SessionHandler::new`]
319pub fn sessions<Store>(store: Store, secret: impl AsRef<[u8]>) -> SessionHandler<Store>
320where
321 Store: SessionStore,
322{
323 SessionHandler::new(store, secret)
324}