Skip to main content

typed_session/
session_store.rs

1use crate::session::{SessionId, SessionState};
2use crate::session_store::cookie_generator::SessionCookieGenerator;
3use crate::{DefaultSessionCookieGenerator, Error, Session, SessionExpiry};
4use async_trait::async_trait;
5use chrono::Utc;
6use chrono::{DateTime, Duration};
7use std::fmt::Debug;
8use std::marker::PhantomData;
9
10pub(crate) mod cookie_generator;
11
12/// An async session store.
13///
14/// This is the "front-end" interface of the session store.
15///
16/// Note that most of its methods require passing a connection to the storage backend,
17/// which is of type `SessionStoreConnection`.
18/// This is to allow the usage of e.g. an external connection pool without dependence on the session store.
19///
20/// `SessionData` is the data associated with a session.
21/// `SessionStoreConnection` is the connection to the backend session store.
22/// `CookieGenerator` is the type used to generate random session cookies.
23#[derive(Debug)]
24pub struct SessionStore<
25    SessionData,
26    SessionStoreConnection,
27    CookieGenerator = DefaultSessionCookieGenerator,
28> {
29    cookie_generator: CookieGenerator,
30    session_renewal_strategy: SessionRenewalStrategy,
31    data: PhantomData<SessionData>,
32    connection: PhantomData<SessionStoreConnection>,
33}
34
35/// The strategy to renew sessions.
36#[derive(Clone, Copy, Debug)]
37pub enum SessionRenewalStrategy {
38    /// Never update the expiry of a session.
39    /// This leaves updating expiry times to the user.
40    Ignore,
41
42    /// Sessions have a given time-to-live, and their expiry is renewed periodically.
43    /// For example, if the TTL is 7 days, and the maximum remaining TTL for renewal is 6 days,
44    /// then the session's expiry will be updated about daily, if the session is being used.
45    AutomaticRenewal {
46        /// The time-to-live for a new or renewed session.
47        time_to_live: Duration,
48        /// The maximum remaining time-to-live to trigger a session renewal.
49        maximum_remaining_time_to_live_for_renewal: Duration,
50    },
51}
52
53impl<SessionData, SessionStoreConnection>
54    SessionStore<SessionData, SessionStoreConnection, DefaultSessionCookieGenerator>
55{
56    /// Create a new session store with the given cookie generator and session renewal strategy.
57    pub fn new(expiry_strategy: SessionRenewalStrategy) -> Self {
58        Self {
59            cookie_generator: Default::default(),
60            session_renewal_strategy: expiry_strategy,
61            data: Default::default(),
62            connection: Default::default(),
63        }
64    }
65}
66
67impl<SessionData, SessionStoreConnection, CookieGenerator>
68    SessionStore<SessionData, SessionStoreConnection, CookieGenerator>
69{
70    /// Create a new session store with the given cookie generator and session renewal strategy.
71    pub fn new_with_cookie_generator(
72        cookie_generator: CookieGenerator,
73        session_renewal_strategy: SessionRenewalStrategy,
74    ) -> Self {
75        Self {
76            cookie_generator,
77            session_renewal_strategy,
78            data: Default::default(),
79            connection: Default::default(),
80        }
81    }
82
83    /// A reference to the session renewal strategy of this session store.
84    pub fn session_renewal_strategy(&self) -> &SessionRenewalStrategy {
85        &self.session_renewal_strategy
86    }
87
88    /// A mutable reference to the session renewal strategy of this session store.
89    pub fn session_renewal_strategy_mut(&mut self) -> &mut SessionRenewalStrategy {
90        &mut self.session_renewal_strategy
91    }
92}
93
94impl<
95        SessionData: Debug,
96        SessionStoreConnection: SessionStoreConnector<SessionData>,
97        CookieGenerator: SessionCookieGenerator,
98    > SessionStore<SessionData, SessionStoreConnection, CookieGenerator>
99{
100    /// Store a session in the storage backend.
101    /// If the session is marked for deletion, this method deletes the session.
102    ///
103    /// If the session cookie requires to be updated, because the session data or expiry changed,
104    /// then a [SessionCookieCommand] is returned.
105    pub async fn store_session(
106        &self,
107        mut session: Session<SessionData>,
108        connection: &mut SessionStoreConnection,
109    ) -> Result<SessionCookieCommand, Error<SessionStoreConnection::Error>> {
110        if matches!(
111            &session.state,
112            SessionState::NewChanged { .. }
113                | SessionState::Changed { .. }
114                | SessionState::Deleted { .. }
115        ) {
116            // If we store a new session, we need to update its expiry.
117            // In all other cases, the expiry is updated when loading the session.
118            // This allows the user to see the current session expiry by inspecting the session.
119            if matches!(&session.state, SessionState::NewChanged { .. }) {
120                self.session_renewal_strategy
121                    .apply_to_session(&mut session, Utc::now());
122            }
123
124            if let Some(maximum_retries_on_collision) = connection.maximum_retries_on_id_collision()
125            {
126                for _ in 0..maximum_retries_on_collision {
127                    match self.try_store_session(&session, connection).await? {
128                        WriteSessionResult::Ok(command) => return Ok(command),
129                        WriteSessionResult::SessionIdExists => { /* continue trying */ }
130                    }
131                }
132
133                Err(Error::MaximumSessionIdGenerationTriesReached {
134                    maximum: maximum_retries_on_collision,
135                })
136            } else {
137                loop {
138                    match self.try_store_session(&session, connection).await? {
139                        WriteSessionResult::Ok(command) => return Ok(command),
140                        WriteSessionResult::SessionIdExists => { /* continue trying */ }
141                    }
142                }
143            }
144        } else {
145            Ok(SessionCookieCommand::DoNothing)
146        }
147    }
148
149    async fn try_store_session(
150        &self,
151        session: &Session<SessionData>,
152        connection: &mut SessionStoreConnection,
153    ) -> Result<WriteSessionResult<SessionCookieCommand>, Error<SessionStoreConnection::Error>>
154    {
155        match &session.state {
156            SessionState::NewChanged { expiry, data } => {
157                let cookie_value = self.cookie_generator.generate_cookie();
158                let id = SessionId::from_cookie_value(&cookie_value);
159                Ok(connection
160                    .create_session(&id, expiry, data)
161                    .await?
162                    .map(|()| SessionCookieCommand::Set {
163                        cookie_value,
164                        expiry: *expiry,
165                    }))
166            }
167            SessionState::Changed {
168                current_id: previous_id,
169                expiry,
170                data,
171            } => {
172                let cookie_value = self.cookie_generator.generate_cookie();
173                let current_id = SessionId::from_cookie_value(&cookie_value);
174                Ok(connection
175                    .update_session(&current_id, previous_id, expiry, data)
176                    .await?
177                    .map(|()| SessionCookieCommand::Set {
178                        cookie_value,
179                        expiry: *expiry,
180                    }))
181            }
182            SessionState::Deleted { current_id } => {
183                connection.delete_session(current_id).await?;
184                Ok(WriteSessionResult::Ok(SessionCookieCommand::Delete))
185            }
186            SessionState::NewUnchanged { .. }
187            | SessionState::Unchanged { .. }
188            | SessionState::NewDeleted => unreachable!(),
189            SessionState::Invalid => unreachable!("Invalid state is used internally only"),
190        }
191    }
192
193    /// Empties the entire store, deleting all sessions.
194    pub async fn clear_store(
195        &self,
196        connection: &mut SessionStoreConnection,
197    ) -> Result<(), Error<SessionStoreConnection::Error>> {
198        connection.clear().await
199    }
200
201    /// Get a session from the storage backend.
202    ///
203    /// The `cookie_value` is the value of a cookie identifying the session.
204    ///
205    /// The return value is `Ok(Some(_))` if there is a session identified by the given cookie that is not expired,
206    /// or `Ok(None)` if there is no such session that is not expired.
207    pub async fn load_session(
208        &self,
209        cookie_value: impl AsRef<str>,
210        connection: &mut SessionStoreConnection,
211    ) -> Result<Option<Session<SessionData>>, Error<SessionStoreConnection::Error>> {
212        if cookie_value.as_ref().as_bytes().len() != CookieGenerator::COOKIE_LENGTH {
213            return Err(Error::WrongCookieLength {
214                expected: CookieGenerator::COOKIE_LENGTH,
215                actual: cookie_value.as_ref().as_bytes().len(),
216            });
217        }
218
219        let session_id = SessionId::from_cookie_value(cookie_value.as_ref());
220        if let Some(mut session) = connection.read_session(session_id).await? {
221            let now = Utc::now();
222            if session.is_expired(now) {
223                // We could delete expired sessions here, but that does not make sense:
224                // the client will not purposefully send us an expired session cookie, so only in the unlikely
225                // event that the session expires while being transmitted this will actually be triggered.
226                return Ok(None);
227            }
228
229            self.session_renewal_strategy
230                .apply_to_session(&mut session, now);
231
232            Ok(Some(session))
233        } else {
234            Ok(None)
235        }
236    }
237}
238
239impl<SessionData, SessionStoreConnection, CookieGenerator: Clone> Clone
240    for SessionStore<SessionData, SessionStoreConnection, CookieGenerator>
241{
242    fn clone(&self) -> Self {
243        Self {
244            cookie_generator: self.cookie_generator.clone(),
245            session_renewal_strategy: self.session_renewal_strategy,
246            data: self.data,
247            connection: self.connection,
248        }
249    }
250}
251
252/// This is the backend-facing interface of the session store.
253/// It defines simple [CRUD]-methods on sessions.
254///
255/// Sessions are identified by a session id (`current_id`).
256/// The session store must ensure that there is never any overlap between the ids.
257///
258/// [CRUD]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
259#[async_trait]
260pub trait SessionStoreConnector<SessionData> {
261    /// The error type of this connector.
262    type Error: Debug;
263
264    /// Writing a session may fail if the session id already exists.
265    /// This constant indicates how often the caller should retry with different randomly generated ids until it should give up.
266    /// The value `None` indicates that the caller should never give up, possibly looping infinitely.
267    fn maximum_retries_on_id_collision(&self) -> Option<u32>;
268
269    /// Create a session with the given `current_id`, `expiry` and `data`.
270    async fn create_session(
271        &mut self,
272        current_id: &SessionId,
273        expiry: &SessionExpiry,
274        data: &SessionData,
275    ) -> Result<WriteSessionResult, Error<Self::Error>>;
276
277    /// Read the session with the given `id`.
278    async fn read_session(
279        &mut self,
280        id: SessionId,
281    ) -> Result<Option<Session<SessionData>>, Error<Self::Error>>;
282
283    /// Update a session with new ids, data and expiry.
284    ///
285    /// This method must be implemented as follows:
286    ///  1. Find the session `A` identified by the given `previous_id`.
287    ///  2. Remap `A` to be identified by `current_id` instead of `previous_id`.
288    ///  3. Set `A.expiry = expiry` and `A.data = data`.
289    ///
290    /// **Security:** To avoid race conditions, this method must not allow concurrent updates of a session id.
291    /// It must never happen that by updating a session id `X` concurrently, there are suddenly two different session ids `Y` and `Z`, both stemming from `X`.
292    /// Instead, one of the updates must fail.
293    async fn update_session(
294        &mut self,
295        current_id: &SessionId,
296        previous_id: &SessionId,
297        expiry: &SessionExpiry,
298        data: &SessionData,
299    ) -> Result<WriteSessionResult, Error<Self::Error>>;
300
301    /// Delete the session with the given `id`.
302    async fn delete_session(&mut self, id: &SessionId) -> Result<(), Error<Self::Error>>;
303
304    /// Delete all sessions in the store.
305    async fn clear(&mut self) -> Result<(), Error<Self::Error>>;
306}
307
308/// The result of writing a session, indicating if the session could be written, or if the id collided.
309/// Annotated with `#[must_use]`, because silently dropping this may cause sessions to be dropped silently.
310#[derive(Debug)]
311#[must_use]
312pub enum WriteSessionResult<OkData = ()> {
313    /// The session could be written without id collision.
314    Ok(OkData),
315    /// The session could not be written, because the chosen id already exists.
316    SessionIdExists,
317}
318
319impl<OkData> WriteSessionResult<OkData> {
320    fn map<OtherOkData>(
321        self,
322        f: impl FnOnce(OkData) -> OtherOkData,
323    ) -> WriteSessionResult<OtherOkData> {
324        match self {
325            Self::Ok(data) => WriteSessionResult::Ok(f(data)),
326            Self::SessionIdExists => WriteSessionResult::SessionIdExists,
327        }
328    }
329}
330
331/// Indicates if the client's session cookie should be updated.
332/// Annotated with `#[must_use]`, because silently dropping this
333/// very likely indicates that the communication of the session to the client was forgotten about.
334#[derive(Debug, Eq, PartialEq)]
335#[must_use]
336pub enum SessionCookieCommand {
337    /// Set or update the session cookie.
338    Set {
339        /// The value of the session cookie.
340        cookie_value: String,
341        /// The expiry time of the session cookie.
342        expiry: SessionExpiry,
343    },
344    /// Delete the session cookie.
345    Delete,
346    /// Do not inform the client about any updates to the session cookie.
347    /// This means that the cookie stayed the same.
348    DoNothing,
349}
350
351impl SessionRenewalStrategy {
352    fn apply_to_session<SessionData: Debug>(
353        &self,
354        session: &mut Session<SessionData>,
355        now: DateTime<Utc>,
356    ) {
357        match self {
358            SessionRenewalStrategy::Ignore => { /* do nothing */ }
359            SessionRenewalStrategy::AutomaticRenewal {
360                time_to_live,
361                maximum_remaining_time_to_live_for_renewal,
362            } => {
363                let new_expiry = now + *time_to_live;
364                match *session.expiry() {
365                    SessionExpiry::DateTime(old_expiry) => {
366                        // Renew only if within maximum remaining time.
367                        if old_expiry - now <= *maximum_remaining_time_to_live_for_renewal {
368                            session.set_expiry(new_expiry);
369                        }
370                    }
371                    // Always renew if the expiry is set to never, otherwise the session will never expire.
372                    SessionExpiry::Never => session.set_expiry(new_expiry),
373                }
374            }
375        }
376    }
377}