trillium_sessions/
session_handler.rs

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