use argon2::{ Argon2, password_hash::{PasswordHasher, PasswordVerifier, SaltString}, }; use domain::{AuthPort, PasswordHashPort, UserId}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use rand_core::OsRng; use serde::{Deserialize, Serialize}; pub struct AuthConfig { pub secret: String, pub ttl_seconds: u64, } impl AuthConfig { pub fn from_env() -> Result { let secret = std::env::var("JWT_SECRET") .map_err(|_| "JWT_SECRET env var is required".to_string())?; if secret.is_empty() { return Err("JWT_SECRET must not be empty".into()); } let ttl_seconds = std::env::var("JWT_TTL_SECONDS") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(3600u64); Ok(Self { secret, ttl_seconds, }) } } #[derive(Serialize, Deserialize)] struct Claims { sub: u32, exp: u64, } pub struct JwtAuthService { config: AuthConfig, } impl JwtAuthService { pub fn new(config: AuthConfig) -> Self { Self { config } } } impl AuthPort for JwtAuthService { fn generate_token(&self, user_id: UserId) -> String { let exp = jsonwebtoken::get_current_timestamp() + self.config.ttl_seconds; let claims = Claims { sub: user_id, exp }; encode( &Header::default(), &claims, &EncodingKey::from_secret(self.config.secret.as_bytes()), ) .expect("JWT encoding should not fail") } fn validate_token(&self, token: &str) -> Option { let data = decode::( token, &DecodingKey::from_secret(self.config.secret.as_bytes()), &Validation::default(), ) .ok()?; Some(data.claims.sub) } } pub struct Argon2Hasher; impl PasswordHashPort for Argon2Hasher { async fn hash(&self, plain: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(plain.as_bytes(), &salt) .map_err(|e| e.to_string())? .to_string(); Ok(hash) } async fn verify(&self, plain: &str, hash: &str) -> Result { let parsed = argon2::password_hash::PasswordHash::new(hash).map_err(|e| e.to_string())?; Ok(Argon2::default() .verify_password(plain.as_bytes(), &parsed) .is_ok()) } }