Skip to main content

session_guard/
lib.rs

1//! Stolen session cookies become useless: rotating tokens with reuse detection and risk-based
2//! step-up for Rust web apps.
3//!
4//! A [`Guard`] issues opaque session tokens, stores only their SHA-256 hashes, rotates them
5//! every few minutes and revokes the session when a superseded token comes back after its
6//! successor was used: the signature of a copied cookie. Every request is also scored against
7//! the session's client fingerprint (OS, browser, client hints, country, network); a moderate
8//! change asks the app to re-verify the subject, a drastic one revokes the session.
9//!
10//! The crate handles sessions only. Logging in, verifying passwords and rate limiting stay in
11//! the app.
12//!
13//! ```
14//! # async fn demo() -> Result<(), session_guard::GuardError> {
15//! use session_guard::{ClientContext, Guard, MemoryStore, Outcome};
16//!
17//! let guard = Guard::new(MemoryStore::new());
18//! let ctx = ClientContext::new("203.0.113.7".parse().unwrap()).with_user_agent("Mozilla/5.0 …");
19//!
20//! // After the app has verified the user's credentials:
21//! let user = uuid::Uuid::new_v4();
22//! let session = guard.login(&user, &ctx, None).await?;
23//! let set_cookie = session.token.set_cookie(); // send as `Set-Cookie`
24//! # let _ = set_cookie;
25//!
26//! // On every later request:
27//! match guard.authenticate(session.token.expose(), &ctx).await? {
28//!     Outcome::Authenticated(auth) => {
29//!         assert_eq!(auth.session.subject, user);
30//!         // send `auth.rotated` as `Set-Cookie` when present
31//!     }
32//!     Outcome::Rejected(rejection) => {
33//!         // answer 401 with `{"error": rejection.as_str()}`
34//!         # let _ = rejection;
35//!     }
36//! }
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! # Features
42//!
43//! - `axum`: a tower layer that authenticates requests, the `AuthSession` and `Sessions`
44//!   extractors and response impls (see the `axum` module). Enables `tokio`.
45//! - `aide`: OpenAPI docs through `aide` and `rovo`: the `session_cookie` security scheme
46//!   (see the `aide` module).
47//! - `tokio`: `Guard::spawn_purge`, a background task that purges expired sessions.
48//! - `postgres`: a [`SessionStore`] on sqlx with embedded migrations.
49//! - `valkey`: a [`SessionStore`] on Valkey or Redis.
50//! - `maxmind`: [`IpIntel`] backed by `MaxMind` `GeoLite2`/`GeoIP2` databases.
51//! - `metrics`: counters through the `metrics` facade.
52//! - `testing`: the `store_conformance!` suite for custom stores.
53#![forbid(unsafe_code)]
54#![warn(missing_docs)]
55#![cfg_attr(docsrs, feature(doc_cfg))]
56
57#[cfg(feature = "aide")]
58#[cfg_attr(docsrs, doc(cfg(feature = "aide")))]
59pub mod aide;
60#[cfg(feature = "axum")]
61#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
62pub mod axum;
63pub mod checks;
64pub mod client;
65pub mod clock;
66pub mod config;
67pub mod cookie;
68pub mod error;
69pub mod event;
70pub mod fingerprint;
71mod guard;
72pub mod intel;
73pub mod risk;
74pub mod store;
75pub mod subject;
76mod telemetry;
77#[cfg(any(test, feature = "testing"))]
78#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
79pub mod testing;
80pub mod token;
81
82/// Internals the crate's own benchmarks measure. Not part of the public API; may change in any
83/// release.
84#[cfg(feature = "testing")]
85#[doc(hidden)]
86pub mod __bench {
87    use crate::{ClientContext, Fingerprint, IpInfo, fingerprint::UaCache};
88
89    /// Fingerprint capture through the guard's User-Agent cache.
90    #[derive(Debug)]
91    pub struct CachedCapture(UaCache);
92
93    impl CachedCapture {
94        /// A cache holding up to `capacity` parsed User-Agents.
95        pub fn new(capacity: usize) -> Self {
96            Self(UaCache::new(capacity))
97        }
98
99        /// [`Fingerprint::capture`], parsing the User-Agent through the cache.
100        pub fn capture(&self, ctx: &ClientContext, info: IpInfo) -> Fingerprint {
101            Fingerprint::capture_cached(ctx, info, &self.0)
102        }
103    }
104}
105
106#[cfg(feature = "axum")]
107#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
108pub use crate::axum::{GuardLayer, GuardService, Sessions};
109pub use checks::{Check, Checks, Decision, Thresholds, UnknownCheck};
110pub use client::{ClientContext, HeaderSource, canonical_ip, resolve_client_ip};
111pub use clock::{Clock, ManualClock, SystemClock};
112pub use config::{GuardConfig, GuardConfigBuilder, Mode, RotationConfig, StepUpPolicy};
113pub use cookie::{CookieConfig, SameSite, find_cookie};
114pub use error::{ConfigError, GuardError, StoreError};
115pub use event::{EventKind, SessionEvent};
116pub use fingerprint::{Baseline, Fingerprint, ip_prefix};
117pub use guard::{
118    AuthSession, Authenticated, Guard, GuardBuilder, IssuedToken, NewSession, Outcome, PurgeReport, Rejection,
119    SessionInfo, StepUpOutcome,
120};
121pub use intel::{IpInfo, IpIntel, NoIpIntel};
122#[cfg(feature = "maxmind")]
123#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
124pub use intel::{MaxMindDatabase, MaxMindError, MaxMindIntel};
125pub use risk::{Assessment, assess};
126#[cfg(feature = "postgres")]
127#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
128pub use store::PgStore;
129#[cfg(feature = "valkey")]
130#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
131pub use store::ValkeyStore;
132pub use store::{
133    MemoryStore, Rotation, SessionRecord, SessionStore, SessionUpdate, Supersede, TokenLookup, TokenRecord,
134};
135pub use subject::Subject;
136pub use token::{SessionToken, TOKEN_LEN, TokenHash, is_well_formed};
137
138// The README examples use these features, so other feature sets skip them.
139#[cfg(all(
140    doctest,
141    feature = "aide",
142    feature = "axum",
143    feature = "maxmind",
144    feature = "postgres",
145    feature = "testing"
146))]
147#[doc = include_str!("../README.md")]
148struct ReadmeDoctests;