Skip to main content

axum_security/cookie/
mod.rs

1//! Server-side cookie session management.
2//!
3//! This module provides [`CookieContext`], which manages sessions backed by a
4//! pluggable [`CookieStore`]. Sessions are identified by a cookie containing
5//! a unique [`SessionId`]. The session state lives server-side in your store.
6//!
7//! Use [`CookieContext`] as a Tower [`Layer`](tower::Layer) to automatically
8//! load sessions from incoming requests. Then extract [`CookieSession<S>`] in
9//! handlers to access the session data.
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use axum::{Router, routing::get};
15//! use axum_security::cookie::{CookieContext, CookieSession, MemStore};
16//!
17//! #[derive(Clone)]
18//! struct User { name: String }
19//!
20//! let cookie_ctx = CookieContext::builder()
21//!     .store(MemStore::new())
22//!     .build::<User>();
23//!
24//! let app = Router::new()
25//!     .route("/", get(|session: CookieSession<User>| async move {
26//!         format!("Hello, {}!", session.state.name)
27//!     }))
28//!     .layer(cookie_ctx);
29//! ```
30
31mod builder;
32mod expiry;
33mod id;
34mod service;
35mod session;
36mod store;
37
38use std::{borrow::Cow, convert::Infallible, sync::Arc};
39
40use axum::{
41    extract::{FromRef, FromRequestParts},
42    http::{HeaderMap, request::Parts},
43    response::Response,
44};
45
46pub use builder::CookieSessionBuilder;
47pub use id::SessionId;
48pub use session::CookieSession;
49pub use store::{CookieStore, MemStore};
50
51pub use cookie_monster::{Cookie, CookieBuilder, CookieJar, Expires, SameSite};
52use tokio::task::JoinHandle;
53
54use crate::{cookie::store::ErasedStore, utils::utc_now};
55
56/// Manages server-side sessions identified by a cookie.
57///
58/// Construct with [`CookieContext::builder`]. Use as a Tower [`Layer`](tower::Layer)
59/// to load sessions from incoming requests, then extract [`CookieSession<S>`] in handlers.
60///
61/// Also implements [`FromRequestParts`] (via [`FromRef`]) so you can extract it
62/// in handlers to create or remove sessions.
63pub struct CookieContext<S>(Arc<CookieContextInner<S>>);
64
65struct CookieContextInner<S> {
66    store: ErasedStore<S>,
67    cookie_opts: CookieBuilder,
68    handle: Option<JoinHandle<()>>,
69}
70
71impl CookieContext<()> {
72    /// Create a [`CookieSessionBuilder`] to configure the session store and cookie options.
73    pub fn builder() -> CookieSessionBuilder<()> {
74        CookieSessionBuilder::new()
75    }
76}
77
78impl<S: 'static> CookieContext<S> {
79    /// Build a cookie for the given session ID using the configured cookie options.
80    pub fn get_cookie(&self, session_id: SessionId) -> Cookie {
81        self.0.cookie_opts.clone().value(session_id).build()
82    }
83
84    /// Create a new session, store it, and return a `Set-Cookie` cookie.
85    pub async fn create_session(&self, state: S) -> Result<Cookie, Response> {
86        let session_id = SessionId::new();
87        crate::debug!("Storing {session_id:?} in cookie store");
88        let now = utc_now().as_secs();
89        let session = CookieSession::new(session_id.clone(), now, state);
90        self.0.store.store_session(session).await?;
91
92        Ok(self.get_cookie(session_id))
93    }
94
95    /// Remove the session identified by the cookie in the given jar.
96    pub async fn remove_session_jar(
97        &self,
98        jar: &CookieJar,
99    ) -> Result<Option<CookieSession<S>>, Response> {
100        let Some(session_id) = self.session_id_from_jar(jar) else {
101            return Ok(None);
102        };
103
104        self.0.store.remove_session(&session_id).await
105    }
106
107    /// Remove the session identified by the given cookie.
108    pub async fn remove_session_cookie(
109        &self,
110        cookie: &Cookie,
111    ) -> Result<Option<CookieSession<S>>, Response> {
112        let session_id = SessionId::from_cookie(cookie);
113        self.remove_session(&session_id).await
114    }
115
116    /// Remove the session with the given ID from the store.
117    pub async fn remove_session(
118        &self,
119        session_id: &SessionId,
120    ) -> Result<Option<CookieSession<S>>, Response> {
121        self.0.store.remove_session(session_id).await
122    }
123
124    /// Create a cookie builder with the configured options and a custom name.
125    pub fn build_cookie(&self, name: impl Into<Cow<'static, str>>) -> CookieBuilder {
126        self.0.cookie_opts.clone().name(name)
127    }
128
129    /// Return a reference to the underlying cookie builder.
130    pub fn cookie_builder(&self) -> &CookieBuilder {
131        &self.0.cookie_opts
132    }
133
134    /// Remove all sessions created before `deadline` (Unix timestamp in seconds).
135    pub async fn remove_before(&self, deadline: u64) -> Result<(), Response> {
136        self.0.store.remove_before(deadline).await
137    }
138
139    pub(crate) async fn load_from_headers(
140        &self,
141        headers: &HeaderMap,
142    ) -> Result<Option<CookieSession<S>>, Response> {
143        let cookies = CookieJar::from_headers(headers);
144
145        self.load_from_jar(&cookies).await
146    }
147
148    pub(crate) async fn load_from_jar(
149        &self,
150        cookies: &CookieJar,
151    ) -> Result<Option<CookieSession<S>>, Response> {
152        let Some(session_id) = self.session_id_from_jar(cookies) else {
153            return Ok(None);
154        };
155
156        self.0.store.load_session(&session_id).await
157    }
158
159    pub(crate) fn session_id_from_jar(&self, jar: &CookieJar) -> Option<SessionId> {
160        let cookie = jar.get(self.0.cookie_opts.get_name())?;
161
162        Some(SessionId::from_cookie(cookie))
163    }
164
165    /// Load a session from the store using a cookie's value as the session ID.
166    pub async fn load_from_cookie(
167        &self,
168        cookie: &Cookie,
169    ) -> Result<Option<CookieSession<S>>, Response> {
170        let session_id = SessionId::from_cookie(cookie);
171
172        self.0.store.load_session(&session_id).await
173    }
174}
175
176impl<S, U> FromRequestParts<S> for CookieContext<U>
177where
178    CookieContext<U>: FromRef<S>,
179    S: Send + Sync,
180{
181    type Rejection = Infallible;
182
183    async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
184        Ok(Self::from_ref(state))
185    }
186}
187
188impl<S> Drop for CookieContextInner<S> {
189    fn drop(&mut self) {
190        // Make sure to cancel the bg task if the cookie context is dropped. This is only
191        // implemented for the Inner type because we don't to cancel the task if a weak reference
192        // is dropped.
193        if let Some(handle) = &self.handle {
194            handle.abort();
195        }
196    }
197}
198impl<S> Clone for CookieContext<S> {
199    fn clone(&self) -> Self {
200        CookieContext(self.0.clone())
201    }
202}