Skip to main content

rainier_session/
session.rs

1//! The session bag — [`Session`] and the [`SessionData`] that is persisted.
2
3use std::sync::{Arc, Mutex};
4
5use rainier_support::Result;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10/// The key the CSRF token is kept under.
11pub const TOKEN_KEY: &str = "_token";
12
13/// Generate a session id: 256 bits of randomness, hex-encoded.
14///
15/// A session id is a bearer credential, so it comes from the OS CSPRNG rather
16/// than a counter or a timestamp — anything guessable here is an account
17/// takeover.
18pub fn generate_session_id() -> String {
19    use rand::RngCore;
20
21    let mut bytes = [0u8; 32];
22    rand::rngs::OsRng.fill_bytes(&mut bytes);
23    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
24}
25
26/// What a [`SessionStore`](crate::SessionStore) holds for one session.
27#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct SessionData {
29    /// The values themselves.
30    #[serde(default)]
31    pub values: Map<String, Value>,
32
33    /// Keys flashed during the request that just ended, and therefore
34    /// readable during the next one and deleted at the end of it.
35    #[serde(default)]
36    pub flash: Vec<String>,
37}
38
39#[derive(Debug)]
40struct State {
41    id: String,
42    values: Map<String, Value>,
43    /// Flashed by a previous request: readable now, deleted when this one ends
44    /// unless [`Session::keep`] rescues it.
45    flash_old: Vec<String>,
46    /// Flashed by this request: readable in the next one.
47    flash_new: Vec<String>,
48    dirty: bool,
49    /// Ids superseded by [`Session::regenerate`] or
50    /// [`Session::invalidate`], for the middleware to delete.
51    superseded: Vec<String>,
52}
53
54/// A request's session.
55///
56/// Cheap to clone — every clone is the same underlying bag, which is what lets
57/// the middleware hold one while the handler writes through another.
58///
59/// Reached from a handler with
60/// [`request.session()`](crate::SessionRequestExt::session), which is `Some`
61/// exactly when the [`StartSession`](crate::StartSession) middleware ran.
62#[derive(Clone)]
63pub struct Session {
64    state: Arc<Mutex<State>>,
65}
66
67impl Session {
68    /// A fresh, empty session with a new id.
69    pub fn new() -> Self {
70        Self::restore(generate_session_id(), SessionData::default())
71    }
72
73    /// A session loaded from a store.
74    pub fn restore(id: impl Into<String>, data: SessionData) -> Self {
75        Self {
76            state: Arc::new(Mutex::new(State {
77                id: id.into(),
78                values: data.values,
79                flash_old: data.flash,
80                flash_new: Vec::new(),
81                dirty: false,
82                superseded: Vec::new(),
83            })),
84        }
85    }
86
87    fn lock(&self) -> std::sync::MutexGuard<'_, State> {
88        // A panic in a handler must not make every later request fail on a
89        // poisoned lock; the bag's invariants do not depend on the panicking
90        // section having finished.
91        self.state.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
92    }
93
94    /// The session id, as it appears in the cookie.
95    pub fn id(&self) -> String {
96        self.lock().id.clone()
97    }
98
99    /// Whether anything has changed and the session needs writing back.
100    pub fn is_dirty(&self) -> bool {
101        self.lock().dirty
102    }
103
104    // --- reading -----------------------------------------------------------
105
106    /// A value, deserialised.
107    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
108        serde_json::from_value(self.lock().values.get(key)?.clone()).ok()
109    }
110
111    /// A value as a string, if it is one.
112    pub fn string(&self, key: &str) -> Option<String> {
113        match self.lock().values.get(key)? {
114            Value::String(value) => Some(value.clone()),
115            other => Some(other.to_string()),
116        }
117    }
118
119    /// The raw value.
120    pub fn value(&self, key: &str) -> Option<Value> {
121        self.lock().values.get(key).cloned()
122    }
123
124    /// Whether the key is present and not null.
125    pub fn has(&self, key: &str) -> bool {
126        matches!(self.lock().values.get(key), Some(value) if !value.is_null())
127    }
128
129    /// Every value.
130    pub fn all(&self) -> Map<String, Value> {
131        self.lock().values.clone()
132    }
133
134    /// How many values are held.
135    pub fn len(&self) -> usize {
136        self.lock().values.len()
137    }
138
139    /// Whether the session holds nothing.
140    pub fn is_empty(&self) -> bool {
141        self.len() == 0
142    }
143
144    // --- writing -----------------------------------------------------------
145
146    /// Store a value.
147    pub fn put(&self, key: impl Into<String>, value: impl Serialize) -> Result<()> {
148        let value = serde_json::to_value(value)?;
149        let mut state = self.lock();
150        state.values.insert(key.into(), value);
151        state.dirty = true;
152        Ok(())
153    }
154
155    /// Read a value and remove it in one step.
156    pub fn pull<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
157        let mut state = self.lock();
158        let value = state.values.remove(key)?;
159        state.dirty = true;
160        serde_json::from_value(value).ok()
161    }
162
163    /// Remove a value.
164    pub fn forget(&self, key: &str) {
165        let mut state = self.lock();
166        if state.values.remove(key).is_some() {
167            state.dirty = true;
168        }
169    }
170
171    /// Remove every value, keeping the id.
172    pub fn flush(&self) {
173        let mut state = self.lock();
174        state.values.clear();
175        state.flash_new.clear();
176        state.flash_old.clear();
177        state.dirty = true;
178    }
179
180    // --- flash -------------------------------------------------------------
181
182    /// Store a value for the **next** request only.
183    ///
184    /// The redirect-then-show-a-message pattern: the value survives exactly
185    /// one further request and is then deleted, without anything having to
186    /// remember to clean it up.
187    pub fn flash(&self, key: impl Into<String>, value: impl Serialize) -> Result<()> {
188        let key = key.into();
189        let value = serde_json::to_value(value)?;
190
191        let mut state = self.lock();
192        state.values.insert(key.clone(), value);
193        if !state.flash_new.contains(&key) {
194            state.flash_new.push(key);
195        }
196        state.dirty = true;
197        Ok(())
198    }
199
200    /// Keep the named flashed values for one further request.
201    ///
202    /// What a redirect chain needs: without it, a value flashed before a
203    /// redirect that itself redirects is gone before anything renders it.
204    pub fn keep(&self, keys: &[&str]) {
205        let mut state = self.lock();
206        for key in keys {
207            let key = (*key).to_string();
208            if state.flash_old.contains(&key) && !state.flash_new.contains(&key) {
209                state.flash_new.push(key);
210                state.dirty = true;
211            }
212        }
213    }
214
215    /// Keep every flashed value for one further request.
216    pub fn reflash(&self) {
217        let mut state = self.lock();
218        let carried: Vec<String> =
219            state.flash_old.iter().filter(|key| !state.flash_new.contains(key)).cloned().collect();
220
221        if !carried.is_empty() {
222            state.flash_new.extend(carried);
223            state.dirty = true;
224        }
225    }
226
227    // --- identity ----------------------------------------------------------
228
229    /// The CSRF token, minted on first use.
230    pub fn token(&self) -> String {
231        if let Some(Value::String(token)) = self.lock().values.get(TOKEN_KEY) {
232            return token.clone();
233        }
234
235        let token = generate_session_id();
236        let mut state = self.lock();
237        state.values.insert(TOKEN_KEY.to_string(), Value::String(token.clone()));
238        state.dirty = true;
239        token
240    }
241
242    /// Give the session a new id, keeping its contents.
243    ///
244    /// **Call this on login.** Without it, an attacker who can set a victim's
245    /// session cookie before they authenticate ends up holding a cookie for
246    /// their authenticated session — session fixation, and it is invisible in
247    /// testing because everything works.
248    pub fn regenerate(&self) {
249        let mut state = self.lock();
250        let previous = std::mem::replace(&mut state.id, generate_session_id());
251        state.superseded.push(previous);
252        state.dirty = true;
253    }
254
255    /// Give the session a new id and throw its contents away. Logging out.
256    pub fn invalidate(&self) {
257        self.flush();
258        self.regenerate();
259    }
260
261    // --- persistence -------------------------------------------------------
262
263    /// Ids this session has left behind, for the store to delete.
264    pub fn superseded_ids(&self) -> Vec<String> {
265        self.lock().superseded.clone()
266    }
267
268    /// Age the flash data and produce what should be written back.
269    ///
270    /// Called once by the middleware, after the handler has run: values
271    /// flashed by a *previous* request are dropped unless they were kept, and
272    /// values flashed by *this* one become the next request's.
273    pub fn age_and_take(&self) -> SessionData {
274        let mut state = self.lock();
275
276        let expired: Vec<String> =
277            state.flash_old.iter().filter(|key| !state.flash_new.contains(key)).cloned().collect();
278
279        for key in expired {
280            state.values.remove(&key);
281        }
282
283        let flash = std::mem::take(&mut state.flash_new);
284        state.flash_old = flash.clone();
285
286        SessionData { values: state.values.clone(), flash }
287    }
288}
289
290impl Default for Session {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296/// Deliberately opaque: a session holds whatever the application put in it,
297/// which in practice includes user ids and CSRF tokens.
298impl std::fmt::Debug for Session {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        let state = self.lock();
301        f.debug_struct("Session")
302            .field("id", &"<redacted>")
303            .field("values", &state.values.len())
304            .field("dirty", &state.dirty)
305            .finish()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn ids_are_long_and_unpredictable() {
315        let a = generate_session_id();
316        let b = generate_session_id();
317
318        assert_eq!(a.len(), 64, "256 bits, hex-encoded");
319        assert_ne!(a, b);
320        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
321    }
322
323    #[test]
324    fn values_round_trip() {
325        let session = Session::new();
326        session.put("user_id", 42u64).unwrap();
327        session.put("name", "Ada").unwrap();
328
329        assert_eq!(session.get::<u64>("user_id"), Some(42));
330        assert_eq!(session.string("name").as_deref(), Some("Ada"));
331        assert!(session.has("user_id"));
332        assert_eq!(session.len(), 2);
333    }
334
335    #[test]
336    fn a_fresh_session_is_not_dirty() {
337        assert!(!Session::new().is_dirty(), "nothing to write back yet");
338    }
339
340    #[test]
341    fn writing_marks_it_dirty_and_reading_does_not() {
342        let session = Session::restore("id", SessionData::default());
343        assert!(!session.is_dirty());
344
345        session.get::<u64>("absent");
346        session.has("absent");
347        assert!(!session.is_dirty(), "reads must not force a write");
348
349        session.put("x", 1).unwrap();
350        assert!(session.is_dirty());
351    }
352
353    #[test]
354    fn forgetting_an_absent_key_is_not_a_change() {
355        let session = Session::restore("id", SessionData::default());
356        session.forget("absent");
357
358        assert!(!session.is_dirty());
359    }
360
361    #[test]
362    fn pull_reads_and_removes() {
363        let session = Session::new();
364        session.put("once", "value").unwrap();
365
366        assert_eq!(session.pull::<String>("once").as_deref(), Some("value"));
367        assert!(!session.has("once"));
368    }
369
370    #[test]
371    fn flushing_empties_it_but_keeps_the_id() {
372        let session = Session::new();
373        let id = session.id();
374        session.put("a", 1).unwrap();
375
376        session.flush();
377
378        assert!(session.is_empty());
379        assert_eq!(session.id(), id);
380    }
381
382    #[test]
383    fn a_flashed_value_survives_exactly_one_more_request() {
384        // Request 1 flashes it.
385        let session = Session::new();
386        session.flash("status", "Saved.").unwrap();
387        assert_eq!(session.string("status").as_deref(), Some("Saved."), "readable immediately");
388        let after_one = session.age_and_take();
389
390        // Request 2 reads it.
391        let session = Session::restore("id", after_one);
392        assert_eq!(session.string("status").as_deref(), Some("Saved."));
393        let after_two = session.age_and_take();
394
395        // Request 3 does not.
396        let session = Session::restore("id", after_two);
397        assert!(!session.has("status"), "flash data must not linger");
398    }
399
400    #[test]
401    fn keep_rescues_a_flashed_value_for_one_more_request() {
402        let session = Session::new();
403        session.flash("status", "Saved.").unwrap();
404        let data = session.age_and_take();
405
406        // The next request redirects again, so it keeps the message.
407        let session = Session::restore("id", data);
408        session.keep(&["status"]);
409        let data = session.age_and_take();
410
411        let session = Session::restore("id", data);
412        assert_eq!(session.string("status").as_deref(), Some("Saved."));
413    }
414
415    #[test]
416    fn reflash_keeps_everything() {
417        let session = Session::new();
418        session.flash("a", 1).unwrap();
419        session.flash("b", 2).unwrap();
420        let data = session.age_and_take();
421
422        let session = Session::restore("id", data);
423        session.reflash();
424        let data = session.age_and_take();
425
426        let session = Session::restore("id", data);
427        assert!(session.has("a") && session.has("b"));
428    }
429
430    #[test]
431    fn keeping_an_unflashed_key_does_nothing() {
432        let session = Session::new();
433        session.put("permanent", 1).unwrap();
434        session.keep(&["permanent"]);
435
436        let data = session.age_and_take();
437        assert!(data.values.contains_key("permanent"));
438        assert!(data.flash.is_empty(), "a normal value is not flash data");
439    }
440
441    #[test]
442    fn a_permanent_value_is_not_aged_out() {
443        let session = Session::new();
444        session.put("user_id", 42u64).unwrap();
445        session.flash("status", "hi").unwrap();
446
447        let data = session.age_and_take();
448        let session = Session::restore("id", data);
449        let data = session.age_and_take();
450
451        assert!(data.values.contains_key("user_id"));
452        assert!(!data.values.contains_key("status"));
453    }
454
455    #[test]
456    fn the_token_is_minted_once_and_then_stable() {
457        let session = Session::new();
458        let first = session.token();
459
460        assert_eq!(session.token(), first, "a second read must not rotate it");
461        assert_eq!(first.len(), 64);
462        assert!(session.is_dirty(), "minting it needs writing back");
463    }
464
465    #[test]
466    fn regenerating_changes_the_id_and_keeps_the_data() {
467        let session = Session::new();
468        let before = session.id();
469        session.put("user_id", 42u64).unwrap();
470
471        session.regenerate();
472
473        assert_ne!(session.id(), before);
474        assert_eq!(session.get::<u64>("user_id"), Some(42), "login must not lose the session");
475        assert_eq!(session.superseded_ids(), vec![before], "the old row should be deleted");
476    }
477
478    #[test]
479    fn invalidating_changes_the_id_and_drops_the_data() {
480        let session = Session::new();
481        let before = session.id();
482        session.put("user_id", 42u64).unwrap();
483
484        session.invalidate();
485
486        assert_ne!(session.id(), before);
487        assert!(session.is_empty());
488        assert_eq!(session.superseded_ids(), vec![before]);
489    }
490
491    #[test]
492    fn clones_share_one_bag() {
493        let session = Session::new();
494        let handle = session.clone();
495
496        handle.put("written-through-the-clone", true).unwrap();
497
498        assert!(session.has("written-through-the-clone"));
499    }
500
501    #[test]
502    fn debug_does_not_disclose_the_id_or_the_values() {
503        let session = Session::new();
504        session.put("secret", "hunter2").unwrap();
505
506        let rendered = format!("{session:?}");
507        assert!(!rendered.contains("hunter2"), "{rendered}");
508        assert!(!rendered.contains(&session.id()), "{rendered}");
509    }
510
511    #[test]
512    fn data_round_trips_through_json() {
513        let session = Session::new();
514        session.put("user_id", 42u64).unwrap();
515        session.flash("status", "Saved.").unwrap();
516
517        let data = session.age_and_take();
518        let encoded = serde_json::to_string(&data).unwrap();
519        let decoded: SessionData = serde_json::from_str(&encoded).unwrap();
520
521        assert_eq!(decoded, data);
522    }
523}