use std::sync::Arc; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use chrono::{Duration, Utc}; use domain::auth::{AuthEvent, AuthEventKind, RefreshToken, TokenPair}; use domain::ports::{ AccessTokenIssuer, AuditLog, PasswordHasher, RefreshTokenRepository, UserRepository, }; use domain::user::User; use domain::DomainError; use rand::RngCore; use sha2::{Digest, Sha256}; use uuid::Uuid; pub const REFRESH_TOKEN_TTL_DAYS: i64 = 30; pub struct AuthService { users: Arc, refresh: Arc, audit: Arc, hasher: Arc, tokens: Arc, refresh_ttl: Duration, } impl AuthService { pub fn new( users: Arc, refresh: Arc, audit: Arc, hasher: Arc, tokens: Arc, ) -> Self { Self { users, refresh, audit, hasher, tokens, refresh_ttl: Duration::days(REFRESH_TOKEN_TTL_DAYS), } } pub async fn login( &self, email: &str, password: &str, ip: Option, ) -> Result { let email = email.trim().to_lowercase(); let user = self.users.find_by_email(&email).await?; let valid = user .as_ref() .is_some_and(|u| self.hasher.verify(password, &u.password_hash)); let Some(user) = user.filter(|_| valid) else { self.record(None, &email, AuthEventKind::LoginFailed, ip) .await?; return Err(DomainError::InvalidCredentials); }; if !user.is_active { self.record(Some(user.id), &email, AuthEventKind::LoginFailed, ip) .await?; return Err(DomainError::InactiveUser); } let pair = self.issue_pair(&user, Uuid::new_v4()).await?; self.record(Some(user.id), &email, AuthEventKind::LoginSuccess, ip) .await?; Ok(pair) } pub async fn refresh( &self, refresh_token: &str, ip: Option, ) -> Result { let stored = self .refresh .find_by_hash(&hash_token(refresh_token)) .await? .ok_or(DomainError::InvalidToken)?; let user = self .users .find_by_id(stored.user_id) .await? .ok_or(DomainError::InvalidToken)?; if stored.revoked { // A revoked token is presented again: someone else may hold the rotated one. self.refresh.revoke_family(stored.family).await?; self.record( Some(user.id), &user.email, AuthEventKind::RefreshReuseDetected, ip, ) .await?; return Err(DomainError::InvalidToken); } if !stored.is_valid(Utc::now()) { return Err(DomainError::InvalidToken); } if !user.is_active { return Err(DomainError::InactiveUser); } self.refresh.revoke(stored.id).await?; let pair = self.issue_pair(&user, stored.family).await?; self.record(Some(user.id), &user.email, AuthEventKind::Refresh, ip) .await?; Ok(pair) } pub async fn logout(&self, refresh_token: &str) -> Result<(), DomainError> { if let Some(stored) = self .refresh .find_by_hash(&hash_token(refresh_token)) .await? { self.refresh.revoke_family(stored.family).await?; if let Some(user) = self.users.find_by_id(stored.user_id).await? { self.record(Some(user.id), &user.email, AuthEventKind::Logout, None) .await?; } } Ok(()) } /// Resolve the user behind an access token; fails for invalid tokens and inactive users. pub async fn authenticate(&self, access_token: &str) -> Result { let claims = self.tokens.verify(access_token)?; let user = self .users .find_by_id(claims.sub) .await? .ok_or(DomainError::InvalidToken)?; if !user.is_active { return Err(DomainError::InactiveUser); } Ok(user) } async fn issue_pair(&self, user: &User, family: Uuid) -> Result { let raw = random_token(); self.refresh .insert(&RefreshToken { id: Uuid::new_v4(), user_id: user.id, family, token_hash: hash_token(&raw), expires_at: Utc::now() + self.refresh_ttl, revoked: false, }) .await?; Ok(TokenPair { access_token: self.tokens.issue(user)?, refresh_token: raw, }) } async fn record( &self, user_id: Option, email: &str, kind: AuthEventKind, ip: Option, ) -> Result<(), DomainError> { self.audit .record(&AuthEvent { user_id, email: email.into(), kind, ip, at: Utc::now(), }) .await } } fn random_token() -> String { let mut bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut bytes); URL_SAFE_NO_PAD.encode(bytes) } fn hash_token(raw: &str) -> String { format!("{:x}", Sha256::digest(raw.as_bytes())) }