tower_sessions/
lib.rs

1//! # Overview
2//!
3//! This crate provides sessions, key-value pairs associated with a site
4//! visitor, as a [`tower`](https://docs.rs/tower/latest/tower/) middleware.
5//!
6//! It offers:
7//!
8//! - **Pluggable Storage Backends:** Bring your own backend simply by
9//!   implementing the [`SessionStore`] trait, fully decoupling sessions from
10//!   their storage.
11//! - **Minimal Overhead**: Sessions are only loaded from their backing stores
12//!   when they're actually used and only in e.g. the handler they're used in.
13//!   That means this middleware can be installed at any point in your route
14//!   graph with minimal overhead.
15//! - **An `axum` Extractor for [`Session`]:** Applications built with `axum`
16//!   can use `Session` as an extractor directly in their handlers. This makes
17//!   using sessions as easy as including `Session` in your handler.
18//! - **Simple Key-Value Interface:** Sessions offer a key-value interface that
19//!   supports native Rust types. So long as these types are `Serialize` and can
20//!   be converted to JSON, it's straightforward to insert, get, and remove any
21//!   value.
22//! - **Strongly-Typed Sessions:** Strong typing guarantees are easy to layer on
23//!   top of this foundational key-value interface.
24//!
25//! This crate's session implementation is inspired by the [Django sessions middleware](https://docs.djangoproject.com/en/4.2/topics/http/sessions) and it provides a transliteration of those semantics.
26//! ### Session stores
27//!
28//! Session data persistence is managed by user-provided types that implement
29//! [`SessionStore`]. What this means is that applications can and should
30//! implement session stores to fit their specific needs.
31//!
32//! That said, a number of session store implmentations already exist and may be
33//! useful starting points.
34//!
35//! | Crate                                                                                                            | Persistent | Description                                |
36//! | ---------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------ |
37//! | [`tower-sessions-dynamodb-store`](https://github.com/necrobious/tower-sessions-dynamodb-store)                   | Yes        | DynamoDB session store                     |
38//! | [`tower-sessions-file-store`](https://github.com/mousetail/tower-sessions-file-store)                            | Yes        | Local filesystem store                     |
39//! | [`tower-sessions-firestore-store`](https://github.com/AtTheTavern/tower-sessions-firestore-store)                | Yes        | Firestore session store                    |
40//! | [`tower-sessions-libsql-store`](https://github.com/daybowbow-dev/tower-sessions-libsql-store)                    | Yes        | libSQL session store                       |
41//! | [`tower-sessions-mongodb-store`](https://github.com/maxcountryman/tower-sessions-stores/tree/main/mongodb-store) | Yes        | MongoDB session store                      |
42//! | [`tower-sessions-moka-store`](https://github.com/maxcountryman/tower-sessions-stores/tree/main/moka-store)       | No         | Moka session store                         |
43//! | [`tower-sessions-redis-store`](https://github.com/maxcountryman/tower-sessions-stores/tree/main/redis-store)     | Yes        | Redis via `fred` session store             |
44//! | [`tower-sessions-rusqlite-store`](https://github.com/patte/tower-sessions-rusqlite-store)                        | Yes        | Rusqlite session store                     |
45//! | [`tower-sessions-sled-store`](https://github.com/Zatzou/tower-sessions-sled-store)                               | Yes        | Sled session store                         |
46//! | [`tower-sessions-sqlx-store`](https://github.com/maxcountryman/tower-sessions-stores/tree/main/sqlx-store)       | Yes        | SQLite, Postgres, and MySQL session stores |
47//! | [`tower-sessions-surrealdb-store`](https://github.com/rynoV/tower-sessions-surrealdb-store)                      | Yes        | SurrealDB session store                    |
48//!
49//! Have a store to add? Please open a PR adding it.
50//!
51//! ### User session management
52//!
53//! To facilitate authentication and authorization, we've built [`axum-login`](https://github.com/maxcountryman/axum-login) on top of this crate. Please check it out if you're looking for a generalized auth solution.
54//!
55//! # Usage with an `axum` application
56//!
57//! A common use-case for sessions is when building HTTP servers. Using `axum`,
58//! it's straightforward to leverage sessions.
59//!
60//! ```rust,no_run
61//! use std::net::SocketAddr;
62//!
63//! use axum::{response::IntoResponse, routing::get, Router};
64//! use serde::{Deserialize, Serialize};
65//! use time::Duration;
66//! use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer};
67//!
68//! const COUNTER_KEY: &str = "counter";
69//!
70//! #[derive(Default, Deserialize, Serialize)]
71//! struct Counter(usize);
72//!
73//! async fn handler(session: Session) -> impl IntoResponse {
74//!     let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
75//!     session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
76//!     format!("Current count: {}", counter.0)
77//! }
78//!
79//! #[tokio::main]
80//! async fn main() {
81//!     let session_store = MemoryStore::default();
82//!     let session_layer = SessionManagerLayer::new(session_store)
83//!         .with_secure(false)
84//!         .with_expiry(Expiry::OnInactivity(Duration::seconds(10)));
85//!
86//!     let app = Router::new().route("/", get(handler)).layer(session_layer);
87//!
88//!     let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
89//!     let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
90//!     axum::serve(listener, app.into_make_service())
91//!         .await
92//!         .unwrap();
93//! }
94//! ```
95//!
96//! ## Session expiry management
97//!
98//! In cases where you are utilizing stores that lack automatic session expiry
99//! functionality, such as SQLx or MongoDB stores, it becomes essential to
100//! periodically clean up stale sessions. For instance, both SQLx and MongoDB
101//! stores offer
102//! `continuously_delete_expired`
103//! which is designed to be executed as a recurring task. This process ensures
104//! the removal of expired sessions, maintaining your application's data
105//! integrity and performance.
106//! ```rust,no_run,ignore
107//! # use tower_sessions::{session_store::ExpiredDeletion};
108//! # use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore};
109//! # tokio_test::block_on(async {
110//! let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
111//! let session_store = SqliteStore::new(pool);
112//! let deletion_task = tokio::task::spawn(
113//!     session_store
114//!         .clone()
115//!         .continuously_delete_expired(tokio::time::Duration::from_secs(60)),
116//! );
117//! deletion_task.await.unwrap().unwrap();
118//! # });
119//! ```
120//!
121//! Note that by default or when using browser session expiration, sessions are
122//! considered expired after two weeks.
123//!
124//! # Extractor pattern
125//!
126//! When using `axum`, the [`Session`] will already function as an extractor.
127//! It's possible to build further on this to create extractors of custom types.
128//! ```rust,no_run
129//! # use async_trait::async_trait;
130//! # use axum::extract::FromRequestParts;
131//! # use http::{request::Parts, StatusCode};
132//! # use serde::{Deserialize, Serialize};
133//! # use tower_sessions::{SessionStore, Session, MemoryStore};
134//! const COUNTER_KEY: &str = "counter";
135//!
136//! #[derive(Default, Deserialize, Serialize)]
137//! struct Counter(usize);
138//!
139//! impl<S> FromRequestParts<S> for Counter
140//! where
141//!     S: Send + Sync,
142//! {
143//!     type Rejection = (http::StatusCode, &'static str);
144//!
145//!     async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
146//!         let session = Session::from_request_parts(req, state).await?;
147//!         let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
148//!         session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
149//!
150//!         Ok(counter)
151//!     }
152//! }
153//! ```
154//!
155//! Now in our handler, we can use `Counter` directly to read its fields.
156//!
157//! A complete example can be found in [`examples/counter-extractor.rs`](https://github.com/maxcountryman/tower-sessions/blob/main/examples/counter-extractor.rs).
158//!
159//! # Strongly-typed sessions
160//!
161//! The extractor pattern can be extended further to provide strong typing
162//! guarantees over the key-value substrate. Whereas our previous extractor
163//! example was effectively read-only. This pattern enables mutability of the
164//! underlying structure while also leveraging the full power of the type
165//! system.
166//! ```rust,no_run
167//! # use async_trait::async_trait;
168//! # use axum::extract::FromRequestParts;
169//! # use http::{request::Parts, StatusCode};
170//! # use serde::{Deserialize, Serialize};
171//! # use time::OffsetDateTime;
172//! # use tower_sessions::{SessionStore, Session};
173//! #[derive(Clone, Deserialize, Serialize)]
174//! struct GuestData {
175//!     pageviews: usize,
176//!     first_seen: OffsetDateTime,
177//!     last_seen: OffsetDateTime,
178//! }
179//!
180//! impl Default for GuestData {
181//!     fn default() -> Self {
182//!         Self {
183//!             pageviews: 0,
184//!             first_seen: OffsetDateTime::now_utc(),
185//!             last_seen: OffsetDateTime::now_utc(),
186//!         }
187//!     }
188//! }
189//!
190//! struct Guest {
191//!     session: Session,
192//!     guest_data: GuestData,
193//! }
194//!
195//! impl Guest {
196//!     const GUEST_DATA_KEY: &'static str = "guest_data";
197//!
198//!     fn first_seen(&self) -> OffsetDateTime {
199//!         self.guest_data.first_seen
200//!     }
201//!
202//!     fn last_seen(&self) -> OffsetDateTime {
203//!         self.guest_data.last_seen
204//!     }
205//!
206//!     fn pageviews(&self) -> usize {
207//!         self.guest_data.pageviews
208//!     }
209//!
210//!     async fn mark_pageview(&mut self) {
211//!         self.guest_data.pageviews += 1;
212//!         Self::update_session(&self.session, &self.guest_data).await
213//!     }
214//!
215//!     async fn update_session(session: &Session, guest_data: &GuestData) {
216//!         session
217//!             .insert(Self::GUEST_DATA_KEY, guest_data.clone())
218//!             .await
219//!             .unwrap()
220//!     }
221//! }
222//!
223//! impl<S> FromRequestParts<S> for Guest
224//! where
225//!     S: Send + Sync,
226//! {
227//!     type Rejection = (StatusCode, &'static str);
228//!
229//!     async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
230//!         let session = Session::from_request_parts(req, state).await?;
231//!
232//!         let mut guest_data: GuestData = session
233//!             .get(Self::GUEST_DATA_KEY)
234//!             .await
235//!             .unwrap()
236//!             .unwrap_or_default();
237//!
238//!         guest_data.last_seen = OffsetDateTime::now_utc();
239//!
240//!         Self::update_session(&session, &guest_data).await;
241//!
242//!         Ok(Self {
243//!             session,
244//!             guest_data,
245//!         })
246//!     }
247//! }
248//! ```
249//!
250//! Here we can use `Guest` as an extractor in our handler. We'll be able to
251//! read values, like the ID as well as update the pageview count with our
252//! `mark_pageview` method.
253//!
254//! A complete example can be found in [`examples/strongly-typed.rs`](https://github.com/maxcountryman/tower-sessions/blob/main/examples/strongly-typed.rs)
255//!
256//! ## Name-spaced and strongly-typed buckets
257//!
258//! Our example demonstrates a single extractor, but in a real application we
259//! might imagine a set of common extractors, all living in the same session.
260//! Each extractor forms a kind of bucketed name-space with a typed structure.
261//! Importantly, each is self-contained by its own name-space.
262//!
263//! For instance, we might also have a site preferences bucket, an analytics
264//! bucket, a feature flag bucket and so on. All these together would live in
265//! the same session, but would be segmented by their own name-space, avoiding
266//! the mixing of domains unnecessarily.[^data-domains]
267//!
268//! # Layered caching
269//!
270//! In some cases, the canonical store for a session may benefit from a cache.
271//! For example, rather than loading a session from a store on every request,
272//! this roundtrip can be mitigated by placing a cache in front of the storage
273//! backend. A specialized session store, [`CachingSessionStore`], is provided
274//! for exactly this purpose.
275//!
276//! This store manages a cache and a store. Where the cache acts as a frontend
277//! and the store a backend. When a session is loaded, the store first attempts
278//! to load the session from the cache, if that fails only then does it try to
279//! load from the store. By doing so, read-heavy workloads will incur far fewer
280//! roundtrips to the store itself.
281//!
282//! To illustrate, this is how we might use the
283//! `MokaStore` as a frontend cache to a
284//! `PostgresStore` backend.
285//! ```rust,no_run,ignore
286//! # use tower::ServiceBuilder;
287//! # use tower_sessions::{CachingSessionStore, SessionManagerLayer};
288//! # use tower_sessions_sqlx_store::{sqlx::PgPool, PostgresStore};
289//! # use tower_sessions_moka_store::MokaStore;
290//! # use time::Duration;
291//! # tokio_test::block_on(async {
292//! let database_url = std::option_env!("DATABASE_URL").unwrap();
293//! let pool = PgPool::connect(database_url).await.unwrap();
294//!
295//! let postgres_store = PostgresStore::new(pool);
296//! postgres_store.migrate().await.unwrap();
297//!
298//! let moka_store = MokaStore::new(Some(10_000));
299//! let caching_store = CachingSessionStore::new(moka_store, postgres_store);
300//!
301//! let session_service = ServiceBuilder::new()
302//!     .layer(SessionManagerLayer::new(caching_store).with_max_age(Duration::days(1)));
303//! # })
304//! ```
305//!
306//! While this example uses Moka, any implementor of [`SessionStore`] may be
307//! used. For instance, we could use the `RedisStore` instead of Moka.
308//!
309//! A cache is most helpful with read-heavy workloads, where the cache hit rate
310//! will be high. This is because write-heavy workloads will require a roundtrip
311//! to the store and therefore benefit less from caching.
312//!
313//! ## Data races under concurrent conditions
314//!
315//! Please note that it is **not safe** to access and mutate session state
316//! concurrently: this will result in data loss if your mutations are dependent
317//! on the state of the session.
318//!
319//! This is because a session is loaded first from its backing store. Once
320//! loaded it's possible for a second request to load the same session, but
321//! without the inflight changes the first request may have made.
322//!
323//! # Implementation
324//!
325//! Sessions are composed of three pieces:
326//!
327//! 1. A cookie that holds the session ID as its value,
328//! 2. An in-memory hash-map, which underpins the key-value API,
329//! 3. A pluggable persistence layer, the session store, where session data is
330//!    housed.
331//!
332//! Together, these pieces form the basis of this crate and allow `tower` and
333//! `axum` applications to use a familiar session interface.
334//!
335//! ## Cookie
336//!
337//! Sessions manifest to clients as cookies. These cookies have a configurable
338//! name and a value that is the session ID. In other words, cookies hold a
339//! pointer to the session in the form of an ID. This ID is an i128 generated by
340//! the [`rand`](https://docs.rs/rand/latest/rand) crate.
341//!
342//! ### Secure nature of cookies
343//!
344//! Session IDs are considered secure if sent over encrypted channels. Note that
345//! this assumption is predicated on the secure nature of the [`rand`](https://docs.rs/rand/latest/rand) crate
346//! and its ability to generate securely-random values using the ChaCha block
347//! cipher with 12 rounds. It's also important to note that session cookies
348//! **must never** be sent over a public, insecure channel. Doing so is **not**
349//! secure and will lead to compromised sessions!
350//!
351//! Additionally, sessions may be optionally signed or encrypted by enabling the
352//! `signed` and `private` feature flags, respectively. When enabled, the
353//! [`with_signed`](SessionManagerLayer::with_signed) and
354//! [`with_private`](SessionManagerLayer::with_private) methods become
355//! available. These methods take a cryptographic key which allows the session
356//! manager to leverage ciphertext as opposed to the default of plaintext. Note
357//! that no data is stored in the session ID beyond the session identifier
358//! itself and so this measure should be considered primarily effective as a
359//! defense in depth tactic.
360//!
361//! ## Key-value API
362//!
363//! Sessions manage a `HashMap<String, serde_json::Value>` but importantly are
364//! transparently persisted to an arbitrary storage backend. Effectively,
365//! `HashMap` is an intermediary, in-memory representation. By using a map-like
366//! structure, we're able to present a familiar key-value interface for managing
367//! sessions. This allows us to store and retrieve native Rust types, so long as
368//! our type is `impl Serialize` and can be represented as JSON.[^json]
369//!
370//! Internally, this hash map state is protected by a lock in the form of
371//! `Mutex`. This allows us to safely share mutable state across thread
372//! boundaries. Note that this lock is only acquired when we read from or write
373//! to this inner session state and not used when the session is provided to the
374//! request. This means that lock contention is minimized for most use
375//! cases.[^lock-contention]
376//!
377//! ## Session store
378//!
379//! Sessions are serialized to arbitrary storage backends via a session record
380//! intermediary. Implementations of `SessionStore` take a record and persist
381//! it such that it can later be loaded via the session ID.
382//!
383//! Three components are needed for storing a session:
384//!
385//! 1. The session ID.
386//! 2. The session expiry.
387//! 3. The session data itself.
388//!
389//! Together, these compose the session record and are enough to both encode and
390//! decode a session from any backend.
391//!
392//! ## Session life cycle
393//!
394//! Cookies hold a pointer to the session, rather than the session's data, and
395//! because of this, the `tower` middleware is focused on managing the process
396//! of initializing a session which can later be used in code to transparently
397//! interact with the store.
398//!
399//! A session is initialized by looking for a cookie that matches the configured
400//! session cookie name. If no such cookie is found or a cookie is found but is
401//! malformed, an empty session is initialized.
402//!  
403//! Modified sessions will invoke the session's [`save`](Session::save) method
404//! as well as append to the `Set-Cookie` header of the response.
405//!
406//! Empty sessions are considered deleted and will set a removal cookie
407//! on the response but are not removed from the store directly.
408//!
409//! Sessions also carry with them a configurable expiry and will be removed in
410//! accordance with this.
411//!
412//! Notably, the session life cycle minimizes overhead with the store. All
413//! session store methods are deferred until the point [`Session`] is used in
414//! code and more specifically one of its methods requiring the store is called.
415//!
416//! [^json]: Using JSON allows us to translate arbitrary types to virtually
417//! any backend and gives us a nice interface with which to interact with the
418//! session.
419//!
420//! [^lock-contention]: We might consider replacing `Mutex` with `RwLock` if
421//! this proves to be a better fit in practice. Another alternative might be
422//! `dashmap` or a different approach entirely. Future iterations should be
423//! based on real-world use cases.
424//!
425//! [^data-domains]: This is particularly useful when we may have data
426//! domains that only belong with ! users in certain states: we can pull these
427//! into our handlers where we need a particular domain. In this way, we
428//! minimize data pollution via self-contained domains in the form of buckets.
429#![warn(
430    clippy::all,
431    nonstandard_style,
432    future_incompatible,
433    missing_debug_implementations
434)]
435#![deny(missing_docs)]
436#![forbid(unsafe_code)]
437#![cfg_attr(docsrs, feature(doc_cfg))]
438
439pub use tower_cookies::cookie;
440pub use tower_sessions_core::{session, session_store};
441#[doc(inline)]
442pub use tower_sessions_core::{
443    session::{Expiry, Session},
444    session_store::{CachingSessionStore, ExpiredDeletion, SessionStore},
445};
446#[cfg(feature = "memory-store")]
447#[cfg_attr(docsrs, doc(cfg(feature = "memory-store")))]
448#[doc(inline)]
449pub use tower_sessions_memory_store::MemoryStore;
450
451pub use crate::service::{SessionManager, SessionManagerLayer};
452
453pub mod service;