typed_session/lib.rs
1//! # Async strongly-typed HTTP sessions.
2//!
3//! This crate provides a session handling mechanism with abstract session store (typically a database).
4//! The crate is not meant to be used directly, but should be wrapped into middleware for your
5//! web-framework of choice.
6//!
7//! This crate handles all the typical plumbing associated with session handling, including:
8//! * change tracking,
9//! * expiry and automatic renewal and
10//! * generation of session ids.
11//!
12//! On the "front-end" of this crate, the [`SessionStore`] provides a simple interface
13//! to load and store sessions given an identifying string, typically the value of a cookie.
14//! The [`Session`] type has a type parameter `SessionData` that decides what session-specific
15//! data is stored in the database.
16//! The user on the front-end is responsible for communicating session cookies to the client
17//! by performing the [`SessionCookieCommand`] returned by [`SessionStore::store_session`].
18//!
19//! On the "back-end" of this crate, the trait [`SessionStoreConnector`]
20//! expects a simple [*CRUD*](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)-based
21//! interface for handling sessions in a database.
22//!
23//! ## Change tracking
24//!
25//! Changes are tracked automatically in an efficient way.
26//! If a client has no session or an invalid session, a new session is created for that client.
27//! However, only if the session contains meaningful data it is stored and communicated to the client.
28//! Session data is assumed to be meaningful when it has been accessed mutably or the session was explicitly constructed with non-default data.
29//! Mutably accessing or mutating the expiry is not considered enough for the session to actually be stored.
30//!
31//! Once the session is stored, if either the data or expiry is accessed mutably by a future request, it is updated.
32//! Each update generates a new session id to prevent simultaneous updates of the same session from producing unexpected results.
33//! If the session is not updated, then we neither touch the session store, nor do we communicate any session cookie to the client.
34//!
35//! ## Session expiry
36//!
37//! Session expiry is only checked when the session is loaded from the store. If it is expired, the
38//! store ignores it and returns no session.
39//! The session's expiry can be updated manually, or automatically with a [`SessionRenewalStrategy`].
40//! In case the session is renewed automatically, the session may be updated by the session store,
41//! even if neither its data nor expiry was accessed mutably.
42//!
43//! Note that **expired sessions are not deleted** from the session store. This is left to a background
44//! job that needs to be set up independently of this crate. Also, expired cookies are not deleted,
45//! it is left to the browser to take care of that.
46//!
47//! ## Manual session removal
48//!
49//! While expiry does not actually delete sessions, but leaves this up to external jobs, sessions can
50//! be deleted from both the session store and the client using the [`Session::delete`] function.
51//! This marks the session for deletion, such that it is deleted from the store when [`SessionStore::store_session`]
52//! is called. The return value of `store_session` is then [`SessionCookieCommand::Delete`],
53//! indicating to the web framework to set the `Set-Cookie` header such that the cookie is deleted.
54//!
55//! ## Security
56//!
57//! Sessions are identified by an alphanumeric string including upper and lower case letters from the
58//! English alphabet. This gives `log_2(26+26+10) ≥ 5.95` bits of entropy per character. The default
59//! cookie length is `32`, resulting in `log_2(26+26+10) * 32 ≥ 190` bits of entropy.
60//! [The OWASP® Foundation](https://owasp.org) recommends session ids to have at least
61//! [`128` bits of entropy](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy).
62//! That is, `64` bits of actual entropy, where a good PRNG is assumed to produce `0.5` bits of entropy per bit.
63//! The random source used by default is [`rand::rngs::ThreadRng`], from the [rand] crate, which is secure.
64//!
65//! Session data is stored only in the session store along with a hashed session id, while the client
66//! only stores the unhashed session id.
67//!
68//! Note that the OWASP® Foundation does not require session ids to be hashed. We anyways use the
69//! fast and secure hash function provided by crate [blake3] for additional security in case the
70//! session store gets compromised.
71//!
72//! This crate updates the session id whenever the session data has changed or the session is expired.
73//! The session id update must be supported by the session store backend in a way that does not allow
74//! session branching, i.e. the creation of two different sessions through the simultaneous update of
75//! a single session.
76//! See [`SessionStoreConnector::update_session`] for more details.
77//!
78//! ## Example
79//!
80//! ```
81//! use typed_session::{Session, SessionStore, MemoryStore};
82//!
83//! # use typed_session::{Error, SessionCookieCommand, SessionRenewalStrategy};
84//! # use std::convert::Infallible;
85//! # fn main() -> Result<(), Error<Infallible>> {
86//! use rand::thread_rng;
87//! # async_std::task::block_on(async {
88//! #
89//! // Initialise a new database connection.
90//! // This is used by the session store to load and store sessions.
91//! // In this example, we use the debug session store `MemoryStore` directly as a database connection,
92//! // but usually you would pass some type wrapping e.g. a connection to Postgres or Redis.
93//! let mut connection = MemoryStore::new();
94//!
95//! // Init a new session store we can persist sessions to.
96//! let mut store = SessionStore::new(SessionRenewalStrategy::Ignore);
97//!
98//! // Create and store a new session.
99//! // The session can hold arbitrary data, but session stores are type safe,
100//! // i.e. all sessions must hold data of the same type.
101//! // Use e.g. an enum to distinguish session states like "anonymous" or "logged-in as user X".
102//! let session = Session::new_with_data(15);
103//! let SessionCookieCommand::Set { cookie_value, .. } = store.store_session(session, &mut connection)
104//! .await? else { unreachable!("New sessions without expiry always set the cookie") };
105//! // The set_cookie_command contains the cookie value and the expiry to be sent to the client.
106//!
107//! // Retrieve the session using the cookie.
108//! let session = store.load_session(cookie_value, &mut connection).await?.unwrap();
109//! assert_eq!(*session.data(), 15);
110//! #
111//! # Ok(()) }) }
112//! ```
113//!
114//! ## Debugging
115//!
116//! To aid in debugging, this crate offers a debug backend implementation called [`MemoryStore`]
117//! under the feature flag `memory-store`.
118//!
119//! ## Comparison with crate [async-session](https://crates.io/crates/async-session)
120//!
121//! This crate was designed after `async-session`. The main difference is that the session data is
122//! generic, such that the user can plug in any type they want. If needed, the same `HashMap<String, String>`
123//! can be used as session data type.
124
125#![forbid(unsafe_code)]
126#![warn(
127 future_incompatible,
128 missing_debug_implementations,
129 nonstandard_style,
130 missing_docs,
131 unreachable_pub,
132 missing_copy_implementations,
133 unused_qualifications
134)]
135
136mod error;
137#[cfg(feature = "memory-store")]
138mod memory_store;
139mod session;
140mod session_store;
141
142pub use error::Error;
143#[cfg(feature = "memory-store")]
144pub use memory_store::{
145 DefaultLogger, MemoryStore, MemoryStoreOperationLogger, NoLogger, Operation,
146};
147pub use session::{Session, SessionExpiry, SessionId, SessionIdType};
148pub use session_store::{
149 cookie_generator::{
150 DebugSessionCookieGenerator, DefaultSessionCookieGenerator, SessionCookieGenerator,
151 },
152 SessionCookieCommand, SessionRenewalStrategy, SessionStore, SessionStoreConnector,
153 WriteSessionResult,
154};