1pub mod authorization_manager;
61pub mod mfa;
62pub mod session_manager;
63pub mod user_manager;
64
65use crate::authentication::credentials::{Credential, CredentialMetadata};
66use crate::config::AuthConfig;
67use crate::errors::{AuthError, MfaError, Result};
68use crate::methods::{AuthMethod, AuthMethodEnum, MethodResult, MfaChallenge};
69use crate::permissions::{Permission, PermissionChecker};
70use crate::storage::{AuthStorage, MemoryStorage};
71use crate::tokens::{AuthToken, TokenManager};
72use crate::utils::rate_limit::RateLimiter;
73use std::collections::HashMap;
74use std::sync::Arc;
75use tokio::sync::RwLock;
76use tracing::{debug, error, info, warn};
77
78pub use authorization_manager::AuthorizationManager;
79pub use mfa::MfaManager;
80pub use session_manager::SessionManager;
81pub use user_manager::{UserInfo, UserManager};
82
83pub use crate::auth::AuthResult;
85
86pub struct AuthFramework {
88 config: AuthConfig,
90
91 methods: HashMap<String, AuthMethodEnum>,
93
94 token_manager: TokenManager,
96
97 storage: Arc<dyn AuthStorage>,
99
100 permission_checker: Arc<RwLock<PermissionChecker>>,
102
103 rate_limiter: Option<RateLimiter>,
105
106 mfa_manager: MfaManager,
108
109 session_manager: SessionManager,
111
112 user_manager: UserManager,
114
115 initialized: bool,
117}
118
119impl AuthFramework {
120 pub fn new(config: AuthConfig) -> crate::errors::Result<Self> {
134 Self::try_new(config)
135 }
136
137 pub fn try_new(config: AuthConfig) -> crate::errors::Result<Self> {
147 config.validate().map_err(|e| {
149 crate::errors::AuthError::configuration(format!("Invalid configuration: {e}"))
150 })?;
151
152 let token_manager = if let Some(secret) = &config.security.secret_key {
154 if secret.len() < 32 {
155 tracing::warn!(
156 "JWT secret is shorter than 32 characters. Consider using a longer secret for better security."
157 );
158 }
159 TokenManager::new_hmac(secret.as_bytes(), "auth-framework", "auth-framework")
160 } else if let Some(secret) = &config.secret {
161 if secret.len() < 32 {
162 tracing::warn!(
163 "JWT secret is shorter than 32 characters. Consider using a longer secret for better security."
164 );
165 }
166 TokenManager::new_hmac(secret.as_bytes(), "auth-framework", "auth-framework")
167 } else if let Ok(jwt_secret) = std::env::var("JWT_SECRET") {
168 if jwt_secret.len() < 32 {
169 tracing::warn!(
170 "JWT_SECRET is shorter than 32 characters. Consider using a longer secret for better security."
171 );
172 }
173 TokenManager::new_hmac(jwt_secret.as_bytes(), "auth-framework", "auth-framework")
174 } else {
175 return Err(crate::errors::AuthError::configuration(
176 "JWT secret not set! Please set JWT_SECRET env variable or provide in config.\n\
177 For security reasons, no default secret is provided.\n\
178 Generate a secure secret with: openssl rand -base64 32",
179 ));
180 };
181
182 let storage: Arc<dyn AuthStorage> = match &config.storage {
184 #[cfg(feature = "redis-storage")]
185 crate::config::StorageConfig::Redis { url, key_prefix } => Arc::new(
186 crate::storage::RedisStorage::new(url, key_prefix).map_err(|e| {
187 crate::errors::AuthError::configuration(format!(
188 "Failed to create Redis storage: {e}"
189 ))
190 })?,
191 ),
192 _ => Arc::new(MemoryStorage::new()),
193 };
194
195 let rate_limiter = if config.rate_limiting.enabled {
197 Some(RateLimiter::new(
198 config.rate_limiting.max_requests,
199 config.rate_limiting.window,
200 ))
201 } else {
202 None
203 };
204
205 let mfa_manager = MfaManager::new(storage.clone());
207 let session_manager = SessionManager::new(storage.clone());
208 let user_manager = UserManager::new(storage.clone());
209
210 Ok(Self {
211 config,
212 methods: HashMap::new(),
213 token_manager,
214 storage,
215 permission_checker: Arc::new(RwLock::new(PermissionChecker::new())),
216 rate_limiter,
217 mfa_manager,
218 session_manager,
219 user_manager,
220 initialized: false,
221 })
222 }
223
224 pub fn replace_storage(&mut self, storage: Arc<dyn AuthStorage>) {
229 self.storage = storage.clone();
231
232 self.mfa_manager = MfaManager::new(self.storage.clone());
234 self.session_manager = SessionManager::new(self.storage.clone());
235 self.user_manager = UserManager::new(self.storage.clone());
236 }
237
238 pub fn new_with_storage(
245 config: AuthConfig,
246 storage: Arc<dyn AuthStorage>,
247 ) -> crate::errors::Result<Self> {
248 let mut framework = Self::new(config)?;
249 framework.replace_storage(storage);
250 Ok(framework)
251 }
252
253 #[cfg(feature = "smskit")]
260 pub fn new_with_smskit_config(
261 config: AuthConfig,
262 smskit_config: crate::auth_modular::mfa::SmsKitConfig,
263 ) -> Result<Self> {
264 let mut framework = Self::new(config)?;
266
267 framework.mfa_manager = crate::auth_modular::mfa::MfaManager::new_with_smskit_config(
269 framework.storage.clone(),
270 smskit_config,
271 )?;
272
273 Ok(framework)
274 }
275
276 pub fn register_method(&mut self, name: impl Into<String>, method: AuthMethodEnum) {
283 let name = name.into();
284 info!("Registering authentication method: {}", name);
285
286 if let Err(e) = method.validate_config() {
288 error!("Method '{}' configuration validation failed: {}", name, e);
289 return;
290 }
291
292 self.methods.insert(name, method);
293 }
294
295 pub async fn initialize(&mut self) -> Result<()> {
305 if self.initialized {
306 return Ok(());
307 }
308
309 info!("Initializing authentication framework");
310
311 {
313 let mut checker = self.permission_checker.write().await;
314 checker.create_default_roles();
315 }
316
317 self.cleanup_expired_data().await?;
319
320 self.initialized = true;
321 info!("Authentication framework initialized successfully");
322
323 Ok(())
324 }
325
326 pub async fn authenticate(
341 &self,
342 method_name: &str,
343 credential: Credential,
344 ) -> Result<AuthResult> {
345 self.authenticate_with_metadata(method_name, credential, CredentialMetadata::new())
346 .await
347 }
348
349 pub async fn authenticate_with_metadata(
361 &self,
362 method_name: &str,
363 credential: Credential,
364 metadata: CredentialMetadata,
365 ) -> Result<AuthResult> {
366 use std::time::Instant;
367 use tokio::time::{Duration as TokioDuration, sleep};
368
369 let start_time = Instant::now();
370
371 if !self.initialized {
372 return Err(AuthError::internal("Framework not initialized"));
373 }
374
375 let result = self
377 .authenticate_internal(method_name, credential, metadata)
378 .await;
379
380 let min_duration = TokioDuration::from_millis(100);
382 let elapsed = start_time.elapsed();
383 if elapsed < min_duration {
384 sleep(min_duration - elapsed).await;
385 }
386
387 result
388 }
389
390 async fn authenticate_internal(
392 &self,
393 method_name: &str,
394 credential: Credential,
395 metadata: CredentialMetadata,
396 ) -> Result<AuthResult> {
397 if let Some(ref rate_limiter) = self.rate_limiter {
399 let rate_key = format!(
400 "auth:{}:{}",
401 method_name,
402 metadata.client_ip.as_deref().unwrap_or("unknown")
403 );
404
405 if !rate_limiter.is_allowed(&rate_key) {
406 warn!(
407 "Rate limit exceeded for method '{}' from IP {:?}",
408 method_name, metadata.client_ip
409 );
410 return Err(AuthError::rate_limit("Too many authentication attempts"));
411 }
412 }
413
414 if method_name == "jwt" {
415 return match credential {
416 Credential::Jwt { token } | Credential::Bearer { token } => {
417 self.authenticate_jwt_builtin(&token, &metadata, "jwt")
418 .await
419 }
420 _ => Ok(AuthResult::Failure(
421 "JWT authentication expects Credential::jwt or Credential::bearer".to_string(),
422 )),
423 };
424 }
425
426 if matches!(method_name, "api_key" | "api-key") {
427 return match credential {
428 Credential::ApiKey { key } => {
429 self.authenticate_api_key_builtin(&key, &metadata, "api_key")
430 .await
431 }
432 _ => Ok(AuthResult::Failure(
433 "API key authentication expects Credential::api_key".to_string(),
434 )),
435 };
436 }
437
438 if method_name == "oauth2" {
439 return self
440 .authenticate_oauth2_builtin(credential, &metadata)
441 .await;
442 }
443
444 let method = self.methods.get(method_name).ok_or_else(|| {
446 AuthError::auth_method(method_name, "Authentication method not found".to_string())
447 })?;
448
449 debug!(
451 "Authentication attempt with method '{}' for credential: {}",
452 method_name,
453 credential.safe_display()
454 );
455
456 let result = method.authenticate(credential, metadata.clone()).await?;
458
459 match &result {
461 MethodResult::Success(token) => {
462 info!(
463 "Authentication successful for user '{}' with method '{}'",
464 token.user_id, method_name
465 );
466
467 self.storage.store_token(token).await?;
469
470 self.log_audit_event("auth_success", &token.user_id, method_name, &metadata)
472 .await;
473
474 Ok(AuthResult::Success(token.clone()))
475 }
476
477 MethodResult::MfaRequired(challenge) => {
478 info!(
479 "MFA required for user '{}' with method '{}'",
480 challenge.user_id, method_name
481 );
482
483 self.mfa_manager
485 .store_challenge((**challenge).clone())
486 .await?;
487
488 self.log_audit_event("mfa_required", &challenge.user_id, method_name, &metadata)
490 .await;
491
492 Ok(AuthResult::MfaRequired(challenge.clone()))
493 }
494
495 MethodResult::Failure { reason } => {
496 warn!(
497 "Authentication failed for method '{}': {}",
498 method_name, reason
499 );
500
501 self.log_audit_event("auth_failure", "unknown", method_name, &metadata)
503 .await;
504
505 Ok(AuthResult::Failure(reason.clone()))
506 }
507 }
508 }
509
510 async fn authenticate_jwt_builtin(
511 &self,
512 token: &str,
513 metadata: &CredentialMetadata,
514 auth_method: &str,
515 ) -> Result<AuthResult> {
516 if token.is_empty() {
517 return Ok(AuthResult::Failure("JWT token cannot be empty".to_string()));
518 }
519
520 match self.token_manager.validate_jwt_token(token) {
521 Ok(claims) => {
522 let token =
523 Self::build_validated_jwt_auth_token(token, claims, metadata, auth_method);
524 Ok(AuthResult::Success(Box::new(token)))
525 }
526 Err(error) => {
527 if let Some(reason) = Self::credential_failure_reason(&error) {
528 Ok(AuthResult::Failure(reason))
529 } else {
530 Err(error)
531 }
532 }
533 }
534 }
535
536 async fn authenticate_api_key_builtin(
537 &self,
538 api_key: &str,
539 metadata: &CredentialMetadata,
540 auth_method: &str,
541 ) -> Result<AuthResult> {
542 if api_key.is_empty() {
543 return Ok(AuthResult::Failure("API key cannot be empty".to_string()));
544 }
545
546 match self.user_manager.validate_api_key(api_key).await {
547 Ok(user) => {
548 let scopes: Vec<String> = if user.roles.is_empty() {
549 vec!["api_user".to_string()]
550 } else {
551 user.roles.to_vec()
552 };
553 let mut token = self
554 .token_manager
555 .create_auth_token(&user.id, scopes.clone(), auth_method, None)?
556 .with_roles(user.roles)
557 .with_scopes(scopes);
558 token.metadata.issued_ip = metadata.client_ip.clone();
559 token.metadata.user_agent = metadata.user_agent.clone();
560 Ok(AuthResult::Success(Box::new(token)))
561 }
562 Err(error) => {
563 if let Some(reason) = Self::credential_failure_reason(&error) {
564 Ok(AuthResult::Failure(reason))
565 } else {
566 Err(error)
567 }
568 }
569 }
570 }
571
572 async fn authenticate_oauth2_builtin(
573 &self,
574 credential: Credential,
575 metadata: &CredentialMetadata,
576 ) -> Result<AuthResult> {
577 match credential {
578 Credential::OAuth {
579 authorization_code, ..
580 } => {
581 if authorization_code.is_empty() {
582 return Ok(AuthResult::Failure(
583 "OAuth authorization code cannot be empty".to_string(),
584 ));
585 }
586 Ok(AuthResult::Failure(
587 "OAuth 2.0 authorization codes must be exchanged through an OAuth provider or server endpoint before authentication completes"
588 .to_string(),
589 ))
590 }
591 Credential::OAuthRefresh { refresh_token } => {
592 if refresh_token.is_empty() {
593 return Ok(AuthResult::Failure(
594 "OAuth refresh token cannot be empty".to_string(),
595 ));
596 }
597 Ok(AuthResult::Failure(
598 "OAuth 2.0 refresh tokens must be exchanged through an OAuth provider or server endpoint before authentication completes"
599 .to_string(),
600 ))
601 }
602 Credential::Jwt { token }
603 | Credential::Bearer { token }
604 | Credential::OpenIdConnect { id_token: token, .. } => {
605 self.authenticate_jwt_builtin(&token, metadata, "oauth2").await
606 }
607 _ => Ok(AuthResult::Failure(
608 "OAuth2 authentication expects Credential::oauth_code, Credential::oauth_refresh, Credential::jwt, Credential::bearer, or Credential::openid_connect"
609 .to_string(),
610 )),
611 }
612 }
613
614 fn build_validated_jwt_auth_token(
615 raw_token: &str,
616 claims: crate::tokens::JwtClaims,
617 metadata: &CredentialMetadata,
618 auth_method: &str,
619 ) -> AuthToken {
620 let crate::tokens::JwtClaims {
621 sub,
622 iss,
623 exp,
624 iat,
625 jti,
626 scope,
627 permissions,
628 roles,
629 client_id,
630 ..
631 } = claims;
632
633 let now = chrono::Utc::now();
634 let issued_at = chrono::DateTime::<chrono::Utc>::from_timestamp(iat, 0).unwrap_or(now);
635 let expires_at = chrono::DateTime::<chrono::Utc>::from_timestamp(exp, 0)
636 .unwrap_or(now + chrono::Duration::hours(1));
637 let lifetime = (expires_at - now)
638 .to_std()
639 .unwrap_or_else(|_| std::time::Duration::from_secs(1));
640 let scopes = if scope.trim().is_empty() {
641 Vec::new()
642 } else {
643 scope.split_whitespace().map(str::to_string).collect()
644 };
645
646 let mut token = AuthToken::new(sub.clone(), raw_token.to_string(), lifetime, auth_method)
647 .with_scopes(scopes)
648 .with_permissions(permissions.unwrap_or_default())
649 .with_roles(roles.unwrap_or_default());
650 token.token_id = jti;
651 token.subject = Some(sub);
652 token.issuer = Some(iss);
653 token.issued_at = issued_at;
654 token.expires_at = expires_at;
655 token.metadata.issued_ip = metadata.client_ip.clone();
656 token.metadata.user_agent = metadata.user_agent.clone();
657 if let Some(client_id) = client_id {
658 token.client_id = Some(client_id);
659 }
660 token
661 }
662
663 fn credential_failure_reason(error: &AuthError) -> Option<String> {
664 match error {
665 AuthError::Token(_) | AuthError::Jwt(_) => Some(error.to_string()),
666 AuthError::Validation { message } => Some(message.clone()),
667 AuthError::UserNotFound => Some("User not found".to_string()),
668 _ => None,
669 }
670 }
671
672 pub async fn complete_mfa(&self, challenge: MfaChallenge, mfa_code: &str) -> Result<AuthToken> {
679 debug!("Completing MFA for challenge '{}'", challenge.id);
680
681 let stored_challenge = self
683 .mfa_manager
684 .get_challenge(&challenge.id)
685 .await?
686 .ok_or(MfaError::ChallengeExpired)?;
687
688 if stored_challenge.is_expired() {
689 self.mfa_manager.remove_challenge(&challenge.id).await?;
690 return Err(MfaError::ChallengeExpired.into());
691 }
692
693 let is_valid = match &stored_challenge.mfa_type {
695 crate::methods::MfaType::Totp => {
696 self.mfa_manager
697 .totp
698 .verify_code(&stored_challenge.user_id, mfa_code)
699 .await?
700 }
701 crate::methods::MfaType::Sms { .. } => {
702 self.mfa_manager
703 .sms
704 .verify_code(&challenge.id, mfa_code)
705 .await?
706 }
707 crate::methods::MfaType::Email { .. } => {
708 self.mfa_manager
709 .email
710 .verify_code(&challenge.id, mfa_code)
711 .await?
712 }
713 crate::methods::MfaType::BackupCode => {
714 self.mfa_manager
715 .backup_codes
716 .verify_code(&stored_challenge.user_id, mfa_code)
717 .await?
718 }
719 _ => false,
720 };
721
722 if !is_valid {
723 return Err(MfaError::InvalidCode.into());
724 }
725
726 self.mfa_manager.remove_challenge(&challenge.id).await?;
728
729 let user_key = format!("user:{}", challenge.user_id);
731 let scopes = if let Ok(Some(data)) = self.storage.get_kv(&user_key).await {
732 serde_json::from_slice::<serde_json::Value>(&data)
733 .ok()
734 .and_then(|v| {
735 v.get("roles").and_then(|r| {
736 r.as_array().map(|arr| {
737 arr.iter()
738 .filter_map(|v| v.as_str().map(String::from))
739 .collect::<Vec<_>>()
740 })
741 })
742 })
743 .unwrap_or_else(|| vec!["user".to_string()])
744 } else {
745 vec!["user".to_string()]
746 };
747
748 let token =
750 self.token_manager
751 .create_auth_token(&challenge.user_id, scopes, "mfa", None)?;
752
753 self.storage.store_token(&token).await?;
755
756 info!(
757 "MFA completed successfully for user '{}'",
758 challenge.user_id
759 );
760
761 Ok(token)
762 }
763
764 pub async fn validate_token(&self, token: &AuthToken) -> Result<bool> {
771 if !self.initialized {
772 return Err(AuthError::internal("Framework not initialized"));
773 }
774
775 if !token.is_valid() {
777 return Ok(false);
778 }
779
780 self.token_manager.validate_auth_token(token)?;
782
783 if let Some(stored_token) = self.storage.get_token(&token.token_id).await? {
785 let mut updated_token = stored_token;
787 updated_token.mark_used();
788 self.storage.update_token(&updated_token).await?;
789
790 Ok(true)
791 } else {
792 Ok(false)
793 }
794 }
795
796 pub async fn get_user_info(&self, token: &AuthToken) -> Result<UserInfo> {
804 if !self.validate_token(token).await? {
805 return Err(AuthError::auth_method("token", "Invalid token".to_string()));
806 }
807
808 self.user_manager.get_user_info(&token.user_id).await
809 }
810
811 pub async fn check_permission(
818 &self,
819 token: &AuthToken,
820 action: &str,
821 resource: &str,
822 ) -> Result<bool> {
823 if !self.validate_token(token).await? {
824 return Ok(false);
825 }
826
827 let permission = Permission::new(action, resource);
828 let mut checker = self.permission_checker.write().await;
829 checker.check_token_permission(token, &permission)
830 }
831
832 pub fn token_manager(&self) -> &TokenManager {
839 &self.token_manager
840 }
841
842 pub fn mfa_manager(&self) -> &MfaManager {
849 &self.mfa_manager
850 }
851
852 pub fn session_manager(&self) -> &SessionManager {
859 &self.session_manager
860 }
861
862 pub fn user_manager(&self) -> &UserManager {
869 &self.user_manager
870 }
871
872 pub async fn initiate_sms_challenge(&self, user_id: &str) -> Result<String> {
879 self.mfa_manager.sms.initiate_challenge(user_id).await
880 }
881
882 pub async fn send_sms_code(&self, challenge_id: &str, phone_number: &str) -> Result<()> {
889 self.mfa_manager
890 .sms
891 .send_code(challenge_id, phone_number)
892 .await
893 }
894
895 pub async fn generate_sms_code(&self, challenge_id: &str) -> Result<String> {
902 self.mfa_manager.sms.generate_code(challenge_id).await
903 }
904
905 pub async fn verify_sms_code(&self, challenge_id: &str, code: &str) -> Result<bool> {
912 self.mfa_manager.sms.verify_code(challenge_id, code).await
913 }
914
915 pub async fn cleanup_expired_data(&self) -> Result<()> {
922 debug!("Cleaning up expired data");
923
924 self.storage.cleanup_expired().await?;
926
927 self.mfa_manager.cleanup_expired_challenges().await?;
929
930 self.session_manager.cleanup_expired_sessions().await?;
932
933 if let Some(ref rate_limiter) = self.rate_limiter {
935 let _ = rate_limiter.cleanup().ok();
936 }
937
938 Ok(())
939 }
940
941 pub async fn get_stats(&self) -> Result<AuthStats> {
949 let mut stats = AuthStats::default();
950
951 for method in self.methods.keys() {
952 stats.registered_methods.push(method.clone());
953 }
954
955 stats.active_mfa_challenges = self.mfa_manager.get_active_challenge_count().await as u64;
956
957 stats.tokens_issued = self.storage.count_active_sessions().await.unwrap_or(0) as u64;
959
960 Ok(stats)
961 }
962
963 async fn log_audit_event(
965 &self,
966 event_type: &str,
967 user_id: &str,
968 method: &str,
969 metadata: &CredentialMetadata,
970 ) {
971 if self.config.audit.enabled {
972 let should_log = match event_type {
973 "auth_success" => self.config.audit.log_success,
974 "auth_failure" => self.config.audit.log_failures,
975 "mfa_required" => self.config.audit.log_success,
976 _ => true,
977 };
978
979 if should_log {
980 info!(
981 target: "auth_audit",
982 event_type = event_type,
983 user_id = user_id,
984 method = method,
985 client_ip = metadata.client_ip.as_deref().unwrap_or("unknown"),
986 user_agent = metadata.user_agent.as_deref().unwrap_or("unknown"),
987 timestamp = chrono::Utc::now().to_rfc3339(),
988 "Authentication event"
989 );
990 }
991 }
992 }
993}
994
995#[derive(Debug, Clone, Default)]
997pub struct AuthStats {
998 pub registered_methods: Vec<String>,
1000
1001 pub active_mfa_challenges: u64,
1003
1004 pub tokens_issued: u64,
1006
1007 pub auth_attempts: u64,
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::authentication::credentials::Credential;
1015 use crate::config::{AuthConfig, SecurityConfig};
1016 use std::time::Duration;
1017
1018 async fn initialized_framework(config: AuthConfig) -> AuthFramework {
1019 let mut framework = AuthFramework::new(config).expect("test config should be valid");
1020 framework
1021 .initialize()
1022 .await
1023 .expect("framework initialization should succeed");
1024 framework
1025 }
1026
1027 fn test_config() -> AuthConfig {
1028 AuthConfig::new().secret("test_secret_key_32_bytes_long!!!!")
1029 }
1030
1031 #[tokio::test]
1032 async fn test_modular_framework_initialization() {
1033 let config = AuthConfig::new().security(SecurityConfig {
1034 min_password_length: 8,
1035 require_password_complexity: false,
1036 password_hash_algorithm: crate::config::PasswordHashAlgorithm::Bcrypt,
1037 jwt_algorithm: crate::config::JwtAlgorithm::HS256,
1038 secret_key: Some("test_secret_key_32_bytes_long!!!!".to_string()),
1039 secure_cookies: false,
1040 cookie_same_site: crate::config::CookieSameSite::Lax,
1041 csrf_protection: false,
1042 session_timeout: Duration::from_secs(3600),
1043 previous_secret_key: None,
1044 });
1045 let mut framework = AuthFramework::new(config).expect("test config should be valid");
1046
1047 assert!(framework.initialize().await.is_ok());
1048 assert!(framework.initialized);
1049 }
1050
1051 #[tokio::test]
1052 async fn test_mfa_manager_access() {
1053 let config = AuthConfig::new().security(SecurityConfig {
1054 min_password_length: 8,
1055 require_password_complexity: false,
1056 password_hash_algorithm: crate::config::PasswordHashAlgorithm::Bcrypt,
1057 jwt_algorithm: crate::config::JwtAlgorithm::HS256,
1058 secret_key: Some("test_secret_key_32_bytes_long!!!!".to_string()),
1059 secure_cookies: false,
1060 cookie_same_site: crate::config::CookieSameSite::Lax,
1061 csrf_protection: false,
1062 session_timeout: Duration::from_secs(3600),
1063 previous_secret_key: None,
1064 });
1065 let framework = AuthFramework::new(config).expect("test config should be valid");
1066
1067 let _mfa_manager = framework.mfa_manager();
1069 let _session_manager = framework.session_manager();
1070 let _user_manager = framework.user_manager();
1071 }
1072
1073 #[tokio::test]
1074 async fn test_authenticate_with_metadata_enforces_minimum_duration() {
1075 let framework = initialized_framework(test_config()).await;
1076 let start = std::time::Instant::now();
1077
1078 let result = framework
1079 .authenticate_with_metadata(
1080 "oauth2",
1081 Credential::oauth_refresh(""),
1082 CredentialMetadata::new(),
1083 )
1084 .await
1085 .expect("empty refresh token should return failure, not error");
1086
1087 assert!(
1088 start.elapsed() >= std::time::Duration::from_millis(90),
1089 "timing floor should add a noticeable delay"
1090 );
1091 assert!(matches!(result, AuthResult::Failure(_)));
1092 }
1093
1094 #[tokio::test]
1095 async fn test_authenticate_oauth2_refresh_empty_returns_failure() {
1096 let framework = initialized_framework(test_config()).await;
1097
1098 let result = framework
1099 .authenticate("oauth2", Credential::oauth_refresh(""))
1100 .await
1101 .expect("empty refresh token should return failure, not error");
1102
1103 match result {
1104 AuthResult::Failure(reason) => {
1105 assert!(reason.contains("refresh token cannot be empty"));
1106 }
1107 _ => panic!("expected failure result for empty OAuth refresh token"),
1108 }
1109 }
1110
1111 #[tokio::test]
1112 async fn test_authenticate_oauth2_openid_connect_routes_to_jwt_validation() {
1113 let framework = initialized_framework(test_config()).await;
1114 let jwt = framework
1115 .token_manager()
1116 .create_jwt_token("oidc-user", vec!["openid".to_string()], None)
1117 .expect("jwt creation should succeed");
1118
1119 let result = framework
1120 .authenticate("oauth2", Credential::openid_connect(jwt))
1121 .await
1122 .expect("valid OIDC credential should authenticate");
1123
1124 match result {
1125 AuthResult::Success(token) => {
1126 assert_eq!(token.user_id, "oidc-user");
1127 assert_eq!(token.auth_method, "oauth2");
1128 }
1129 _ => panic!("expected success result for OpenID Connect credential"),
1130 }
1131 }
1132
1133 #[tokio::test]
1134 async fn test_authenticate_oauth2_unsupported_credential_returns_failure() {
1135 let framework = initialized_framework(test_config()).await;
1136
1137 let result = framework
1138 .authenticate("oauth2", Credential::password("alice", "secret"))
1139 .await
1140 .expect("unsupported credential should return failure, not error");
1141
1142 match result {
1143 AuthResult::Failure(reason) => {
1144 assert!(reason.contains("OAuth2 authentication expects"));
1145 }
1146 _ => panic!("expected failure result for unsupported OAuth2 credential"),
1147 }
1148 }
1149
1150 #[tokio::test]
1151 async fn test_rate_limiting_without_client_ip_uses_shared_unknown_bucket() {
1152 let config = AuthConfig::new()
1153 .secret("test_secret_key_32_bytes_long!!!!")
1154 .rate_limiting(crate::config::RateLimitConfig {
1155 enabled: true,
1156 max_requests: 1,
1157 window: Duration::from_secs(60),
1158 burst: 0,
1159 });
1160 let framework = initialized_framework(config).await;
1161
1162 let first = framework
1163 .authenticate_with_metadata(
1164 "oauth2",
1165 Credential::oauth_code("first-code"),
1166 CredentialMetadata::new(),
1167 )
1168 .await
1169 .expect("first request should pass rate limiting");
1170 assert!(matches!(first, AuthResult::Failure(_)));
1171
1172 let second = framework
1173 .authenticate_with_metadata(
1174 "oauth2",
1175 Credential::oauth_code("second-code"),
1176 CredentialMetadata::new(),
1177 )
1178 .await;
1179 assert!(second.is_err(), "second request should be rate limited");
1180 assert!(second
1181 .unwrap_err()
1182 .to_string()
1183 .contains("Too many authentication attempts"));
1184 }
1185
1186 #[tokio::test]
1187 async fn test_complete_mfa_missing_challenge_returns_expired_error() {
1188 let framework = initialized_framework(test_config()).await;
1189 let challenge = MfaChallenge::new(
1190 crate::methods::MfaType::BackupCode,
1191 "user-123",
1192 Duration::from_secs(60),
1193 );
1194
1195 let result = framework.complete_mfa(challenge, "123456").await;
1196 assert!(result.is_err());
1197 assert!(result.unwrap_err().to_string().contains("expired"));
1198 }
1199
1200 #[tokio::test]
1201 async fn test_complete_mfa_expired_challenge_is_removed() {
1202 let framework = initialized_framework(test_config()).await;
1203 let mut challenge = MfaChallenge::new(
1204 crate::methods::MfaType::BackupCode,
1205 "user-123",
1206 Duration::from_secs(60),
1207 );
1208 challenge.expires_at = chrono::Utc::now() - chrono::Duration::seconds(1);
1209 framework
1210 .mfa_manager
1211 .store_challenge(challenge.clone())
1212 .await
1213 .expect("storing challenge should succeed");
1214
1215 let result = framework.complete_mfa(challenge.clone(), "123456").await;
1216 assert!(result.is_err());
1217 assert!(result.unwrap_err().to_string().contains("expired"));
1218 assert!(framework
1219 .mfa_manager
1220 .get_challenge(&challenge.id)
1221 .await
1222 .expect("challenge lookup should succeed")
1223 .is_none());
1224 }
1225
1226 #[tokio::test]
1227 async fn test_validate_token_returns_false_for_expired_token() {
1228 let framework = initialized_framework(test_config()).await;
1229 let mut token = framework
1230 .token_manager()
1231 .create_auth_token("user-123", vec!["read".to_string()], "jwt", None)
1232 .expect("token creation should succeed");
1233 token.expires_at = chrono::Utc::now() - chrono::Duration::seconds(1);
1234
1235 let valid = framework
1236 .validate_token(&token)
1237 .await
1238 .expect("expired token should return false, not error");
1239 assert!(!valid);
1240 }
1241
1242 #[tokio::test]
1243 async fn test_validate_token_returns_false_when_not_in_storage() {
1244 let framework = initialized_framework(test_config()).await;
1245 let token = framework
1246 .token_manager()
1247 .create_auth_token("user-123", vec!["read".to_string()], "jwt", None)
1248 .expect("token creation should succeed");
1249
1250 let valid = framework
1251 .validate_token(&token)
1252 .await
1253 .expect("missing stored token should return false, not error");
1254 assert!(!valid);
1255 }
1256}