use std::fmt;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use secrecy::{ExposeSecret, SecretString};
use zeroize::Zeroizing;
use crate::{risk::Assessment, store::SessionRecord, subject::Subject};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AuthSession<S> {
pub session_id: Uuid,
pub subject: S,
pub created_at: DateTime<Utc>,
pub authenticated_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
impl<S> AuthSession<S> {
pub(crate) fn new(session: &SessionRecord, subject: S) -> Self {
Self {
session_id: session.id,
subject,
created_at: session.created_at,
authenticated_at: session.authenticated_at,
expires_at: session.expires_at,
}
}
}
#[derive(Clone)]
pub struct IssuedToken {
pub(crate) value: SecretString,
pub(crate) expires_at: DateTime<Utc>,
pub(crate) set_cookie: Zeroizing<String>,
}
impl IssuedToken {
pub fn expose(&self) -> &str {
self.value.expose_secret()
}
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
pub fn set_cookie(&self) -> &str {
&self.set_cookie
}
}
impl fmt::Debug for IssuedToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IssuedToken")
.field("token", &"<redacted>")
.field("expires_at", &self.expires_at)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct NewSession {
pub session_id: Uuid,
pub token: IssuedToken,
}
#[derive(Clone, Debug)]
pub enum Outcome<S> {
Authenticated(Authenticated<S>),
Rejected(Rejection),
}
impl<S> Outcome<S> {
pub fn session(&self) -> Option<&AuthSession<S>> {
match self {
Outcome::Authenticated(a) => Some(&a.session),
Outcome::Rejected(_) => None,
}
}
pub fn rejection(&self) -> Option<Rejection> {
match self {
Outcome::Authenticated(_) => None,
Outcome::Rejected(r) => Some(*r),
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Authenticated<S> {
pub session: AuthSession<S>,
pub rotated: Option<IssuedToken>,
pub assessment: Assessment,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Rejection {
NotAuthenticated,
Expired,
Revoked,
StepUpRequired,
}
impl Rejection {
pub const fn as_str(self) -> &'static str {
match self {
Rejection::NotAuthenticated => "not_authenticated",
Rejection::Expired => "session_expired",
Rejection::Revoked => "session_revoked",
Rejection::StepUpRequired => "step_up_required",
}
}
pub const fn clears_cookie(self) -> bool {
!matches!(self, Rejection::StepUpRequired)
}
pub const fn clears_site_data(self) -> bool {
matches!(self, Rejection::Revoked)
}
}
impl fmt::Display for Rejection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug)]
pub enum StepUpOutcome {
Completed(IssuedToken),
SubjectMismatch,
Rejected(Rejection),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct SessionInfo<S> {
pub session_id: Uuid,
pub subject: S,
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 browser: Option<String>,
pub os: Option<String>,
pub country: Option<String>,
}
impl<S: Subject> SessionInfo<S> {
pub(crate) fn from_record(record: &SessionRecord) -> Option<Self> {
let fp = &record.baseline.fingerprint;
Some(Self {
session_id: record.id,
subject: crate::subject::decode(&record.subject)?,
created_at: record.created_at,
authenticated_at: record.authenticated_at,
last_seen_at: record.last_seen_at,
expires_at: record.expires_at,
step_up_required: record.step_up_required,
browser: fp.browser.clone(),
os: fp.os.clone(),
country: fp.country.clone(),
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PurgeReport {
pub sessions: u64,
pub events: u64,
}