pub mod mfa;
pub mod session_manager;
pub mod user_manager;
use crate::config::AuthConfig;
use crate::authentication::credentials::{Credential, CredentialMetadata};
use crate::errors::{AuthError, MfaError, Result};
use crate::methods::{AuthMethod, AuthMethodEnum, MethodResult, MfaChallenge};
use crate::permissions::{Permission, PermissionChecker};
use crate::storage::{AuthStorage, MemoryStorage};
use crate::tokens::{AuthToken, TokenManager};
use crate::utils::rate_limit::RateLimiter;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
pub use mfa::MfaManager;
pub use session_manager::SessionManager;
pub use user_manager::{UserInfo, UserManager};
#[derive(Debug, Clone)]
pub enum AuthResult {
Success(Box<AuthToken>),
MfaRequired(Box<MfaChallenge>),
Failure(String),
}
pub struct AuthFramework {
config: AuthConfig,
methods: HashMap<String, AuthMethodEnum>,
token_manager: TokenManager,
storage: Arc<dyn AuthStorage>,
permission_checker: Arc<RwLock<PermissionChecker>>,
rate_limiter: Option<RateLimiter>,
mfa_manager: MfaManager,
session_manager: SessionManager,
user_manager: UserManager,
initialized: bool,
}
impl AuthFramework {
pub fn new(config: AuthConfig) -> Self {
if let Err(e) = config.validate() {
panic!("Invalid configuration: {}", e);
}
let token_manager = if let Some(secret) = &config.security.secret_key {
if secret.len() < 32 {
eprintln!(
"WARNING: JWT secret is shorter than 32 characters. Consider using a longer secret for better security."
);
}
TokenManager::new_hmac(secret.as_bytes(), "auth-framework", "auth-framework")
} else if let Some(secret) = &config.secret {
if secret.len() < 32 {
eprintln!(
"WARNING: JWT secret is shorter than 32 characters. Consider using a longer secret for better security."
);
}
TokenManager::new_hmac(secret.as_bytes(), "auth-framework", "auth-framework")
} else if let Ok(jwt_secret) = std::env::var("JWT_SECRET") {
if jwt_secret.len() < 32 {
eprintln!(
"WARNING: JWT_SECRET is shorter than 32 characters. Consider using a longer secret for better security."
);
}
TokenManager::new_hmac(jwt_secret.as_bytes(), "auth-framework", "auth-framework")
} else {
panic!(
"JWT secret not set! Please set JWT_SECRET env variable or provide in config.\n\
For security reasons, no default secret is provided.\n\
Generate a secure secret with: openssl rand -base64 32"
);
};
let storage: Arc<dyn AuthStorage> = match &config.storage {
#[cfg(feature = "redis-storage")]
crate::config::StorageConfig::Redis { url, key_prefix } => Arc::new(
crate::storage::RedisStorage::new(url, key_prefix).unwrap_or_else(|e| {
panic!("Failed to create Redis storage: {}", e);
}),
),
_ => Arc::new(MemoryStorage::new()),
};
let rate_limiter = if config.rate_limiting.enabled {
Some(RateLimiter::new(
config.rate_limiting.max_requests,
config.rate_limiting.window,
))
} else {
None
};
let mfa_manager = MfaManager::new(storage.clone());
let session_manager = SessionManager::new(storage.clone());
let user_manager = UserManager::new(storage.clone());
Self {
config,
methods: HashMap::new(),
token_manager,
storage,
permission_checker: Arc::new(RwLock::new(PermissionChecker::new())),
rate_limiter,
mfa_manager,
session_manager,
user_manager,
initialized: false,
}
}
#[cfg(feature = "smskit")]
pub fn new_with_smskit_config(
config: AuthConfig,
smskit_config: crate::auth_modular::mfa::SmsKitConfig,
) -> Result<Self> {
let mut framework = Self::new(config);
framework.mfa_manager = crate::auth_modular::mfa::MfaManager::new_with_smskit_config(
framework.storage.clone(),
smskit_config,
)?;
Ok(framework)
}
pub fn register_method(&mut self, name: impl Into<String>, method: AuthMethodEnum) {
let name = name.into();
info!("Registering authentication method: {}", name);
if let Err(e) = method.validate_config() {
error!("Method '{}' configuration validation failed: {}", name, e);
return;
}
self.methods.insert(name, method);
}
pub async fn initialize(&mut self) -> Result<()> {
if self.initialized {
return Ok(());
}
info!("Initializing authentication framework");
{
let mut checker = self.permission_checker.write().await;
checker.create_default_roles();
}
self.cleanup_expired_data().await?;
self.initialized = true;
info!("Authentication framework initialized successfully");
Ok(())
}
pub async fn authenticate(
&self,
method_name: &str,
credential: Credential,
) -> Result<AuthResult> {
self.authenticate_with_metadata(method_name, credential, CredentialMetadata::new())
.await
}
pub async fn authenticate_with_metadata(
&self,
method_name: &str,
credential: Credential,
metadata: CredentialMetadata,
) -> Result<AuthResult> {
use std::time::Instant;
use tokio::time::{Duration as TokioDuration, sleep};
let start_time = Instant::now();
if !self.initialized {
return Err(AuthError::internal("Framework not initialized"));
}
let result = self
.authenticate_internal(method_name, credential, metadata)
.await;
let min_duration = TokioDuration::from_millis(100);
let elapsed = start_time.elapsed();
if elapsed < min_duration {
sleep(min_duration - elapsed).await;
}
result
}
async fn authenticate_internal(
&self,
method_name: &str,
credential: Credential,
metadata: CredentialMetadata,
) -> Result<AuthResult> {
if let Some(ref rate_limiter) = self.rate_limiter {
let rate_key = format!(
"auth:{}:{}",
method_name,
metadata.client_ip.as_deref().unwrap_or("unknown")
);
if !rate_limiter.is_allowed(&rate_key) {
warn!(
"Rate limit exceeded for method '{}' from IP {:?}",
method_name, metadata.client_ip
);
return Err(AuthError::rate_limit("Too many authentication attempts"));
}
}
let method = self.methods.get(method_name).ok_or_else(|| {
AuthError::auth_method(method_name, "Authentication method not found".to_string())
})?;
debug!(
"Authentication attempt with method '{}' for credential: {}",
method_name,
credential.safe_display()
);
let result = method.authenticate(credential, metadata.clone()).await?;
match &result {
MethodResult::Success(token) => {
info!(
"Authentication successful for user '{}' with method '{}'",
token.user_id, method_name
);
self.storage.store_token(token).await?;
self.log_audit_event("auth_success", &token.user_id, method_name, &metadata)
.await;
Ok(AuthResult::Success(token.clone()))
}
MethodResult::MfaRequired(challenge) => {
info!(
"MFA required for user '{}' with method '{}'",
challenge.user_id, method_name
);
self.mfa_manager
.store_challenge((**challenge).clone())
.await?;
self.log_audit_event("mfa_required", &challenge.user_id, method_name, &metadata)
.await;
Ok(AuthResult::MfaRequired(challenge.clone()))
}
MethodResult::Failure { reason } => {
warn!(
"Authentication failed for method '{}': {}",
method_name, reason
);
self.log_audit_event("auth_failure", "unknown", method_name, &metadata)
.await;
Ok(AuthResult::Failure(reason.clone()))
}
}
}
pub async fn complete_mfa(&self, challenge: MfaChallenge, mfa_code: &str) -> Result<AuthToken> {
debug!("Completing MFA for challenge '{}'", challenge.id);
let stored_challenge = self
.mfa_manager
.get_challenge(&challenge.id)
.await?
.ok_or(MfaError::ChallengeExpired)?;
if stored_challenge.is_expired() {
self.mfa_manager.remove_challenge(&challenge.id).await?;
return Err(MfaError::ChallengeExpired.into());
}
let is_valid = match &stored_challenge.mfa_type {
crate::methods::MfaType::Totp => {
self.mfa_manager
.totp
.verify_code(&stored_challenge.user_id, mfa_code)
.await?
}
crate::methods::MfaType::Sms { .. } => {
self.mfa_manager
.sms
.verify_code(&challenge.id, mfa_code)
.await?
}
crate::methods::MfaType::Email { .. } => {
self.mfa_manager
.email
.verify_code(&challenge.id, mfa_code)
.await?
}
crate::methods::MfaType::BackupCode => {
self.mfa_manager
.backup_codes
.verify_code(&stored_challenge.user_id, mfa_code)
.await?
}
_ => false,
};
if !is_valid {
return Err(MfaError::InvalidCode.into());
}
self.mfa_manager.remove_challenge(&challenge.id).await?;
let token = self.token_manager.create_auth_token(
&challenge.user_id,
vec![], "mfa",
None,
)?;
self.storage.store_token(&token).await?;
info!(
"MFA completed successfully for user '{}'",
challenge.user_id
);
Ok(token)
}
pub async fn validate_token(&self, token: &AuthToken) -> Result<bool> {
if !self.initialized {
return Err(AuthError::internal("Framework not initialized"));
}
if !token.is_valid() {
return Ok(false);
}
self.token_manager.validate_auth_token(token)?;
if let Some(stored_token) = self.storage.get_token(&token.token_id).await? {
let mut updated_token = stored_token;
updated_token.mark_used();
self.storage.update_token(&updated_token).await?;
Ok(true)
} else {
Ok(false)
}
}
pub async fn get_user_info(&self, token: &AuthToken) -> Result<UserInfo> {
if !self.validate_token(token).await? {
return Err(AuthError::auth_method("token", "Invalid token".to_string()));
}
self.user_manager.get_user_info(&token.user_id).await
}
pub async fn check_permission(
&self,
token: &AuthToken,
action: &str,
resource: &str,
) -> Result<bool> {
if !self.validate_token(token).await? {
return Ok(false);
}
let permission = Permission::new(action, resource);
let mut checker = self.permission_checker.write().await;
checker.check_token_permission(token, &permission)
}
pub fn token_manager(&self) -> &TokenManager {
&self.token_manager
}
pub fn mfa_manager(&self) -> &MfaManager {
&self.mfa_manager
}
pub fn session_manager(&self) -> &SessionManager {
&self.session_manager
}
pub fn user_manager(&self) -> &UserManager {
&self.user_manager
}
pub async fn initiate_sms_challenge(&self, user_id: &str) -> Result<String> {
self.mfa_manager.sms.initiate_challenge(user_id).await
}
pub async fn send_sms_code(&self, challenge_id: &str, phone_number: &str) -> Result<()> {
self.mfa_manager
.sms
.send_code(challenge_id, phone_number)
.await
}
pub async fn generate_sms_code(&self, challenge_id: &str) -> Result<String> {
self.mfa_manager.sms.generate_code(challenge_id).await
}
pub async fn verify_sms_code(&self, challenge_id: &str, code: &str) -> Result<bool> {
self.mfa_manager.sms.verify_code(challenge_id, code).await
}
pub async fn cleanup_expired_data(&self) -> Result<()> {
debug!("Cleaning up expired data");
self.storage.cleanup_expired().await?;
self.mfa_manager.cleanup_expired_challenges().await?;
self.session_manager.cleanup_expired_sessions().await?;
if let Some(ref rate_limiter) = self.rate_limiter {
rate_limiter.cleanup();
}
Ok(())
}
pub async fn get_stats(&self) -> Result<AuthStats> {
let mut stats = AuthStats::default();
for method in self.methods.keys() {
stats.registered_methods.push(method.clone());
}
stats.active_mfa_challenges = self.mfa_manager.get_active_challenge_count().await as u64;
Ok(stats)
}
async fn log_audit_event(
&self,
event_type: &str,
user_id: &str,
method: &str,
metadata: &CredentialMetadata,
) {
if self.config.audit.enabled {
let should_log = match event_type {
"auth_success" => self.config.audit.log_success,
"auth_failure" => self.config.audit.log_failures,
"mfa_required" => self.config.audit.log_success,
_ => true,
};
if should_log {
info!(
target: "auth_audit",
event_type = event_type,
user_id = user_id,
method = method,
client_ip = metadata.client_ip.as_deref().unwrap_or("unknown"),
user_agent = metadata.user_agent.as_deref().unwrap_or("unknown"),
timestamp = chrono::Utc::now().to_rfc3339(),
"Authentication event"
);
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AuthStats {
pub registered_methods: Vec<String>,
pub active_mfa_challenges: u64,
pub tokens_issued: u64,
pub auth_attempts: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{AuthConfig, SecurityConfig};
use std::time::Duration;
#[tokio::test]
async fn test_modular_framework_initialization() {
let config = AuthConfig::new().security(SecurityConfig {
min_password_length: 8,
require_password_complexity: false,
password_hash_algorithm: crate::config::PasswordHashAlgorithm::Bcrypt,
jwt_algorithm: crate::config::JwtAlgorithm::HS256,
secret_key: Some("test_secret_key_32_bytes_long!!!!".to_string()),
secure_cookies: false,
cookie_same_site: crate::config::CookieSameSite::Lax,
csrf_protection: false,
session_timeout: Duration::from_secs(3600),
});
let mut framework = AuthFramework::new(config);
assert!(framework.initialize().await.is_ok());
assert!(framework.initialized);
}
#[tokio::test]
async fn test_mfa_manager_access() {
let config = AuthConfig::new().security(SecurityConfig {
min_password_length: 8,
require_password_complexity: false,
password_hash_algorithm: crate::config::PasswordHashAlgorithm::Bcrypt,
jwt_algorithm: crate::config::JwtAlgorithm::HS256,
secret_key: Some("test_secret_key_32_bytes_long!!!!".to_string()),
secure_cookies: false,
cookie_same_site: crate::config::CookieSameSite::Lax,
csrf_protection: false,
session_timeout: Duration::from_secs(3600),
});
let framework = AuthFramework::new(config);
let _mfa_manager = framework.mfa_manager();
let _session_manager = framework.session_manager();
let _user_manager = framework.user_manager();
}
}