trillium_sessions/
session_handler.rs1const 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
19pub 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 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 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 pub fn with_session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
133 self.session_ttl = session_ttl;
134 self
135 }
136
137 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 pub fn without_save_unchanged(mut self) -> Self {
155 self.save_unchanged = false;
156 self
157 }
158
159 pub fn with_same_site_policy(mut self, policy: SameSite) -> Self {
164 self.same_site_policy = policy;
165 self
166 }
167
168 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 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 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 fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
228 let mut mac = Hmac::<Sha256>::new_from_slice(self.key.signing()).expect("good key");
230 mac.update(cookie.value().as_bytes());
231
232 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 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 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
322pub fn sessions<Store>(store: Store, secret: impl AsRef<[u8]>) -> SessionHandler<Store>
324where
325 Store: SessionStore,
326{
327 SessionHandler::new(store, secret)
328}