@jpgilldev / steid

steid/src/infrastructure/web/session_cookie.rs
3.0 KBRaw
1//! A relaxed session cookie for local HTTP development.
2//!
3//! Topcoat's [`CookieTokenStore`](topcoat::session::CookieTokenStore) is hardened:
4//! `__Host-` prefixed, `Secure`, `HttpOnly`, `SameSite=Lax`, scoped to `/`. `Secure`
5//! means the browser only stores it over a trustworthy origin — and browsers disagree
6//! about whether plain-HTTP `localhost` counts. Where it doesn't, the server issues a
7//! session, the browser silently drops it, and every page renders signed out with no
8//! error anywhere.
9//!
10//! This store is the same cookie without `Secure` and without the `__Host-` prefix, so
11//! it survives `http://localhost`. It is strictly worse and exists only for dev.
12
13use std::{borrow::Cow, time::Duration};
14
15use topcoat::{
16 context::Cx,
17 cookie::{Cookie, Cookies, SameSite},
18 session::{TokenStore, TokenStoreFuture},
19};
20
21/// Name of the relaxed cookie. Deliberately not `session`, so a cookie issued in dev
22/// can never be mistaken for one issued by the hardened store.
23pub const INSECURE_SESSION_COOKIE_NAME: &str = "steid-dev-session";
24
25/// Carries the session token in a cookie that works over plain HTTP.
26///
27/// **Never enable this on anything reachable from a network.** Without `Secure` the
28/// cookie is sent over unencrypted connections, where anyone on the path can read it
29/// and use it to impersonate the session.
30pub struct InsecureCookieTokenStore {
31 name: Cow<'static, str>,
32}
33
34impl InsecureCookieTokenStore {
35 pub fn new() -> Self {
36 Self::default()
37 }
38}
39
40impl Default for InsecureCookieTokenStore {
41 fn default() -> Self {
42 Self {
43 name: Cow::Borrowed(INSECURE_SESSION_COOKIE_NAME),
44 }
45 }
46}
47
48/// Everything the hardened store does except `secure` and the `__Host-` prefix.
49/// `HttpOnly` and `SameSite=Lax` are kept — they cost nothing over HTTP and still
50/// block script access and cross-site POSTs.
51fn cookies(cx: &Cx) -> impl Cookies {
52 topcoat::cookie::cookies(cx)
53 .override_same_site(SameSite::Lax)
54 .override_http_only(true)
55 .override_path("/")
56}
57
58impl TokenStore for InsecureCookieTokenStore {
59 fn read<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, Option<topcoat::session::Token>> {
60 Box::pin(async move {
61 let Some(cookie) = cookies(cx).get(&self.name) else {
62 return Ok(None);
63 };
64 Ok(topcoat::session::Token::decode(cookie.value_trimmed()).ok())
65 })
66 }
67
68 fn write<'a>(
69 &'a self,
70 cx: &'a Cx,
71 token: topcoat::session::Token,
72 max_age: Duration,
73 ) -> TokenStoreFuture<'a, ()> {
74 Box::pin(async move {
75 let max_age = topcoat::cookie::time::Duration::try_from(max_age)?;
76 cookies(cx)
77 .override_max_age(max_age)
78 .add(Cookie::new(self.name.clone(), token.encode()));
79 Ok(())
80 })
81 }
82
83 fn delete<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, ()> {
84 Box::pin(async move {
85 cookies(cx).remove(self.name.clone());
86 Ok(())
87 })
88 }
89}