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