//! Authentication utilities for Veza Rust services //! //! This module provides authentication-related utilities and helpers. use uuid::Uuid; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::{VezaError, VezaResult}; /// JWT claims structure #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JwtClaims { pub sub: Uuid, // Subject (user ID) pub iss: String, // Issuer pub aud: String, // Audience pub exp: u64, // Expiration time pub iat: u64, // Issued at pub nbf: Option, // Not before pub jti: Option, // JWT ID pub username: String, pub email: String, pub roles: Vec, } impl JwtClaims { /// Create new JWT claims pub fn new( user_id: Uuid, username: String, email: String, roles: Vec, issuer: String, audience: String, ttl_seconds: u64, ) -> Self { let now = Utc::now().timestamp() as u64; Self { sub: user_id, iss: issuer, aud: audience, exp: now + ttl_seconds, iat: now, nbf: Some(now), jti: Some(uuid::Uuid::new_v4().to_string()), username, email, roles, } } /// Check if claims are expired pub fn is_expired(&self) -> bool { let now = Utc::now().timestamp() as u64; self.exp < now } /// Check if claims are valid (not before) pub fn is_valid(&self) -> bool { let now = Utc::now().timestamp() as u64; if let Some(nbf) = self.nbf { now >= nbf } else { true } } /// Check if user has role pub fn has_role(&self, role: &str) -> bool { self.roles.contains(&role.to_string()) } /// Check if user has any of the roles pub fn has_any_role(&self, roles: &[&str]) -> bool { roles.iter().any(|role| self.has_role(role)) } } /// Authentication result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthResult { pub access_token: String, pub refresh_token: String, pub token_type: String, pub expires_in: u64, pub user_id: Uuid, pub username: String, pub email: String, pub roles: Vec, } /// Password validation result #[derive(Debug, Clone)] pub struct PasswordValidation { pub is_valid: bool, pub errors: Vec, pub strength_score: u8, // 0-100 } impl PasswordValidation { /// Create new password validation pub fn new(password: &str) -> Self { let mut errors = Vec::new(); let mut strength_score = 0u8; // Length check if password.len() >= 8 { strength_score += 20; } else { errors.push("Password must be at least 8 characters long".to_string()); } if password.len() >= 12 { strength_score += 10; } // Character variety checks if password.chars().any(|c| c.is_uppercase()) { strength_score += 20; } else { errors.push("Password must contain at least one uppercase letter".to_string()); } if password.chars().any(|c| c.is_lowercase()) { strength_score += 20; } else { errors.push("Password must contain at least one lowercase letter".to_string()); } if password.chars().any(|c| c.is_numeric()) { strength_score += 20; } else { errors.push("Password must contain at least one digit".to_string()); } if password.chars().any(|c| c.is_ascii_punctuation()) { strength_score += 20; } else { errors.push("Password must contain at least one special character".to_string()); } // Additional strength checks if password.len() >= 16 { strength_score += 10; } if password.chars().filter(|c| c.is_ascii_punctuation()).count() >= 2 { strength_score += 10; } let is_valid = errors.is_empty(); Self { is_valid, errors, strength_score, } } } /// Session information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionInfo { pub session_id: Uuid, pub user_id: Uuid, pub ip_address: Option, pub user_agent: Option, pub created_at: DateTime, pub expires_at: DateTime, pub last_activity: DateTime, pub is_active: bool, } impl SessionInfo { /// Check if session is expired pub fn is_expired(&self) -> bool { Utc::now() > self.expires_at } /// Check if session is valid pub fn is_valid(&self) -> bool { self.is_active && !self.is_expired() } /// Update last activity pub fn update_activity(&mut self) { self.last_activity = Utc::now(); } } /// Permission check result #[derive(Debug, Clone)] pub struct PermissionCheck { pub allowed: bool, pub reason: Option, } impl PermissionCheck { /// Create allowed permission check pub fn allowed() -> Self { Self { allowed: true, reason: None, } } /// Create denied permission check pub fn denied(reason: String) -> Self { Self { allowed: false, reason: Some(reason), } } } /// Role-based access control pub struct RBAC { roles: std::collections::HashMap>, } impl RBAC { /// Create new RBAC instance pub fn new() -> Self { Self { roles: std::collections::HashMap::new(), } } /// Add role with permissions pub fn add_role(&mut self, role: &str, permissions: Vec) { self.roles.insert(role.to_string(), permissions); } /// Check if role has permission pub fn has_permission(&self, role: &str, permission: &str) -> bool { self.roles .get(role) .map(|permissions| permissions.contains(&permission.to_string())) .unwrap_or(false) } /// Check if user has permission (check all roles) pub fn user_has_permission(&self, user_roles: &[String], permission: &str) -> bool { user_roles.iter().any(|role| self.has_permission(role, permission)) } /// Get all permissions for a role pub fn get_role_permissions(&self, role: &str) -> Vec { self.roles .get(role) .cloned() .unwrap_or_default() } } impl Default for RBAC { fn default() -> Self { let mut rbac = Self::new(); // Add default roles rbac.add_role("admin", vec![ "user:read".to_string(), "user:write".to_string(), "user:delete".to_string(), "conversation:read".to_string(), "conversation:write".to_string(), "conversation:delete".to_string(), "track:read".to_string(), "track:write".to_string(), "track:delete".to_string(), "system:admin".to_string(), ]); rbac.add_role("user", vec![ "user:read".to_string(), "conversation:read".to_string(), "conversation:write".to_string(), "track:read".to_string(), "track:write".to_string(), ]); rbac.add_role("guest", vec![ "track:read".to_string(), ]); rbac } } /// Generate secure random password pub fn generate_secure_password(length: usize) -> VezaResult { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; let mut rng = rand::thread_rng(); let password: String = (0..length) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect(); Ok(password) } /// Generate recovery code pub fn generate_recovery_code() -> VezaResult { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let mut rng = rand::thread_rng(); let code: String = (0..8) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect(); Ok(code) } /// Generate TOTP secret pub fn generate_totp_secret() -> VezaResult { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let mut rng = rand::thread_rng(); let secret: String = (0..32) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect(); Ok(secret) } /// Validate TOTP code pub fn validate_totp_code(secret: &str, code: &str, _window: i64) -> VezaResult { use totp_rs::{TOTP, Algorithm, Secret}; // Use Secret::Encoded to handle base32 string directly let secret_obj = Secret::Encoded(secret.to_string()); // Use TOTP::new with 5 arguments (basic validation) let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, secret_obj.to_bytes() .map_err(|e| VezaError::Auth(format!("Invalid TOTP secret: {}", e)))?, ).map_err(|e| VezaError::Auth(format!("Invalid TOTP secret: {}", e)))?; let is_valid = totp.check_current(code) .map_err(|e| VezaError::Auth(format!("TOTP validation error: {}", e)))?; Ok(is_valid) } /// Generate QR code data for TOTP setup pub fn generate_totp_qr_data(secret: &str, username: &str, issuer: &str) -> String { format!( "otpauth://totp/{}:{}?secret={}&issuer={}", issuer, username, secret, issuer ) }