use std::sync::Arc;
use uuid::Uuid;
use crate::errors::AppError;
use crate::repositories::SessionRepository;
pub const DEFAULT_STEP_UP_MAX_AGE_SECS: i64 = 300;
pub struct StepUpService {
session_repo: Arc<dyn SessionRepository>,
max_age_secs: i64,
}
impl StepUpService {
pub fn new(session_repo: Arc<dyn SessionRepository>) -> Self {
Self {
session_repo,
max_age_secs: DEFAULT_STEP_UP_MAX_AGE_SECS,
}
}
pub fn with_max_age(session_repo: Arc<dyn SessionRepository>, max_age_secs: i64) -> Self {
Self {
session_repo,
max_age_secs,
}
}
pub async fn has_recent_strong_auth(&self, session_id: Uuid) -> Result<bool, AppError> {
let session = self
.session_repo
.find_by_id(session_id)
.await?
.ok_or_else(|| AppError::NotFound("Session not found".into()))?;
if !session.is_valid() {
return Err(AppError::Unauthorized("Session is invalid".into()));
}
Ok(session.has_recent_strong_auth(self.max_age_secs))
}
pub async fn record_strong_auth(&self, session_id: Uuid) -> Result<(), AppError> {
self.session_repo.update_strong_auth_at(session_id).await
}
pub async fn require_step_up(&self, session_id: Uuid) -> Result<(), AppError> {
if self.has_recent_strong_auth(session_id).await? {
Ok(())
} else {
Err(AppError::StepUpRequired)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repositories::InMemorySessionRepository;
use crate::repositories::SessionEntity;
use chrono::{Duration, Utc};
fn create_test_session(user_id: Uuid) -> SessionEntity {
SessionEntity::new(
user_id,
"test_hash".to_string(),
Utc::now() + Duration::hours(1),
None,
None,
)
}
#[tokio::test]
async fn test_no_strong_auth_by_default() {
let session_repo = Arc::new(InMemorySessionRepository::new());
let service = StepUpService::new(session_repo.clone());
let user_id = Uuid::new_v4();
let session = create_test_session(user_id);
let session = session_repo.create(session).await.unwrap();
assert!(!service.has_recent_strong_auth(session.id).await.unwrap());
}
#[tokio::test]
async fn test_strong_auth_recorded() {
let session_repo = Arc::new(InMemorySessionRepository::new());
let service = StepUpService::new(session_repo.clone());
let user_id = Uuid::new_v4();
let session = create_test_session(user_id);
let session = session_repo.create(session).await.unwrap();
service.record_strong_auth(session.id).await.unwrap();
assert!(service.has_recent_strong_auth(session.id).await.unwrap());
}
#[tokio::test]
async fn test_require_step_up_fails_without_strong_auth() {
let session_repo = Arc::new(InMemorySessionRepository::new());
let service = StepUpService::new(session_repo.clone());
let user_id = Uuid::new_v4();
let session = create_test_session(user_id);
let session = session_repo.create(session).await.unwrap();
let result = service.require_step_up(session.id).await;
assert!(matches!(result, Err(AppError::StepUpRequired)));
}
#[tokio::test]
async fn test_require_step_up_succeeds_with_strong_auth() {
let session_repo = Arc::new(InMemorySessionRepository::new());
let service = StepUpService::new(session_repo.clone());
let user_id = Uuid::new_v4();
let session = create_test_session(user_id);
let session = session_repo.create(session).await.unwrap();
service.record_strong_auth(session.id).await.unwrap();
let result = service.require_step_up(session.id).await;
assert!(result.is_ok());
}
}