mod memory;
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
mod postgres;
#[cfg(feature = "valkey")]
#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
mod valkey;
use std::future::Future;
use chrono::{DateTime, TimeDelta, Utc};
use uuid::Uuid;
pub use memory::MemoryStore;
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
pub use postgres::PgStore;
#[cfg(feature = "valkey")]
#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
pub use valkey::ValkeyStore;
use crate::{
error::StoreError,
event::{EventKind, SessionEvent, SessionOrigin},
fingerprint::Baseline,
token::TokenHash,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionRecord {
pub id: Uuid,
pub subject: String,
pub created_at: DateTime<Utc>,
pub authenticated_at: DateTime<Utc>,
pub last_seen_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub step_up_required: bool,
pub baseline: Baseline,
pub origin: Option<SessionOrigin>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TokenRecord {
pub hash: TokenHash,
pub issued_at: DateTime<Utc>,
pub first_used_at: Option<DateTime<Utc>>,
pub superseded_at: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TokenLookup {
pub session: SessionRecord,
pub token: TokenRecord,
pub newer_token_used: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Supersede {
Exact(TokenHash),
CurrentIssuedBefore(DateTime<Utc>),
Current,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ClaimScope<'a> {
Session(Uuid),
Subject(&'a str),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rotation {
pub new_token: TokenHash,
pub supersede: Supersede,
pub prune_superseded_before: DateTime<Utc>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionUpdate {
pub session_id: Uuid,
pub now: DateTime<Utc>,
pub mark_used: Option<TokenHash>,
pub baseline: Option<Baseline>,
pub step_up_required: Option<bool>,
pub authenticated_at: Option<DateTime<Utc>>,
pub rotation: Option<Rotation>,
}
impl SessionUpdate {
pub fn touch(session_id: Uuid, now: DateTime<Utc>) -> Self {
Self {
session_id,
now,
mark_used: None,
baseline: None,
step_up_required: None,
authenticated_at: None,
rotation: None,
}
}
}
pub trait SessionStore: Send + Sync + 'static {
fn insert_session(
&self,
session: &SessionRecord,
token: &TokenHash,
) -> impl Future<Output = Result<(), StoreError>> + Send;
fn find_by_token(&self, token: &TokenHash) -> impl Future<Output = Result<Option<TokenLookup>, StoreError>> + Send;
fn apply(&self, update: &SessionUpdate) -> impl Future<Output = Result<bool, StoreError>> + Send;
fn delete_session(&self, session_id: Uuid) -> impl Future<Output = Result<bool, StoreError>> + Send;
fn delete_subject_sessions(&self, subject: &str) -> impl Future<Output = Result<Vec<Uuid>, StoreError>> + Send;
fn list_subject_sessions(
&self,
subject: &str,
) -> impl Future<Output = Result<Vec<SessionRecord>, StoreError>> + Send;
fn delete_expired(
&self,
now: DateTime<Utc>,
idle_cutoff: DateTime<Utc>,
) -> impl Future<Output = Result<u64, StoreError>> + Send;
fn record_event(&self, event: &SessionEvent) -> impl Future<Output = Result<(), StoreError>> + Send;
fn delete_events_before(&self, cutoff: DateTime<Utc>) -> impl Future<Output = Result<u64, StoreError>> + Send;
fn claim_event(
&self,
scope: ClaimScope<'_>,
kind: EventKind,
now: DateTime<Utc>,
window: TimeDelta,
) -> impl Future<Output = Result<bool, StoreError>> + Send {
let _ = (scope, kind, now, window);
std::future::ready(Ok(true))
}
}
impl<T: SessionStore> SessionStore for std::sync::Arc<T> {
fn insert_session(
&self,
session: &SessionRecord,
token: &TokenHash,
) -> impl Future<Output = Result<(), StoreError>> + Send {
(**self).insert_session(session, token)
}
fn find_by_token(&self, token: &TokenHash) -> impl Future<Output = Result<Option<TokenLookup>, StoreError>> + Send {
(**self).find_by_token(token)
}
fn apply(&self, update: &SessionUpdate) -> impl Future<Output = Result<bool, StoreError>> + Send {
(**self).apply(update)
}
fn delete_session(&self, session_id: Uuid) -> impl Future<Output = Result<bool, StoreError>> + Send {
(**self).delete_session(session_id)
}
fn delete_subject_sessions(&self, subject: &str) -> impl Future<Output = Result<Vec<Uuid>, StoreError>> + Send {
(**self).delete_subject_sessions(subject)
}
fn list_subject_sessions(
&self,
subject: &str,
) -> impl Future<Output = Result<Vec<SessionRecord>, StoreError>> + Send {
(**self).list_subject_sessions(subject)
}
fn delete_expired(
&self,
now: DateTime<Utc>,
idle_cutoff: DateTime<Utc>,
) -> impl Future<Output = Result<u64, StoreError>> + Send {
(**self).delete_expired(now, idle_cutoff)
}
fn record_event(&self, event: &SessionEvent) -> impl Future<Output = Result<(), StoreError>> + Send {
(**self).record_event(event)
}
fn delete_events_before(&self, cutoff: DateTime<Utc>) -> impl Future<Output = Result<u64, StoreError>> + Send {
(**self).delete_events_before(cutoff)
}
fn claim_event(
&self,
scope: ClaimScope<'_>,
kind: EventKind,
now: DateTime<Utc>,
window: TimeDelta,
) -> impl Future<Output = Result<bool, StoreError>> + Send {
(**self).claim_event(scope, kind, now, window)
}
}