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

#![forbid(unsafe_code)]
#![warn(
    future_incompatible,
    missing_debug_implementations,
    nonstandard_style,
    missing_docs,
    unreachable_pub,
    missing_copy_implementations,
    unused_qualifications
)]

mod error;
#[cfg(feature = "memory-store")]
mod memory_store;
mod session;
mod session_store;

pub use error::Error;
#[cfg(feature = "memory-store")]
pub use memory_store::{
    DefaultLogger, MemoryStore, MemoryStoreOperationLogger, NoLogger, Operation,
};
pub use session::{Session, SessionExpiry, SessionId, SessionIdType};
pub use session_store::{
    cookie_generator::{
        DebugSessionCookieGenerator, DefaultSessionCookieGenerator, SessionCookieGenerator,
    },
    SessionCookieCommand, SessionRenewalStrategy, SessionStore, SessionStoreConnector,
    WriteSessionResult,
};