use crate::authn::{
factor::FactorKind,
ids::{DeviceId, TenantId, UserId},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub const SESSION_DATA_VERSION: u8 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
#[serde(default = "default_version")]
pub version: u8,
pub auth_state: AuthState,
pub fingerprint: Option<String>,
#[serde(default)]
pub device_id: Option<DeviceId>,
pub custom: serde_json::Value,
}
fn default_version() -> u8 {
1
}
impl Default for SessionData {
fn default() -> Self {
Self {
version: SESSION_DATA_VERSION,
auth_state: AuthState::default(),
fingerprint: None,
device_id: None,
custom: serde_json::Value::default(),
}
}
}
impl SessionData {
pub fn migrate(&mut self) -> bool {
if self.version >= SESSION_DATA_VERSION {
return false;
}
if self.version == 1 {
self.version = 2;
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(tag = "kind")]
pub enum AuthState {
#[default]
Guest,
Identifying {
user_id: UserId,
tenant_id: TenantId,
},
Authenticating {
user_id: UserId,
tenant_id: TenantId,
method_name: Arc<str>,
remaining: Vec<FactorKind>,
#[serde(default)]
completed: Vec<FactorKind>,
attempt_count: u32,
last_attempt: Option<DateTime<Utc>>,
},
Authenticated {
user_id: UserId,
tenant_id: TenantId,
authn_time: DateTime<Utc>,
#[serde(default)]
factors_completed: Vec<FactorKind>,
},
PendingWorkflow {
user_id: UserId,
tenant_id: TenantId,
workflow: WorkflowState,
},
}
impl AuthState {
pub fn user_id(&self) -> Option<&UserId> {
match self {
AuthState::Guest => None,
AuthState::Identifying { user_id, .. } => Some(user_id),
AuthState::Authenticating { user_id, .. } => Some(user_id),
AuthState::Authenticated { user_id, .. } => Some(user_id),
AuthState::PendingWorkflow { user_id, .. } => Some(user_id),
}
}
pub fn tenant_id(&self) -> Option<&TenantId> {
match self {
AuthState::Guest => None,
AuthState::Identifying { tenant_id, .. } => Some(tenant_id),
AuthState::Authenticating { tenant_id, .. } => Some(tenant_id),
AuthState::Authenticated { tenant_id, .. } => Some(tenant_id),
AuthState::PendingWorkflow { tenant_id, .. } => Some(tenant_id),
}
}
pub fn is_authenticated(&self) -> bool {
matches!(self, AuthState::Authenticated { .. })
}
pub fn is_guest(&self) -> bool {
matches!(self, AuthState::Guest)
}
pub fn is_authenticating(&self) -> bool {
matches!(self, AuthState::Authenticating { .. })
}
pub(crate) fn set_identifying(&mut self, user_id: UserId, tenant_id: TenantId) {
*self = AuthState::Identifying { user_id, tenant_id };
}
pub(crate) fn begin_authenticating(
&mut self,
user_id: UserId,
tenant_id: TenantId,
method_name: Arc<str>,
factors: Vec<FactorKind>,
) {
*self = AuthState::Authenticating {
user_id,
tenant_id,
method_name,
remaining: factors,
completed: Vec::new(),
attempt_count: 0,
last_attempt: None,
};
}
pub(crate) fn set_authenticated(
&mut self,
user_id: UserId,
tenant_id: TenantId,
authn_time: DateTime<Utc>,
) {
*self = AuthState::Authenticated {
user_id,
tenant_id,
authn_time,
factors_completed: Vec::new(),
};
}
pub(crate) fn set_pending_workflow(
&mut self,
user_id: UserId,
tenant_id: TenantId,
workflow: WorkflowState,
) {
*self = AuthState::PendingWorkflow {
user_id,
tenant_id,
workflow,
};
}
pub(crate) fn advance_factor(
&mut self,
kind: &FactorKind,
authn_time: DateTime<Utc>,
) -> AdvanceOutcome {
match self {
AuthState::Authenticating {
user_id,
tenant_id,
remaining,
completed,
..
} => {
if let Some(pos) = remaining.iter().position(|k| k == kind) {
let removed = remaining.remove(pos);
completed.push(removed);
}
if remaining.is_empty() {
let uid = *user_id;
let tid = *tenant_id;
let factors = std::mem::take(completed);
*self = AuthState::Authenticated {
user_id: uid,
tenant_id: tid,
authn_time,
factors_completed: factors,
};
AdvanceOutcome::Completed
} else {
AdvanceOutcome::StillAuthenticating
}
}
_ => AdvanceOutcome::NotApplicable,
}
}
pub(crate) fn record_attempt_at(&mut self, now: DateTime<Utc>) {
if let AuthState::Authenticating {
attempt_count,
last_attempt,
..
} = self
{
*attempt_count += 1;
*last_attempt = Some(now);
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AdvanceOutcome {
NotApplicable,
StillAuthenticating,
Completed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WorkflowState {
pub kind: WorkflowKind,
pub current_step: u32,
pub total_steps: u32,
pub initiated_at: DateTime<Utc>,
}
impl WorkflowState {
pub fn new(kind: WorkflowKind, total_steps: u32, now: DateTime<Utc>) -> Self {
Self {
kind,
current_step: 0,
total_steps,
initiated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WorkflowKind {
Signup,
PasswordReset,
EmailVerification,
Custom(Arc<str>),
}
#[cfg(test)]
mod tests;