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, anonymizers);
8//! a moderate 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, reloaded when
51//!   `geoipupdate` replaces the files.
52//! - `metrics`: counters through the `metrics` facade.
53//! - `testing`: the `store_conformance!` suite for custom stores.
54#![forbid(unsafe_code)]
55#![warn(missing_docs)]
56#![cfg_attr(docsrs, feature(doc_cfg))]
57
58#[cfg(feature = "aide")]
59#[cfg_attr(docsrs, doc(cfg(feature = "aide")))]
60pub mod aide;
61#[cfg(feature = "axum")]
62#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
63pub mod axum;
64pub mod checks;
65pub mod client;
66pub mod clock;
67pub mod config;
68pub mod cookie;
69pub mod error;
70pub mod event;
71pub mod fingerprint;
72mod guard;
73pub mod intel;
74pub mod risk;
75pub mod store;
76pub mod subject;
77mod telemetry;
78#[cfg(any(test, feature = "testing"))]
79#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
80pub mod testing;
81pub mod token;
82
83/// Internals the crate's own benchmarks measure. Not part of the public API; may change in any
84/// release.
85#[cfg(feature = "testing")]
86#[doc(hidden)]
87pub mod __bench {
88    use crate::{ClientContext, Fingerprint, IpInfo, fingerprint::UaCache};
89
90    /// Fingerprint capture through the guard's User-Agent cache.
91    #[derive(Debug)]
92    pub struct CachedCapture(UaCache);
93
94    impl CachedCapture {
95        /// A cache holding up to `capacity` parsed User-Agents.
96        pub fn new(capacity: usize) -> Self {
97            Self(UaCache::new(capacity))
98        }
99
100        /// [`Fingerprint::capture`], parsing the User-Agent through the cache.
101        pub fn capture(&self, ctx: &ClientContext, info: &IpInfo) -> Fingerprint {
102            Fingerprint::capture_cached(ctx, info, &self.0)
103        }
104    }
105}
106
107#[cfg(feature = "axum")]
108#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
109pub use crate::axum::{GuardLayer, GuardService, Sessions};
110pub use checks::{Check, Checks, Decision, Thresholds, UnknownCheck};
111pub use client::{ClientContext, HeaderSource, canonical_ip, resolve_client_ip};
112pub use clock::{Clock, ManualClock, SystemClock};
113pub use config::{GuardConfig, GuardConfigBuilder, Mode, RotationConfig, StepUpPolicy};
114pub use cookie::{CookieConfig, SameSite, find_cookie};
115pub use error::{ConfigError, GuardError, StoreError};
116pub use event::{ClientSummary, EventKind, SessionEvent, SessionOrigin};
117pub use fingerprint::{Baseline, Fingerprint, ip_prefix};
118pub use guard::{
119    AuthSession, Authenticated, Guard, GuardBuilder, IssuedToken, NewSession, Outcome, PurgeReport, Rejection,
120    SessionInfo, StepUpOutcome,
121};
122pub use intel::{Anonymity, IpInfo, IpIntel, NoIpIntel};
123#[cfg(feature = "maxmind")]
124#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
125pub use intel::{MaxMindDatabase, MaxMindError, MaxMindIntel};
126pub use risk::{Assessment, assess};
127#[cfg(feature = "postgres")]
128#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
129pub use store::PgStore;
130#[cfg(feature = "valkey")]
131#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
132pub use store::ValkeyStore;
133pub use store::{
134    ClaimScope, MemoryStore, Rotation, SessionRecord, SessionStore, SessionUpdate, Supersede, TokenLookup, TokenRecord,
135    session_bucket, token_bucket,
136};
137pub use subject::Subject;
138pub use token::{SessionToken, TOKEN_LEN, TokenHash, is_well_formed};
139
140// The README examples use these features, so other feature sets skip them.
141#[cfg(all(
142    doctest,
143    feature = "aide",
144    feature = "axum",
145    feature = "maxmind",
146    feature = "postgres",
147    feature = "testing"
148))]
149#[doc = include_str!("../README.md")]
150struct ReadmeDoctests;