1mod memory;
9#[cfg(feature = "postgres")]
10#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
11mod postgres;
12#[cfg(feature = "valkey")]
13#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
14mod valkey;
15
16use std::future::Future;
17
18use chrono::{DateTime, TimeDelta, Utc};
19use uuid::Uuid;
20
21pub use memory::MemoryStore;
22#[cfg(feature = "postgres")]
23#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
24pub use postgres::PgStore;
25#[cfg(feature = "valkey")]
26#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
27pub use valkey::ValkeyStore;
28
29use crate::{
30 error::StoreError,
31 event::{EventKind, SessionEvent, SessionOrigin},
32 fingerprint::Baseline,
33 token::TokenHash,
34};
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SessionRecord {
39 pub id: Uuid,
41 pub subject: String,
43 pub created_at: DateTime<Utc>,
45 pub authenticated_at: DateTime<Utc>,
47 pub last_seen_at: DateTime<Utc>,
50 pub expires_at: DateTime<Utc>,
52 pub step_up_required: bool,
54 pub baseline: Baseline,
56 pub origin: Option<SessionOrigin>,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub struct TokenRecord {
63 pub hash: TokenHash,
65 pub issued_at: DateTime<Utc>,
67 pub first_used_at: Option<DateTime<Utc>>,
69 pub superseded_at: Option<DateTime<Utc>>,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct TokenLookup {
76 pub session: SessionRecord,
78 pub token: TokenRecord,
80 pub newer_token_used: bool,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Supersede {
89 Exact(TokenHash),
92 CurrentIssuedBefore(DateTime<Utc>),
94 Current,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100#[non_exhaustive]
101pub enum ClaimScope<'a> {
102 Session(Uuid),
104 Subject(&'a str),
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub struct Rotation {
111 pub new_token: TokenHash,
113 pub supersede: Supersede,
115 pub prune_superseded_before: DateTime<Utc>,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct SessionUpdate {
122 pub session_id: Uuid,
124 pub now: DateTime<Utc>,
126 pub mark_used: Option<TokenHash>,
128 pub baseline: Option<Baseline>,
130 pub step_up_required: Option<bool>,
132 pub authenticated_at: Option<DateTime<Utc>>,
134 pub rotation: Option<Rotation>,
136}
137
138impl SessionUpdate {
139 pub fn touch(session_id: Uuid, now: DateTime<Utc>) -> Self {
141 Self {
142 session_id,
143 now,
144 mark_used: None,
145 baseline: None,
146 step_up_required: None,
147 authenticated_at: None,
148 rotation: None,
149 }
150 }
151}
152
153pub trait SessionStore: Send + Sync + 'static {
158 fn insert_session(
161 &self,
162 session: &SessionRecord,
163 token: &TokenHash,
164 ) -> impl Future<Output = Result<(), StoreError>> + Send;
165
166 fn find_by_token(&self, token: &TokenHash) -> impl Future<Output = Result<Option<TokenLookup>, StoreError>> + Send;
168
169 fn apply(&self, update: &SessionUpdate) -> impl Future<Output = Result<bool, StoreError>> + Send;
180
181 fn delete_session(&self, session_id: Uuid) -> impl Future<Output = Result<bool, StoreError>> + Send;
183
184 fn delete_subject_sessions(&self, subject: &str) -> impl Future<Output = Result<Vec<Uuid>, StoreError>> + Send;
186
187 fn list_subject_sessions(
189 &self,
190 subject: &str,
191 ) -> impl Future<Output = Result<Vec<SessionRecord>, StoreError>> + Send;
192
193 fn delete_expired(
196 &self,
197 now: DateTime<Utc>,
198 idle_cutoff: DateTime<Utc>,
199 ) -> impl Future<Output = Result<u64, StoreError>> + Send;
200
201 fn record_event(&self, event: &SessionEvent) -> impl Future<Output = Result<(), StoreError>> + Send;
203
204 fn delete_events_before(&self, cutoff: DateTime<Utc>) -> impl Future<Output = Result<u64, StoreError>> + Send;
207
208 fn claim_event(
220 &self,
221 scope: ClaimScope<'_>,
222 kind: EventKind,
223 now: DateTime<Utc>,
224 window: TimeDelta,
225 ) -> impl Future<Output = Result<bool, StoreError>> + Send {
226 let _ = (scope, kind, now, window);
227 std::future::ready(Ok(true))
228 }
229}
230
231impl<T: SessionStore> SessionStore for std::sync::Arc<T> {
232 fn insert_session(
233 &self,
234 session: &SessionRecord,
235 token: &TokenHash,
236 ) -> impl Future<Output = Result<(), StoreError>> + Send {
237 (**self).insert_session(session, token)
238 }
239
240 fn find_by_token(&self, token: &TokenHash) -> impl Future<Output = Result<Option<TokenLookup>, StoreError>> + Send {
241 (**self).find_by_token(token)
242 }
243
244 fn apply(&self, update: &SessionUpdate) -> impl Future<Output = Result<bool, StoreError>> + Send {
245 (**self).apply(update)
246 }
247
248 fn delete_session(&self, session_id: Uuid) -> impl Future<Output = Result<bool, StoreError>> + Send {
249 (**self).delete_session(session_id)
250 }
251
252 fn delete_subject_sessions(&self, subject: &str) -> impl Future<Output = Result<Vec<Uuid>, StoreError>> + Send {
253 (**self).delete_subject_sessions(subject)
254 }
255
256 fn list_subject_sessions(
257 &self,
258 subject: &str,
259 ) -> impl Future<Output = Result<Vec<SessionRecord>, StoreError>> + Send {
260 (**self).list_subject_sessions(subject)
261 }
262
263 fn delete_expired(
264 &self,
265 now: DateTime<Utc>,
266 idle_cutoff: DateTime<Utc>,
267 ) -> impl Future<Output = Result<u64, StoreError>> + Send {
268 (**self).delete_expired(now, idle_cutoff)
269 }
270
271 fn record_event(&self, event: &SessionEvent) -> impl Future<Output = Result<(), StoreError>> + Send {
272 (**self).record_event(event)
273 }
274
275 fn delete_events_before(&self, cutoff: DateTime<Utc>) -> impl Future<Output = Result<u64, StoreError>> + Send {
276 (**self).delete_events_before(cutoff)
277 }
278
279 fn claim_event(
280 &self,
281 scope: ClaimScope<'_>,
282 kind: EventKind,
283 now: DateTime<Utc>,
284 window: TimeDelta,
285 ) -> impl Future<Output = Result<bool, StoreError>> + Send {
286 (**self).claim_event(scope, kind, now, window)
287 }
288}