doido_controller/
session.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
2use doido_cache::CacheStore;
3use doido_core::Result;
4use hmac::{Hmac, KeyInit, Mac};
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use sha2::Sha256;
9use std::sync::Arc;
10
11type HmacSha256 = Hmac<Sha256>;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Session {
16 pub id: String,
17 pub data: Value,
18}
19
20impl Session {
21 pub fn new() -> Self {
23 Self {
24 id: uuid::Uuid::new_v4().to_string(),
25 data: Value::Object(Default::default()),
26 }
27 }
28
29 pub fn with_id(id: impl Into<String>) -> Self {
31 Self {
32 id: id.into(),
33 data: Value::Object(Default::default()),
34 }
35 }
36
37 pub fn set(&mut self, key: &str, value: impl Serialize) {
39 if !self.data.is_object() {
40 self.data = Value::Object(Default::default());
41 }
42 if let Value::Object(map) = &mut self.data {
43 map.insert(
44 key.to_string(),
45 serde_json::to_value(value).unwrap_or(Value::Null),
46 );
47 }
48 }
49
50 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
52 self.data
53 .get(key)
54 .cloned()
55 .and_then(|v| serde_json::from_value(v).ok())
56 }
57}
58
59impl Default for Session {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65#[async_trait::async_trait]
70pub trait SessionStore: Send + Sync {
71 async fn load(&self, id: &str) -> Result<Option<Session>>;
72 async fn save(&self, session: &Session) -> Result<()>;
73 async fn destroy(&self, id: &str) -> Result<()>;
74}
75
76pub struct CookieSessionStore {
83 secret: Vec<u8>,
84}
85
86impl CookieSessionStore {
87 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
89 Self {
90 secret: secret.into(),
91 }
92 }
93
94 pub fn encode(&self, session: &Session) -> String {
96 let payload = serde_json::to_vec(session).unwrap_or_default();
97 let msg = URL_SAFE_NO_PAD.encode(payload);
98 let sig = self.sign(msg.as_bytes());
99 format!("{msg}.{}", URL_SAFE_NO_PAD.encode(sig))
100 }
101
102 pub fn decode(&self, raw: &str) -> Option<Session> {
105 let (msg, sig_b64) = raw.split_once('.')?;
106 let sig = URL_SAFE_NO_PAD.decode(sig_b64).ok()?;
107 let mut mac = HmacSha256::new_from_slice(&self.secret).ok()?;
108 mac.update(msg.as_bytes());
109 mac.verify_slice(&sig).ok()?;
111 let payload = URL_SAFE_NO_PAD.decode(msg).ok()?;
112 serde_json::from_slice(&payload).ok()
113 }
114
115 fn sign(&self, msg: &[u8]) -> Vec<u8> {
116 let mut mac =
117 HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts a key of any length");
118 mac.update(msg);
119 mac.finalize().into_bytes().to_vec()
120 }
121}
122
123impl Default for CookieSessionStore {
124 fn default() -> Self {
127 Self::new(b"doido-dev-insecure-secret".to_vec())
128 }
129}
130
131#[async_trait::async_trait]
132impl SessionStore for CookieSessionStore {
133 async fn load(&self, raw: &str) -> Result<Option<Session>> {
135 Ok(self.decode(raw))
136 }
137
138 async fn save(&self, _session: &Session) -> Result<()> {
141 Ok(())
142 }
143
144 async fn destroy(&self, _id: &str) -> Result<()> {
145 Ok(())
146 }
147}
148
149pub struct EncryptedCookieSessionStore {
157 secret: Vec<u8>,
158}
159
160impl EncryptedCookieSessionStore {
161 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
163 Self {
164 secret: secret.into(),
165 }
166 }
167
168 pub fn encode(&self, session: &Session) -> String {
170 let payload = serde_json::to_vec(session).unwrap_or_default();
171 let blob = doido_core::crypto::encrypt(&self.secret, &payload);
172 URL_SAFE_NO_PAD.encode(blob)
173 }
174
175 pub fn decode(&self, raw: &str) -> Option<Session> {
178 let blob = URL_SAFE_NO_PAD.decode(raw).ok()?;
179 let payload = doido_core::crypto::decrypt(&self.secret, &blob)?;
180 serde_json::from_slice(&payload).ok()
181 }
182}
183
184impl Default for EncryptedCookieSessionStore {
185 fn default() -> Self {
187 Self::new(crate::secret::key_base())
188 }
189}
190
191#[async_trait::async_trait]
192impl SessionStore for EncryptedCookieSessionStore {
193 async fn load(&self, raw: &str) -> Result<Option<Session>> {
194 Ok(self.decode(raw))
195 }
196 async fn save(&self, _session: &Session) -> Result<()> {
197 Ok(())
198 }
199 async fn destroy(&self, _id: &str) -> Result<()> {
200 Ok(())
201 }
202}
203
204pub struct CacheSessionStore {
209 store: Arc<dyn CacheStore>,
210 ttl_secs: Option<u64>,
211}
212
213impl CacheSessionStore {
214 pub fn new(store: Arc<dyn CacheStore>) -> Self {
216 Self {
217 store,
218 ttl_secs: None,
219 }
220 }
221
222 pub fn with_ttl(mut self, secs: u64) -> Self {
224 self.ttl_secs = Some(secs);
225 self
226 }
227
228 fn key(id: &str) -> String {
229 format!("session:{id}")
230 }
231}
232
233#[async_trait::async_trait]
234impl SessionStore for CacheSessionStore {
235 async fn load(&self, id: &str) -> Result<Option<Session>> {
236 match self.store.get(&Self::key(id)).await? {
237 Some(value) => Ok(serde_json::from_value(value).ok()),
238 None => Ok(None),
239 }
240 }
241
242 async fn save(&self, session: &Session) -> Result<()> {
243 let value = serde_json::to_value(session)?;
244 self.store
245 .set(&Self::key(&session.id), value, self.ttl_secs)
246 .await
247 }
248
249 async fn destroy(&self, id: &str) -> Result<()> {
250 self.store.delete(&Self::key(id)).await
251 }
252}