Skip to main content

topcoat_session/token/
store.rs

1use std::{pin::Pin, time::Duration};
2
3use topcoat_core::{context::Cx, error::Result};
4
5use crate::{Token, config};
6
7/// The future returned by [`TokenStore`] methods: a boxed, `Send` future
8/// borrowing the store and the request context.
9pub type TokenStoreFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
10
11/// The client-side transport for the session token.
12///
13/// A token store moves the raw [`Token`] between the client and the server;
14/// it is not the session database, which the application owns. The default
15/// [`CookieTokenStore`](cookie::CookieTokenStore) carries the token in a
16/// hardened cookie; implement this trait to carry it elsewhere, such as an
17/// `Authorization` header.
18pub trait TokenStore: Send + Sync {
19    /// Reads the token presented by the current request, or `None` when the
20    /// request carries none (or a malformed one).
21    fn read<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, Option<Token>>;
22
23    /// Issues `token` to the client with the given time to live, replacing
24    /// any previously issued token.
25    fn write<'a>(&'a self, cx: &'a Cx, token: Token, max_age: Duration)
26    -> TokenStoreFuture<'a, ()>;
27
28    /// Instructs the client to discard its token.
29    fn delete<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, ()>;
30}
31
32pub(crate) fn token_store(cx: &Cx) -> &dyn TokenStore {
33    &*config(cx).token_store
34}
35
36#[cfg(feature = "cookie")]
37pub mod cookie {
38    use std::{borrow::Cow, time::Duration};
39
40    use topcoat_cookie::{Cookie, Cookies, SameSite};
41    use topcoat_core::context::Cx;
42
43    use crate::{Token, TokenStore, TokenStoreFuture};
44
45    fn cookies(cx: &Cx) -> impl Cookies {
46        topcoat_cookie::cookies(cx)
47            .override_same_site(SameSite::Lax)
48            .override_http_only(true)
49            .override_secure(true)
50            .override_path("/")
51            .override_prefix_host()
52    }
53
54    /// The default name of the session cookie.
55    pub const SESSION_COOKIE_NAME: &str = "session";
56
57    /// A [`TokenStore`] carrying the token in a hardened cookie: `__Host-`
58    /// prefixed, `Secure`, `HttpOnly`, `SameSite=Lax`, and scoped to `/`.
59    ///
60    /// Requires the cookie layer (`RouterBuilder::cookies`) to be registered
61    /// on the router.
62    pub struct CookieTokenStore {
63        name: Cow<'static, str>,
64    }
65
66    impl CookieTokenStore {
67        /// Creates a store using [`SESSION_COOKIE_NAME`].
68        #[must_use]
69        pub fn new() -> Self {
70            Self::default()
71        }
72
73        /// Overrides the name of the session cookie.
74        #[must_use]
75        pub fn name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
76            self.name = name.into();
77            self
78        }
79    }
80
81    impl Default for CookieTokenStore {
82        fn default() -> Self {
83            Self {
84                name: Cow::Borrowed(SESSION_COOKIE_NAME),
85            }
86        }
87    }
88
89    impl TokenStore for CookieTokenStore {
90        fn read<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, Option<Token>> {
91            Box::pin(async move {
92                let Some(cookie) = cookies(cx).get(&self.name) else {
93                    return Ok(None);
94                };
95                Ok(Token::decode(cookie.value_trimmed()).ok())
96            })
97        }
98
99        fn write<'a>(
100            &'a self,
101            cx: &'a Cx,
102            token: Token,
103            max_age: Duration,
104        ) -> TokenStoreFuture<'a, ()> {
105            Box::pin(async move {
106                let max_age = topcoat_cookie::time::Duration::try_from(max_age)?;
107                cookies(cx)
108                    .override_max_age(max_age)
109                    .add(Cookie::new(self.name.clone(), token.encode()));
110                Ok(())
111            })
112        }
113
114        fn delete<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, ()> {
115            Box::pin(async move {
116                cookies(cx).remove(Cookie::new(self.name.clone(), ""));
117                Ok(())
118            })
119        }
120    }
121}