Skip to main content

ocre/
cookies.rs

1//! The request's cookies as an extractor, plain, signed or encrypted
2//! (Rails' `cookies`, `cookies.signed` and `cookies.encrypted`).
3
4use std::{
5    sync::{Arc, Mutex, MutexGuard, PoisonError},
6    time::Duration,
7};
8
9use axum::{
10    extract::FromRequestParts,
11    http::{HeaderMap, HeaderValue, header, request::Parts},
12};
13use cookie::{Cookie, CookieJar, SameSite};
14
15use crate::{
16    Error, Result,
17    session::{Keys, Rejection, SESSION_COOKIE, reject},
18};
19
20/// The request's cookies; what handlers set goes out with the response.
21///
22/// - [`get`](Self::get) / [`set`](Self::set): plain values the browser can read and change.
23/// - [`signed`](Self::signed) / [`set_signed`](Self::set_signed): readable,
24///   but a changed value reads as `None` (HMAC-SHA256).
25/// - [`encrypted`](Self::encrypted) / [`set_encrypted`](Self::set_encrypted):
26///   hidden and tamper-proof (AES-256-GCM).
27///
28/// Signed and encrypted cookies use keys derived from `SECRET_KEY_BASE`;
29/// values made with a key of `SECRET_KEY_BASE_PREVIOUS` still read. Cookies
30/// Ocre sets are `HttpOnly`, `SameSite=Lax`, `Path=/`, and `Secure` over
31/// HTTPS. For per-visitor state prefer the [`Session`](crate::Session),
32/// which is encrypted too; use cookies for values that outlive it or that
33/// another part of the site reads (a theme, a "remember me" token).
34///
35/// # Free plan
36///
37/// Nothing billed: cookies travel with the requests.
38///
39/// # Examples
40///
41/// ```no_run
42/// use std::time::Duration;
43/// use ocre::{Cookies, Result};
44///
45/// async fn theme(cookies: Cookies) -> Result<String> {
46///     let theme = cookies.get("theme").unwrap_or_else(|| "light".to_owned());
47///     cookies.set_signed("seen_banner", "1", Some(Duration::from_secs(30 * 86_400)))?;
48///     Ok(theme)
49/// }
50/// # let _ = theme;
51/// ```
52#[derive(Clone)]
53pub struct Cookies(Arc<Mutex<State>>);
54
55struct State {
56    jar: CookieJar,
57    keys: std::result::Result<Keys, String>,
58    secure: bool,
59}
60
61impl Cookies {
62    /// The cookies of a request; `secure` marks those set over HTTPS.
63    pub(crate) fn from_headers(headers: &HeaderMap, keys: std::result::Result<Keys, String>, secure: bool) -> Self {
64        let mut jar = CookieJar::new();
65        let cookies = headers.get_all(header::COOKIE).iter().filter_map(|value| value.to_str().ok());
66        for cookie in cookies.flat_map(Cookie::split_parse_encoded).filter_map(std::result::Result::ok) {
67            jar.add_original(cookie.into_owned());
68        }
69        Self(Arc::new(Mutex::new(State { jar, keys, secure })))
70    }
71
72    fn state(&self) -> MutexGuard<'_, State> {
73        self.0.lock().unwrap_or_else(PoisonError::into_inner)
74    }
75
76    /// The value of the plain cookie `name`.
77    ///
78    /// # Examples
79    ///
80    /// ```no_run
81    /// async fn theme(cookies: ocre::Cookies) -> String {
82    ///     cookies.get("theme").unwrap_or_default()
83    /// }
84    /// # let _ = theme;
85    /// ```
86    pub fn get(&self, name: &str) -> Option<String> {
87        self.state().jar.get(name).map(|cookie| cookie.value().to_owned())
88    }
89
90    /// The value of the signed cookie `name`; `None` when absent or changed by the client.
91    ///
92    /// # Errors
93    ///
94    /// [`Error::Internal`] when `SECRET_KEY_BASE` is missing or shorter than 64 characters.
95    pub fn signed(&self, name: &str) -> Result<Option<String>> {
96        let state = self.state();
97        let keys = keys(&state)?;
98        Ok(keys.iter().find_map(|key| state.jar.signed(key).get(name)).map(|cookie| cookie.value().to_owned()))
99    }
100
101    /// The value of the encrypted cookie `name`; `None` when absent or changed by the client.
102    ///
103    /// # Errors
104    ///
105    /// [`Error::Internal`] when `SECRET_KEY_BASE` is missing or shorter than 64 characters.
106    pub fn encrypted(&self, name: &str) -> Result<Option<String>> {
107        let state = self.state();
108        let keys = keys(&state)?;
109        Ok(keys.iter().find_map(|key| state.jar.private(key).get(name)).map(|cookie| cookie.value().to_owned()))
110    }
111
112    /// Sets a plain cookie; `max_age` `None` makes it last until the browser closes.
113    ///
114    /// # Errors
115    ///
116    /// [`Error::Internal`] for the session cookie's name, which Ocre manages.
117    pub fn set(&self, name: &str, value: &str, max_age: Option<Duration>) -> Result<()> {
118        let cookie = self.build(name, value, max_age)?;
119        self.state().jar.add(cookie);
120        Ok(())
121    }
122
123    /// Sets a signed cookie: the browser sees the value, and a changed one reads as `None`.
124    ///
125    /// # Errors
126    ///
127    /// [`Error::Internal`] for the session cookie's name, or when
128    /// `SECRET_KEY_BASE` is missing or shorter than 64 characters.
129    pub fn set_signed(&self, name: &str, value: &str, max_age: Option<Duration>) -> Result<()> {
130        let cookie = self.build(name, value, max_age)?;
131        let mut state = self.state();
132        let key = keys(&state)?[0].clone();
133        state.jar.signed_mut(&key).add(cookie);
134        Ok(())
135    }
136
137    /// Sets an encrypted cookie: the browser can neither read nor change the value.
138    ///
139    /// # Errors
140    ///
141    /// [`Error::Internal`] for the session cookie's name, or when
142    /// `SECRET_KEY_BASE` is missing or shorter than 64 characters.
143    pub fn set_encrypted(&self, name: &str, value: &str, max_age: Option<Duration>) -> Result<()> {
144        let cookie = self.build(name, value, max_age)?;
145        let mut state = self.state();
146        let key = keys(&state)?[0].clone();
147        state.jar.private_mut(&key).add(cookie);
148        Ok(())
149    }
150
151    /// Deletes the cookie `name` from the browser.
152    pub fn remove(&self, name: &str) {
153        // Not `CookieJar::remove`: its removal cookie reads the clock, which
154        // panics on wasm32-unknown-unknown. Max-Age=0 and a past date do the same.
155        let mut cookie = Cookie::build((name.to_owned(), "")).path("/").build();
156        cookie.set_max_age(cookie::time::Duration::ZERO);
157        cookie.set_expires(cookie::time::OffsetDateTime::UNIX_EPOCH);
158        self.state().jar.add(cookie);
159    }
160
161    fn build(&self, name: &str, value: &str, max_age: Option<Duration>) -> Result<Cookie<'static>> {
162        if name == SESSION_COOKIE {
163            return Err(Error::internal(format!(
164                "`{SESSION_COOKIE}` is the session cookie. Fix: store the value in the `Session`, or pick another cookie name"
165            )));
166        }
167        let mut cookie = Cookie::build((name.to_owned(), value.to_owned()))
168            .path("/")
169            .http_only(true)
170            .same_site(SameSite::Lax)
171            .secure(self.state().secure)
172            .build();
173        if let Some(max_age) = max_age {
174            cookie.set_max_age(cookie::time::Duration::seconds(i64::try_from(max_age.as_secs()).unwrap_or(i64::MAX)));
175        }
176        Ok(cookie)
177    }
178
179    /// `Set-Cookie` values of the cookies set during the request.
180    pub(crate) fn set_cookies(&self) -> Vec<HeaderValue> {
181        let state = self.state();
182        let values = state.jar.delta().map(|cookie| HeaderValue::from_str(&cookie.encoded().to_string()));
183        values.filter_map(std::result::Result::ok).collect()
184    }
185}
186
187/// The current key, then the previous ones.
188fn keys(state: &State) -> Result<Vec<cookie::Key>> {
189    let keys = state.keys.as_ref().map_err(|err| Error::internal(err.clone()))?;
190    Ok(std::iter::once(&keys.current).chain(&keys.previous).cloned().collect())
191}
192
193impl<S: Sync> FromRequestParts<S> for Cookies {
194    type Rejection = Rejection;
195
196    async fn from_request_parts(parts: &mut Parts, _state: &S) -> std::result::Result<Self, Rejection> {
197        parts.extensions.get::<Cookies>().cloned().ok_or_else(|| {
198            reject(Error::internal(
199                "no cookies on this request. Fix: serve the app with `ocre::serve`, which adds them",
200            ))
201        })
202    }
203}
204
205#[cfg(test)]
206#[path = "../tests/cookies.rs"]
207mod tests;