axum_extra/extract/cookie/private.rs
1use super::{cookies_from_request, set_cookies, Cookie, Key};
2use axum::{
3 extract::{FromRef, FromRequestParts},
4 response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
5};
6use cookie::PrivateJar;
7use http::{request::Parts, HeaderMap};
8use std::{convert::Infallible, fmt, marker::PhantomData};
9
10/// Extractor that grabs private cookies from the request and manages the jar.
11///
12/// All cookies will be private and encrypted with a [`Key`]. This makes it suitable for storing
13/// private data.
14///
15/// Note that methods like [`PrivateCookieJar::add`], [`PrivateCookieJar::remove`], etc updates the
16/// [`PrivateCookieJar`] and returns it. This value _must_ be returned from the handler as part of
17/// the response for the changes to be propagated.
18///
19/// # Example
20///
21/// ```rust
22/// use axum::{
23/// Router,
24/// routing::{post, get},
25/// extract::FromRef,
26/// response::{IntoResponse, Redirect},
27/// http::StatusCode,
28/// };
29/// use axum_extra::{
30/// TypedHeader,
31/// headers::authorization::{Authorization, Bearer},
32/// extract::cookie::{PrivateCookieJar, Cookie, Key},
33/// };
34///
35/// async fn set_secret(
36/// jar: PrivateCookieJar,
37/// ) -> (PrivateCookieJar, Redirect) {
38/// let updated_jar = jar.add(Cookie::new("secret", "secret-data"));
39/// (updated_jar, Redirect::to("/get"))
40/// }
41///
42/// async fn get_secret(jar: PrivateCookieJar) {
43/// if let Some(data) = jar.get("secret") {
44/// // ...
45/// }
46/// }
47///
48/// // our application state
49/// #[derive(Clone)]
50/// struct AppState {
51/// // that holds the key used to encrypt cookies
52/// key: Key,
53/// }
54///
55/// // this impl tells `PrivateCookieJar` how to access the key from our state
56/// impl FromRef<AppState> for Key {
57/// fn from_ref(state: &AppState) -> Self {
58/// state.key.clone()
59/// }
60/// }
61///
62/// let state = AppState {
63/// // Generate a secure key
64/// //
65/// // You probably don't wanna generate a new one each time the app starts though
66/// key: Key::generate(),
67/// };
68///
69/// let app = Router::new()
70/// .route("/set", post(set_secret))
71/// .route("/get", get(get_secret))
72/// .with_state(state);
73/// # let _: axum::Router = app;
74/// ```
75///
76/// If you have been using `Arc<AppState>` you cannot implement `FromRef<Arc<AppState>> for Key`.
77/// You can use a new type instead:
78///
79/// ```rust
80/// # use axum::extract::FromRef;
81/// # use axum_extra::extract::cookie::{PrivateCookieJar, Cookie, Key};
82/// use std::sync::Arc;
83/// use std::ops::Deref;
84///
85/// #[derive(Clone)]
86/// struct AppState(Arc<InnerState>);
87///
88/// // deref so you can still access the inner fields easily
89/// impl Deref for AppState {
90/// type Target = InnerState;
91///
92/// fn deref(&self) -> &Self::Target {
93/// &self.0
94/// }
95/// }
96///
97/// struct InnerState {
98/// key: Key
99/// }
100///
101/// impl FromRef<AppState> for Key {
102/// fn from_ref(state: &AppState) -> Self {
103/// state.0.key.clone()
104/// }
105/// }
106/// ```
107pub struct PrivateCookieJar<K = Key> {
108 jar: cookie::CookieJar,
109 key: Key,
110 // The key used to extract the key. Allows users to use multiple keys for different
111 // jars. Maybe a library wants its own key.
112 _marker: PhantomData<K>,
113}
114
115impl<K> fmt::Debug for PrivateCookieJar<K> {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("PrivateCookieJar")
118 .field("jar", &self.jar)
119 .field("key", &"REDACTED")
120 .finish()
121 }
122}
123
124impl<S, K> FromRequestParts<S> for PrivateCookieJar<K>
125where
126 S: Send + Sync,
127 K: FromRef<S> + Into<Key>,
128{
129 type Rejection = Infallible;
130
131 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
132 let k = K::from_ref(state);
133 let key = k.into();
134 let PrivateCookieJar {
135 jar,
136 key,
137 _marker: _,
138 } = PrivateCookieJar::from_headers(&parts.headers, key);
139 Ok(PrivateCookieJar {
140 jar,
141 key,
142 _marker: PhantomData,
143 })
144 }
145}
146
147impl PrivateCookieJar {
148 /// Create a new `PrivateCookieJar` from a map of request headers.
149 ///
150 /// The valid cookies in `headers` will be added to the jar.
151 ///
152 /// This is intended to be used in middleware and other where places it might be difficult to
153 /// run extractors. Normally you should create `PrivateCookieJar`s through [`FromRequestParts`].
154 ///
155 /// [`FromRequestParts`]: axum::extract::FromRequestParts
156 pub fn from_headers(headers: &HeaderMap, key: Key) -> Self {
157 let mut jar = cookie::CookieJar::new();
158 let mut private_jar = jar.private_mut(&key);
159 for cookie in cookies_from_request(headers) {
160 if let Some(cookie) = private_jar.decrypt(cookie) {
161 private_jar.add_original(cookie);
162 }
163 }
164
165 Self {
166 jar,
167 key,
168 _marker: PhantomData,
169 }
170 }
171
172 /// Create a new empty `PrivateCookieJarIter`.
173 ///
174 /// This is intended to be used in middleware and other places where it might be difficult to
175 /// run extractors. Normally you should create `PrivateCookieJar`s through [`FromRequestParts`].
176 ///
177 /// [`FromRequestParts`]: axum::extract::FromRequestParts
178 pub fn new(key: Key) -> Self {
179 Self {
180 jar: Default::default(),
181 key,
182 _marker: PhantomData,
183 }
184 }
185}
186
187impl<K> PrivateCookieJar<K> {
188 /// Get a cookie from the jar.
189 ///
190 /// If the cookie exists and can be decrypted then it is returned in plaintext.
191 ///
192 /// # Example
193 ///
194 /// ```rust
195 /// use axum_extra::extract::cookie::PrivateCookieJar;
196 /// use axum::response::IntoResponse;
197 ///
198 /// async fn handle(jar: PrivateCookieJar) {
199 /// let value: Option<String> = jar
200 /// .get("foo")
201 /// .map(|cookie| cookie.value().to_owned());
202 /// }
203 /// ```
204 pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
205 self.private_jar().get(name)
206 }
207
208 /// Remove a cookie from the jar.
209 ///
210 /// # Example
211 ///
212 /// ```rust
213 /// use axum_extra::extract::cookie::{PrivateCookieJar, Cookie};
214 /// use axum::response::IntoResponse;
215 ///
216 /// async fn handle(jar: PrivateCookieJar) -> PrivateCookieJar {
217 /// jar.remove(Cookie::from("foo"))
218 /// }
219 /// ```
220 #[must_use]
221 pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
222 self.private_jar_mut().remove(cookie);
223 self
224 }
225
226 /// Add a cookie to the jar.
227 ///
228 /// The value will automatically be percent-encoded.
229 ///
230 /// # Example
231 ///
232 /// ```rust
233 /// use axum_extra::extract::cookie::{PrivateCookieJar, Cookie};
234 /// use axum::response::IntoResponse;
235 ///
236 /// async fn handle(jar: PrivateCookieJar) -> PrivateCookieJar {
237 /// jar.add(Cookie::new("foo", "bar"))
238 /// }
239 /// ```
240 #[must_use]
241 #[allow(clippy::should_implement_trait)]
242 pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
243 self.private_jar_mut().add(cookie);
244 self
245 }
246
247 /// Authenticates and decrypts `cookie`, returning the plaintext version if decryption succeeds
248 /// or `None` otherwise.
249 pub fn decrypt(&self, cookie: Cookie<'static>) -> Option<Cookie<'static>> {
250 self.private_jar().decrypt(cookie)
251 }
252
253 /// Get an iterator over all cookies in the jar.
254 ///
255 /// Only cookies with valid authenticity and integrity are yielded by the iterator.
256 pub fn iter(&self) -> impl Iterator<Item = Cookie<'static>> + '_ {
257 PrivateCookieJarIter {
258 jar: self,
259 iter: self.jar.iter(),
260 }
261 }
262
263 fn private_jar(&self) -> PrivateJar<&'_ cookie::CookieJar> {
264 self.jar.private(&self.key)
265 }
266
267 fn private_jar_mut(&mut self) -> PrivateJar<&'_ mut cookie::CookieJar> {
268 self.jar.private_mut(&self.key)
269 }
270}
271
272impl<K> IntoResponseParts for PrivateCookieJar<K> {
273 type Error = Infallible;
274
275 fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
276 set_cookies(self.jar, res.headers_mut());
277 Ok(res)
278 }
279}
280
281impl<K> IntoResponse for PrivateCookieJar<K> {
282 fn into_response(self) -> Response {
283 (self, ()).into_response()
284 }
285}
286
287struct PrivateCookieJarIter<'a, K> {
288 jar: &'a PrivateCookieJar<K>,
289 iter: cookie::Iter<'a>,
290}
291
292impl<K> Iterator for PrivateCookieJarIter<'_, K> {
293 type Item = Cookie<'static>;
294
295 fn next(&mut self) -> Option<Self::Item> {
296 loop {
297 let cookie = self.iter.next()?;
298
299 if let Some(cookie) = self.jar.get(cookie.name()) {
300 return Some(cookie);
301 }
302 }
303 }
304}
305
306impl<K> Clone for PrivateCookieJar<K> {
307 fn clone(&self) -> Self {
308 Self {
309 jar: self.jar.clone(),
310 key: self.key.clone(),
311 _marker: self._marker,
312 }
313 }
314}