siderite_sessions/store.rs
1//! The [`SessionStore`] trait and the data it persists.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use std::fmt;
7use std::time::Duration;
8
9/// One stored session.
10///
11/// Times are seconds since the Unix epoch. The record never contains the
12/// session id: stores key it by the SHA-256 hex of the id (see
13/// [`id_hash`](crate::id_hash)).
14#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub struct SessionRecord {
16 /// The authenticated user, set by [`Session::login`](crate::Session).
17 pub user: Option<String>,
18 /// Application data.
19 pub data: Map<String, Value>,
20 /// When the session was created (login resets it).
21 pub created_at: u64,
22 /// When the session was last written; drives the idle timeout.
23 pub last_seen: u64,
24 /// The user's revocation epoch at login. A store that revokes by
25 /// epoch (see [`SessionStore::revoke_user`]) rejects older records.
26 pub auth_epoch: u64,
27}
28
29impl fmt::Debug for SessionRecord {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 // Application data and user names stay out of logs.
32 f.debug_struct("SessionRecord")
33 .field("authenticated", &self.user.is_some())
34 .field("keys", &self.data.len())
35 .field("created_at", &self.created_at)
36 .field("last_seen", &self.last_seen)
37 .field("auth_epoch", &self.auth_epoch)
38 .finish()
39 }
40}
41
42/// A store failure.
43///
44/// The message must never contain addresses, credentials, session ids or
45/// stored data: it can reach logs, and a failure never reaches clients.
46#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[error("session store error: {0}")]
48pub struct StoreError(String);
49
50impl StoreError {
51 /// Create an error with a short, secret-free `message`.
52 #[must_use]
53 pub fn new(message: impl Into<String>) -> Self {
54 Self(message.into())
55 }
56}
57
58/// Server-side storage of sessions, keyed by the SHA-256 hex of the id.
59///
60/// The store never sees a raw session id, so a leaked table or backup does
61/// not hand out live sessions. Implementations must be safe for concurrent
62/// use. Requests of one session are last-writer-wins: the layer does not
63/// merge concurrent changes.
64///
65/// A store is not asked to enforce idle or absolute lifetimes (the layer
66/// does, from the record), but it must drop entries after `ttl` so that
67/// abandoned sessions disappear.
68#[async_trait]
69pub trait SessionStore: Send + Sync + 'static {
70 /// Fetch the record for `id_hash`; `Ok(None)` when absent or expired
71 /// or revoked.
72 ///
73 /// # Errors
74 /// [`StoreError`] when the store cannot be read. The layer then
75 /// answers `503` instead of treating the request as anonymous.
76 async fn load(
77 &self,
78 id_hash: &str,
79 ) -> Result<Option<SessionRecord>, StoreError>;
80
81 /// Store `record` under `id_hash`, replacing any record, for `ttl`.
82 ///
83 /// # Errors
84 /// [`StoreError`] when the record was not stored.
85 async fn save(
86 &self,
87 id_hash: &str,
88 record: &SessionRecord,
89 ttl: Duration,
90 ) -> Result<(), StoreError>;
91
92 /// Replace the record under `id_hash` only if one is stored, for
93 /// `ttl`; `Ok(false)` when there is none.
94 ///
95 /// The layer writes existing sessions through this method, so a slow
96 /// request that loaded a session before another request logged it
97 /// out or rotated its id cannot store it again. Implementations must
98 /// never let a deleted id become loadable again, even when the check
99 /// and the write race with [`delete`](Self::delete).
100 ///
101 /// # Errors
102 /// [`StoreError`] when the record may not have been stored.
103 async fn update(
104 &self,
105 id_hash: &str,
106 record: &SessionRecord,
107 ttl: Duration,
108 ) -> Result<bool, StoreError>;
109
110 /// Remove `id_hash`. Removing an absent record is not an error.
111 ///
112 /// # Errors
113 /// [`StoreError`] when the record may still exist.
114 async fn delete(&self, id_hash: &str) -> Result<(), StoreError>;
115
116 /// Invalidate every session of `user`, now and for stale in-flight
117 /// writes. Use it after password changes and account recovery.
118 ///
119 /// # Errors
120 /// [`StoreError`] when the revocation may not have taken effect.
121 async fn revoke_user(&self, user: &str) -> Result<(), StoreError>;
122
123 /// The user's current revocation epoch, recorded in
124 /// [`SessionRecord::auth_epoch`] at login. Stores that revoke by
125 /// deleting records can keep the default of `0`.
126 ///
127 /// # Errors
128 /// [`StoreError`] when the epoch cannot be read; the login then fails.
129 async fn auth_epoch(&self, _user: &str) -> Result<u64, StoreError> {
130 Ok(0)
131 }
132}