1pub mod backup_codes;
4pub mod email;
5pub mod sms_kit;
6pub mod totp;
7
8use crate::errors::Result;
9use crate::methods::MfaChallenge;
10use crate::storage::AuthStorage;
11use base64::Engine as _;
12use hmac::{Hmac, Mac};
13use sha2::Sha256;
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::Duration;
17use tokio::sync::RwLock;
18use tracing::debug;
19
20type EmergencyBypassHmac = Hmac<Sha256>;
21
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23struct EmergencyBypassClaims {
24 admin_user_id: String,
25 target_user_id: String,
26 iat: i64,
27 exp: i64,
28 jti: String,
29}
30
31pub use backup_codes::BackupCodesManager;
32pub use email::EmailManager;
33pub use totp::TotpManager;
34
35pub use sms_kit::{
37 RateLimitConfig as SmsKitRateLimitConfig, SmsKitConfig, SmsKitManager, SmsKitProvider,
38 SmsKitProviderConfig, WebhookConfig,
39};
40
41pub use sms_kit::SmsKitManager as SmsManager;
43
44pub struct MfaManager {
113 pub totp: TotpManager,
115
116 pub sms: SmsKitManager,
118
119 pub email: EmailManager,
121
122 pub backup_codes: BackupCodesManager,
124
125 challenges: Arc<RwLock<HashMap<String, MfaChallenge>>>,
127
128 storage: Arc<dyn AuthStorage>,
130}
131
132impl MfaManager {
133 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
135 Self {
136 totp: TotpManager::new(storage.clone()),
137 sms: SmsKitManager::new(storage.clone()),
138 email: EmailManager::new(storage.clone()),
139 backup_codes: BackupCodesManager::new(storage.clone()),
140 challenges: Arc::new(RwLock::new(HashMap::new())),
141 storage,
142 }
143 }
144
145 pub fn new_with_smskit_config(
147 storage: Arc<dyn AuthStorage>,
148 smskit_config: SmsKitConfig,
149 ) -> Result<Self> {
150 Ok(Self {
151 totp: TotpManager::new(storage.clone()),
152 sms: SmsKitManager::new_with_config(storage.clone(), smskit_config)?,
153 email: EmailManager::new(storage.clone()),
154 backup_codes: BackupCodesManager::new(storage.clone()),
155 challenges: Arc::new(RwLock::new(HashMap::new())),
156 storage,
157 })
158 }
159
160 pub async fn store_challenge(&self, challenge: MfaChallenge) -> Result<()> {
162 debug!("Storing MFA challenge '{}'", challenge.id);
163
164 let mut challenges = self.challenges.write().await;
165 challenges.insert(challenge.id.clone(), challenge);
166
167 Ok(())
168 }
169
170 pub async fn guard_and_store(&self, challenge: MfaChallenge) -> Result<()> {
175 const MAX_TOTAL_CHALLENGES: usize = 10_000;
176 if self.get_active_challenge_count().await >= MAX_TOTAL_CHALLENGES {
177 tracing::warn!("Maximum MFA challenges ({}) exceeded", MAX_TOTAL_CHALLENGES);
178 return Err(crate::errors::AuthError::rate_limit(
179 "Too many pending MFA challenges. Please try again later.",
180 ));
181 }
182 self.store_challenge(challenge).await
183 }
184
185 pub async fn get_challenge(&self, challenge_id: &str) -> Result<Option<MfaChallenge>> {
187 let challenges = self.challenges.read().await;
188 Ok(challenges.get(challenge_id).cloned())
189 }
190
191 pub async fn remove_challenge(&self, challenge_id: &str) -> Result<()> {
193 debug!("Removing MFA challenge '{}'", challenge_id);
194
195 let mut challenges = self.challenges.write().await;
196 challenges.remove(challenge_id);
197
198 Ok(())
199 }
200
201 pub async fn cleanup_expired_challenges(&self) -> Result<()> {
203 debug!("Cleaning up expired MFA challenges");
204
205 let mut challenges = self.challenges.write().await;
206 let now = chrono::Utc::now();
207 challenges.retain(|_, challenge| challenge.expires_at > now);
208
209 Ok(())
210 }
211
212 pub async fn get_active_challenge_count(&self) -> usize {
214 self.challenges.read().await.len()
215 }
216
217 pub async fn verify_challenge_code(
219 &self,
220 challenge: &crate::methods::MfaChallenge,
221 code: &str,
222 ) -> Result<bool> {
223 use crate::security::secure_utils::constant_time_compare;
224
225 if challenge.is_expired() {
226 return Ok(false);
227 }
228
229 match &challenge.mfa_type {
230 crate::methods::MfaType::Totp => {
231 self.totp.verify_login_code(&challenge.user_id, code).await
232 }
233 crate::methods::MfaType::Sms { .. } => {
234 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
235 return Ok(false);
236 }
237 let sms_key = format!("smskit_challenge:{}:code", challenge.id);
238 match self.storage.get_kv(&sms_key).await? {
239 Some(stored) => {
240 let stored_code = std::str::from_utf8(&stored).unwrap_or("");
241 Ok(constant_time_compare(
242 stored_code.as_bytes(),
243 code.as_bytes(),
244 ))
245 }
246 None => Ok(false),
247 }
248 }
249 crate::methods::MfaType::Email { .. } => {
250 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
251 return Ok(false);
252 }
253 let email_key = format!("email_challenge:{}:code", challenge.id);
254 match self.storage.get_kv(&email_key).await? {
255 Some(stored) => {
256 let stored_code = std::str::from_utf8(&stored).unwrap_or("");
257 Ok(constant_time_compare(
258 stored_code.as_bytes(),
259 code.as_bytes(),
260 ))
261 }
262 None => Ok(false),
263 }
264 }
265 crate::methods::MfaType::BackupCode => {
266 self.backup_codes
267 .verify_login_code(&challenge.user_id, code)
268 .await
269 }
270 crate::methods::MfaType::MultiMethod => {
271 if self
272 .totp
273 .verify_login_code(&challenge.user_id, code)
274 .await?
275 {
276 return Ok(true);
277 }
278 self.backup_codes
279 .verify_login_code(&challenge.user_id, code)
280 .await
281 }
282 _ => Ok(false),
283 }
284 }
285
286 pub async fn initiate_step_up_authentication(
288 &self,
289 user_id: &str,
290 required_methods: &[MfaMethod],
291 risk_level: RiskLevel,
292 ) -> Result<CrossMethodChallenge> {
293 tracing::info!(
294 "Initiating step-up authentication for user: {} with risk level: {:?}",
295 user_id,
296 risk_level
297 );
298
299 let adaptive_methods = self
301 .adapt_required_methods(required_methods, risk_level.clone())
302 .await?;
303
304 let challenge_id = uuid::Uuid::new_v4().to_string();
306
307 let mut method_challenges = HashMap::new();
309 let mut completion_status = HashMap::new();
310
311 for method in &adaptive_methods {
312 let method_challenge = match method {
313 MfaMethod::Totp => {
314 completion_status.insert(method.clone(), false);
315 self.create_totp_challenge(user_id, &challenge_id).await?
316 }
317 MfaMethod::Sms => {
318 completion_status.insert(method.clone(), false);
319 self.create_sms_challenge(user_id, &challenge_id).await?
320 }
321 MfaMethod::Email => {
322 completion_status.insert(method.clone(), false);
323 self.create_email_challenge(user_id, &challenge_id).await?
324 }
325 MfaMethod::BackupCode => {
326 completion_status.insert(method.clone(), false);
327 MethodChallenge::BackupCode {
328 challenge_id: format!("{}-backup", challenge_id),
329 instructions: "Enter one of your backup codes".to_string(),
330 }
331 }
332 };
333
334 method_challenges.insert(method.clone(), method_challenge);
335 }
336
337 let cross_method_challenge = CrossMethodChallenge {
338 id: challenge_id,
339 user_id: user_id.to_string(),
340 required_methods: adaptive_methods.clone(),
341 method_challenges,
342 completion_status,
343 risk_level,
344 expires_at: chrono::Utc::now() + chrono::Duration::minutes(10),
345 created_at: chrono::Utc::now(),
346 };
347
348 {
350 let mut challenges = self.challenges.write().await;
351 challenges.insert(
352 cross_method_challenge.id.clone(),
353 MfaChallenge {
354 id: cross_method_challenge.id.clone(),
355 mfa_type: crate::methods::MfaType::MultiMethod,
356 user_id: user_id.to_string(),
357 expires_at: cross_method_challenge.expires_at,
358 created_at: chrono::Utc::now(),
359 attempts: 0,
360 max_attempts: 3,
361 code_hash: None,
362 message: Some("Complete all required authentication methods".to_string()),
363 data: {
364 let mut data = HashMap::new();
365 data.insert(
366 "cross_method_data".to_string(),
367 serde_json::to_value(&cross_method_challenge)?,
368 );
369 data
370 },
371 },
372 );
373 }
374
375 tracing::info!(
376 "Step-up authentication initiated with {} methods",
377 adaptive_methods.len()
378 );
379 Ok(cross_method_challenge)
380 }
381
382 pub async fn complete_cross_method_step(
384 &self,
385 challenge_id: &str,
386 method: MfaMethod,
387 response: &str,
388 ) -> Result<CrossMethodCompletionResult> {
389 tracing::debug!(
390 "Completing cross-method step: {:?} for challenge: {}",
391 method,
392 challenge_id
393 );
394
395 let mut cross_challenge = self.get_cross_method_challenge(challenge_id).await?;
397
398 if cross_challenge.completion_status.get(&method) == Some(&true) {
399 return Ok(CrossMethodCompletionResult {
400 method,
401 success: true,
402 remaining_methods: self.get_remaining_methods(&cross_challenge),
403 all_completed: false,
404 error: Some("Method already completed".to_string()),
405 });
406 }
407
408 let verification_result = match method {
410 MfaMethod::Totp => {
411 self.totp
412 .verify_code(&cross_challenge.user_id, response)
413 .await
414 }
415 MfaMethod::Sms => {
416 self.sms
417 .verify_code(&cross_challenge.user_id, response)
418 .await
419 }
420 MfaMethod::Email => {
421 self.email
422 .verify_code(&cross_challenge.user_id, response)
423 .await
424 }
425 MfaMethod::BackupCode => {
426 self.backup_codes
427 .verify_code(&cross_challenge.user_id, response)
428 .await
429 }
430 };
431
432 let success = verification_result.is_ok();
433
434 if success {
435 cross_challenge
437 .completion_status
438 .insert(method.clone(), true);
439
440 self.update_cross_method_challenge(&cross_challenge).await?;
442
443 tracing::info!("Cross-method step completed successfully: {:?}", method);
444 } else {
445 tracing::warn!(
446 "Cross-method step failed: {:?} - {:?}",
447 method,
448 verification_result
449 );
450 }
451
452 let remaining_methods = self.get_remaining_methods(&cross_challenge);
453 let all_completed = remaining_methods.is_empty();
454
455 if all_completed {
456 tracing::info!(
457 "All cross-method authentication steps completed for challenge: {}",
458 challenge_id
459 );
460 self.remove_challenge(challenge_id).await?;
462 }
463
464 Ok(CrossMethodCompletionResult {
465 method,
466 success,
467 remaining_methods,
468 all_completed,
469 error: if success {
470 None
471 } else {
472 Some(format!(
473 "Verification failed: {:?}",
474 verification_result.unwrap_err()
475 ))
476 },
477 })
478 }
479
480 pub async fn get_available_methods(&self, user_id: &str) -> Result<Vec<MfaMethod>> {
482 tracing::debug!("Getting available MFA methods for user: {}", user_id);
483
484 let mut available_methods = Vec::new();
485
486 if self.totp.has_totp_secret(user_id).await.unwrap_or(false) {
488 available_methods.push(MfaMethod::Totp);
489 }
490
491 if self.sms.has_phone_number(user_id).await.unwrap_or(false) {
493 available_methods.push(MfaMethod::Sms);
494 }
495
496 if self.email.has_email(user_id).await.unwrap_or(false) {
498 available_methods.push(MfaMethod::Email);
499 }
500
501 if self
503 .backup_codes
504 .has_backup_codes(user_id)
505 .await
506 .unwrap_or(false)
507 {
508 available_methods.push(MfaMethod::BackupCode);
509 }
510
511 tracing::debug!(
512 "Available methods for user {}: {:?}",
513 user_id,
514 available_methods
515 );
516 Ok(available_methods)
517 }
518
519 pub async fn perform_method_fallback(
521 &self,
522 user_id: &str,
523 failed_method: MfaMethod,
524 fallback_order: &[MfaMethod],
525 ) -> Result<MethodFallbackResult> {
526 tracing::info!(
527 "Performing method fallback for user: {} after failed method: {:?}",
528 user_id,
529 failed_method
530 );
531
532 let available_methods = self.get_available_methods(user_id).await?;
533
534 for fallback_method in fallback_order {
536 if available_methods.contains(fallback_method) && fallback_method != &failed_method {
537 let fallback_challenge = match fallback_method {
539 MfaMethod::Totp => self.create_totp_challenge(user_id, "fallback").await?,
540 MfaMethod::Sms => self.create_sms_challenge(user_id, "fallback").await?,
541 MfaMethod::Email => self.create_email_challenge(user_id, "fallback").await?,
542 MfaMethod::BackupCode => MethodChallenge::BackupCode {
543 challenge_id: "fallback-backup".to_string(),
544 instructions: "Enter one of your backup codes".to_string(),
545 },
546 };
547
548 tracing::info!(
549 "Fallback method activated: {:?} for user: {}",
550 fallback_method,
551 user_id
552 );
553
554 return Ok(MethodFallbackResult {
555 fallback_method: fallback_method.clone(),
556 challenge: fallback_challenge,
557 remaining_fallbacks: fallback_order
558 .iter()
559 .skip_while(|&m| m != fallback_method)
560 .skip(1)
561 .filter(|&m| available_methods.contains(m))
562 .cloned()
563 .collect(),
564 });
565 }
566 }
567
568 Err(crate::errors::AuthError::validation(
569 "No fallback methods available",
570 ))
571 }
572
573 async fn adapt_required_methods(
575 &self,
576 base_methods: &[MfaMethod],
577 risk_level: RiskLevel,
578 ) -> Result<Vec<MfaMethod>> {
579 let mut adapted_methods = base_methods.to_vec();
580
581 match risk_level {
582 RiskLevel::Low => {
583 adapted_methods.truncate(1);
585 }
586 RiskLevel::Medium => {
587 }
590 RiskLevel::High => {
591 if !adapted_methods.contains(&MfaMethod::Email) {
593 adapted_methods.push(MfaMethod::Email);
594 }
595 if !adapted_methods.contains(&MfaMethod::Sms) {
596 adapted_methods.push(MfaMethod::Sms);
597 }
598 }
599 RiskLevel::Critical => {
600 adapted_methods = vec![MfaMethod::Totp, MfaMethod::Sms, MfaMethod::Email];
602 }
603 }
604
605 Ok(adapted_methods)
606 }
607
608 async fn get_cross_method_challenge(&self, challenge_id: &str) -> Result<CrossMethodChallenge> {
610 let challenges = self.challenges.read().await;
611 let challenge = challenges
612 .get(challenge_id)
613 .ok_or_else(|| crate::errors::AuthError::validation("Challenge not found"))?;
614
615 let cross_challenge: CrossMethodChallenge =
616 if let Some(cross_method_value) = challenge.data.get("cross_method_data") {
617 serde_json::from_value(cross_method_value.clone())?
618 } else {
619 return Err(crate::errors::AuthError::validation(
620 "Invalid cross-method challenge data",
621 ));
622 };
623 Ok(cross_challenge)
624 }
625
626 async fn update_cross_method_challenge(
627 &self,
628 cross_challenge: &CrossMethodChallenge,
629 ) -> Result<()> {
630 let mut challenges = self.challenges.write().await;
631 if let Some(challenge) = challenges.get_mut(&cross_challenge.id) {
632 challenge.data.insert(
633 "cross_method_data".to_string(),
634 serde_json::to_value(cross_challenge)?,
635 );
636 }
637 Ok(())
638 }
639
640 fn get_remaining_methods(&self, cross_challenge: &CrossMethodChallenge) -> Vec<MfaMethod> {
641 cross_challenge
642 .completion_status
643 .iter()
644 .filter_map(|(method, &completed)| {
645 if !completed {
646 Some(method.clone())
647 } else {
648 None
649 }
650 })
651 .collect()
652 }
653
654 async fn create_totp_challenge(
656 &self,
657 _user_id: &str,
658 challenge_prefix: &str,
659 ) -> Result<MethodChallenge> {
660 Ok(MethodChallenge::Totp {
661 challenge_id: format!("{}-totp", challenge_prefix),
662 instructions: "Enter the 6-digit code from your authenticator app".to_string(),
663 })
664 }
665
666 async fn create_sms_challenge(
667 &self,
668 user_id: &str,
669 challenge_prefix: &str,
670 ) -> Result<MethodChallenge> {
671 let _code = self.sms.send_verification_code(user_id).await?;
672 Ok(MethodChallenge::Sms {
673 challenge_id: format!("{}-sms", challenge_prefix),
674 instructions: "Enter the verification code sent to your phone".to_string(),
675 phone_hint: self
676 .get_phone_hint(user_id)
677 .await
678 .unwrap_or_else(|_| "***-***-****".to_string()),
679 })
680 }
681
682 async fn create_email_challenge(
683 &self,
684 user_id: &str,
685 challenge_prefix: &str,
686 ) -> Result<MethodChallenge> {
687 let _code = self.email.send_email_code(user_id).await?;
688 Ok(MethodChallenge::Email {
689 challenge_id: format!("{}-email", challenge_prefix),
690 instructions: "Enter the verification code sent to your email".to_string(),
691 email_hint: self
692 .get_email_hint(user_id)
693 .await
694 .unwrap_or_else(|_| "****@****.com".to_string()),
695 })
696 }
697
698 async fn get_phone_hint(&self, user_id: &str) -> Result<String> {
699 if let Ok(Some(data)) = self
701 .storage
702 .get_kv(&format!("user_phone:{}", user_id))
703 .await
704 {
705 if let Ok(phone) = String::from_utf8(data) {
706 if phone.len() >= 4 {
707 return Ok(format!("***-***-{}", &phone[phone.len() - 4..]));
708 }
709 }
710 }
711 Ok("Phone on file".to_string())
713 }
714
715 async fn get_email_hint(&self, user_id: &str) -> Result<String> {
716 if let Ok(Some(data)) = self
718 .storage
719 .get_kv(&format!("user_email:{}", user_id))
720 .await
721 {
722 if let Ok(email) = String::from_utf8(data) {
723 if let Some(at_pos) = email.find('@') {
724 let prefix_len = at_pos.min(2);
725 return Ok(format!(
726 "{}****@****{}",
727 &email[..prefix_len],
728 &email[at_pos..]
729 ));
730 }
731 }
732 }
733 Ok(format!("{}****@****.com", &user_id[..user_id.len().min(2)]))
735 }
736
737 fn emergency_bypass_secret() -> Result<Vec<u8>> {
738 let secret = std::env::var("AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET").map_err(|_| {
739 crate::errors::AuthError::config(
740 "Emergency MFA bypass is disabled until AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET is configured",
741 )
742 })?;
743
744 if secret.len() < 32 {
745 return Err(crate::errors::AuthError::config(
746 "AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET must be at least 32 bytes long",
747 ));
748 }
749
750 Ok(secret.into_bytes())
751 }
752
753 fn sign_emergency_bypass_payload(secret: &[u8], payload: &str) -> Result<String> {
754 let mut mac = EmergencyBypassHmac::new_from_slice(secret).map_err(|_| {
755 crate::errors::AuthError::config("Invalid emergency bypass signing key")
756 })?;
757 mac.update(payload.as_bytes());
758 Ok(hex::encode(mac.finalize().into_bytes()))
759 }
760
761 fn verify_emergency_bypass_token(secret: &[u8], token: &str) -> Result<EmergencyBypassClaims> {
762 let (payload_b64, signature_hex) = token.split_once('.').ok_or_else(|| {
763 crate::errors::AuthError::validation("Invalid emergency bypass token format")
764 })?;
765
766 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
767 .decode(payload_b64)
768 .map_err(|_| {
769 crate::errors::AuthError::validation("Invalid emergency bypass token encoding")
770 })?;
771 let payload = String::from_utf8(payload_bytes).map_err(|_| {
772 crate::errors::AuthError::validation("Invalid emergency bypass token payload")
773 })?;
774
775 let expected_signature = Self::sign_emergency_bypass_payload(secret, &payload)?;
776 if !crate::security::secure_utils::constant_time_compare(
777 expected_signature.as_bytes(),
778 signature_hex.as_bytes(),
779 ) {
780 return Err(crate::errors::AuthError::validation(
781 "Emergency bypass token signature verification failed",
782 ));
783 }
784
785 let claims: EmergencyBypassClaims = serde_json::from_str(&payload).map_err(|_| {
786 crate::errors::AuthError::validation("Invalid emergency bypass token claims")
787 })?;
788
789 let now = chrono::Utc::now().timestamp();
790 if now >= claims.exp {
791 return Err(crate::errors::AuthError::validation(
792 "Emergency bypass token has expired",
793 ));
794 }
795
796 Ok(claims)
797 }
798
799 async fn user_has_admin_role(&self, user_id: &str) -> Result<bool> {
800 let user_key = format!("user:{}", user_id);
801 let Some(user_data) = self.storage.get_kv(&user_key).await? else {
802 return Ok(false);
803 };
804
805 let profile: serde_json::Value = serde_json::from_slice(&user_data).map_err(|e| {
806 crate::errors::AuthError::internal(format!(
807 "Failed to parse stored user profile for emergency bypass validation: {}",
808 e
809 ))
810 })?;
811
812 Ok(profile
813 .get("roles")
814 .and_then(|v| v.as_array())
815 .map(|roles| roles.iter().any(|role| role.as_str() == Some("admin")))
816 .unwrap_or(false))
817 }
818
819 pub async fn generate_emergency_bypass_token(
821 &self,
822 admin_user_id: &str,
823 target_user_id: &str,
824 lifetime: Duration,
825 ) -> Result<String> {
826 if !self.user_has_admin_role(admin_user_id).await? {
827 return Err(crate::errors::AuthError::Permission(
828 crate::errors::PermissionError::Denied {
829 action: "generate emergency MFA bypass token".to_string(),
830 resource: admin_user_id.to_string(),
831 message:
832 "Admin privileges are required to generate an emergency MFA bypass token"
833 .to_string(),
834 },
835 ));
836 }
837
838 let secret = Self::emergency_bypass_secret()?;
839 let now = chrono::Utc::now().timestamp();
840 let exp = now
841 + i64::try_from(lifetime.as_secs()).map_err(|_| {
842 crate::errors::AuthError::validation(
843 "Emergency bypass token lifetime exceeds supported range",
844 )
845 })?;
846
847 let claims = EmergencyBypassClaims {
848 admin_user_id: admin_user_id.to_string(),
849 target_user_id: target_user_id.to_string(),
850 iat: now,
851 exp,
852 jti: uuid::Uuid::new_v4().to_string(),
853 };
854 let payload = serde_json::to_string(&claims)
855 .map_err(|e| crate::errors::AuthError::internal(e.to_string()))?;
856 let payload_b64 =
857 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload.as_bytes());
858 let signature = Self::sign_emergency_bypass_payload(&secret, &payload)?;
859 Ok(format!("{}.{}", payload_b64, signature))
860 }
861
862 pub async fn emergency_mfa_bypass(&self, user_id: &str, admin_token: &str) -> Result<bool> {
865 tracing::warn!("Emergency MFA bypass requested for user: {}", user_id);
866
867 let secret = Self::emergency_bypass_secret()?;
868 let claims = match Self::verify_emergency_bypass_token(&secret, admin_token) {
869 Ok(claims) => claims,
870 Err(e) => {
871 tracing::error!(error = %e, "Invalid emergency MFA bypass token");
872 return Ok(false);
873 }
874 };
875
876 if claims.target_user_id != user_id {
877 tracing::error!(
878 target = %claims.target_user_id,
879 requested = %user_id,
880 "Emergency MFA bypass token target user mismatch"
881 );
882 return Ok(false);
883 }
884
885 if !self.user_has_admin_role(&claims.admin_user_id).await? {
886 tracing::error!(
887 admin_user_id = %claims.admin_user_id,
888 "Emergency MFA bypass denied because issuing admin no longer has admin role"
889 );
890 return Ok(false);
891 }
892
893 tracing::info!(
894 admin_user_id = %claims.admin_user_id,
895 target_user_id = %claims.target_user_id,
896 "Emergency MFA bypass granted"
897 );
898
899 let bypass_key = format!("mfa_bypass:{}:{}", user_id, chrono::Utc::now().timestamp());
900 let bypass_data = serde_json::json!({
901 "admin_user_id": claims.admin_user_id,
902 "target_user_id": claims.target_user_id,
903 "issued_at": claims.iat,
904 "expires_at": claims.exp,
905 "jti": claims.jti,
906 "bypassed_at": chrono::Utc::now().to_rfc3339(),
907 })
908 .to_string();
909 self.storage
910 .store_kv(
911 &bypass_key,
912 bypass_data.as_bytes(),
913 Some(Duration::from_secs(86400)),
914 )
915 .await?;
916
917 Ok(true)
918 }
919}
920
921#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
923pub enum MfaMethod {
924 Totp,
925 Sms,
926 Email,
927 BackupCode,
928}
929
930#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
932pub enum RiskLevel {
933 Low,
934 Medium,
935 High,
936 Critical,
937}
938
939#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
941pub struct CrossMethodChallenge {
942 pub id: String,
943 pub user_id: String,
944 pub required_methods: Vec<MfaMethod>,
945 pub method_challenges: HashMap<MfaMethod, MethodChallenge>,
946 pub completion_status: HashMap<MfaMethod, bool>,
947 pub risk_level: RiskLevel,
948 pub expires_at: chrono::DateTime<chrono::Utc>,
949 pub created_at: chrono::DateTime<chrono::Utc>,
950}
951
952#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
954pub enum MethodChallenge {
955 Totp {
956 challenge_id: String,
957 instructions: String,
958 },
959 Sms {
960 challenge_id: String,
961 instructions: String,
962 phone_hint: String,
963 },
964 Email {
965 challenge_id: String,
966 instructions: String,
967 email_hint: String,
968 },
969 BackupCode {
970 challenge_id: String,
971 instructions: String,
972 },
973}
974
975#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
977pub struct CrossMethodCompletionResult {
978 pub method: MfaMethod,
979 pub success: bool,
980 pub remaining_methods: Vec<MfaMethod>,
981 pub all_completed: bool,
982 pub error: Option<String>,
983}
984
985#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
987pub struct MethodFallbackResult {
988 pub fallback_method: MfaMethod,
989 pub challenge: MethodChallenge,
990 pub remaining_fallbacks: Vec<MfaMethod>,
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use crate::storage::MemoryStorage;
997
998 fn make_mfa() -> MfaManager {
999 MfaManager::new(Arc::new(MemoryStorage::new()))
1000 }
1001
1002 fn make_challenge(user_id: &str) -> MfaChallenge {
1003 use crate::methods::{MfaChallenge, MfaType};
1004 MfaChallenge {
1005 id: format!("chal_{}", uuid::Uuid::new_v4()),
1006 mfa_type: MfaType::Totp,
1007 user_id: user_id.to_string(),
1008 created_at: chrono::Utc::now(),
1009 expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
1010 attempts: 0,
1011 max_attempts: 3,
1012 code_hash: None,
1013 message: None,
1014 data: HashMap::new(),
1015 }
1016 }
1017
1018 #[tokio::test]
1021 async fn test_store_and_get_challenge() {
1022 let mfa = make_mfa();
1023 let chal = make_challenge("u1");
1024 let id = chal.id.clone();
1025 mfa.store_challenge(chal).await.unwrap();
1026 let retrieved = mfa.get_challenge(&id).await.unwrap();
1027 assert!(retrieved.is_some());
1028 assert_eq!(retrieved.unwrap().user_id, "u1");
1029 }
1030
1031 #[tokio::test]
1032 async fn test_remove_challenge() {
1033 let mfa = make_mfa();
1034 let chal = make_challenge("u2");
1035 let id = chal.id.clone();
1036 mfa.store_challenge(chal).await.unwrap();
1037 mfa.remove_challenge(&id).await.unwrap();
1038 assert!(mfa.get_challenge(&id).await.unwrap().is_none());
1039 }
1040
1041 #[tokio::test]
1042 async fn test_get_challenge_nonexistent() {
1043 let mfa = make_mfa();
1044 assert!(mfa.get_challenge("ghost").await.unwrap().is_none());
1045 }
1046
1047 #[tokio::test]
1048 async fn test_get_active_challenge_count() {
1049 let mfa = make_mfa();
1050 assert_eq!(mfa.get_active_challenge_count().await, 0);
1051 mfa.store_challenge(make_challenge("u3")).await.unwrap();
1052 assert_eq!(mfa.get_active_challenge_count().await, 1);
1053 }
1054
1055 #[tokio::test]
1056 async fn test_cleanup_expired_challenges() {
1057 let mfa = make_mfa();
1058 let mut chal = make_challenge("u4");
1059 chal.expires_at = chrono::Utc::now() - chrono::Duration::minutes(1);
1060 mfa.store_challenge(chal).await.unwrap();
1061 mfa.cleanup_expired_challenges().await.unwrap();
1062 assert_eq!(mfa.get_active_challenge_count().await, 0);
1063 }
1064
1065 #[tokio::test]
1066 async fn test_guard_and_store_succeeds() {
1067 let mfa = make_mfa();
1068 let chal = make_challenge("guard_user");
1069 mfa.guard_and_store(chal).await.unwrap();
1070 assert_eq!(mfa.get_active_challenge_count().await, 1);
1071 }
1072
1073 #[tokio::test]
1076 async fn test_get_available_methods_none() {
1077 let mfa = make_mfa();
1078 let methods = mfa.get_available_methods("nobody").await.unwrap();
1079 assert!(methods.len() <= 4);
1082 }
1083
1084 #[tokio::test]
1085 async fn test_get_available_methods_with_totp() {
1086 let mfa = make_mfa();
1087 let _secret = mfa.totp.generate_secret("totp_user").await.unwrap();
1088 let methods = mfa.get_available_methods("totp_user").await.unwrap();
1089 assert!(methods.contains(&MfaMethod::Totp));
1090 }
1091
1092 #[tokio::test]
1093 async fn test_perform_method_fallback_uses_api_enrolled_totp() {
1094 let storage: Arc<dyn AuthStorage> = Arc::new(MemoryStorage::new());
1095 let mfa = MfaManager::new(storage.clone());
1096 let api_secret = base32::encode(base32::Alphabet::Rfc4648 { padding: true }, &[42; 20]);
1097
1098 storage
1099 .store_kv("mfa_secret:api_totp_user", api_secret.as_bytes(), None)
1100 .await
1101 .unwrap();
1102
1103 let fallback = mfa
1104 .perform_method_fallback("api_totp_user", MfaMethod::Email, &[MfaMethod::Totp])
1105 .await
1106 .unwrap();
1107
1108 assert_eq!(fallback.fallback_method, MfaMethod::Totp);
1109 assert!(matches!(fallback.challenge, MethodChallenge::Totp { .. }));
1110 }
1111
1112 #[tokio::test]
1115 async fn test_initiate_step_up_authentication() {
1116 let mfa = make_mfa();
1117 let _secret = mfa.totp.generate_secret("step_user").await.unwrap();
1119
1120 let cross = mfa
1121 .initiate_step_up_authentication("step_user", &[MfaMethod::Totp], RiskLevel::Medium)
1122 .await
1123 .unwrap();
1124 assert_eq!(cross.user_id, "step_user");
1125 assert_eq!(cross.risk_level, RiskLevel::Medium);
1126 }
1127
1128 async fn setup_admin(storage: &Arc<dyn AuthStorage>, user_id: &str) {
1130 let profile = serde_json::json!({
1131 "user_id": user_id,
1132 "username": user_id,
1133 "roles": ["admin"]
1134 });
1135 let key = format!("user:{}", user_id);
1136 storage
1137 .store_kv(&key, profile.to_string().as_bytes(), None)
1138 .await
1139 .unwrap();
1140 }
1141
1142 #[tokio::test]
1145 async fn test_generate_and_use_emergency_bypass() {
1146 let _env = crate::testing::test_infrastructure::TestEnvironmentGuard::new()
1147 .with_custom_var(
1148 "AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET",
1149 "this-is-a-very-long-test-secret-that-is-at-least-32-bytes",
1150 );
1151 let storage: Arc<dyn AuthStorage> = Arc::new(MemoryStorage::new());
1152 let mfa = MfaManager::new(storage.clone());
1153 setup_admin(&storage, "admin1").await;
1154 let token = mfa
1155 .generate_emergency_bypass_token("admin1", "target1", Duration::from_secs(300))
1156 .await
1157 .unwrap();
1158 assert!(!token.is_empty());
1159 let result = mfa.emergency_mfa_bypass("target1", &token).await.unwrap();
1160 assert!(result);
1161 }
1162
1163 #[tokio::test]
1164 async fn test_emergency_bypass_wrong_user() {
1165 let _env = crate::testing::test_infrastructure::TestEnvironmentGuard::new()
1166 .with_custom_var(
1167 "AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET",
1168 "this-is-a-very-long-test-secret-that-is-at-least-32-bytes",
1169 );
1170 let storage: Arc<dyn AuthStorage> = Arc::new(MemoryStorage::new());
1171 let mfa = MfaManager::new(storage.clone());
1172 setup_admin(&storage, "admin1").await;
1173 let token = mfa
1174 .generate_emergency_bypass_token("admin1", "target1", Duration::from_secs(300))
1175 .await
1176 .unwrap();
1177 let result = mfa
1178 .emergency_mfa_bypass("wrong_user", &token)
1179 .await
1180 .unwrap();
1181 assert!(!result);
1182 }
1183
1184 #[tokio::test]
1185 async fn test_emergency_bypass_invalid_token() {
1186 let mfa = make_mfa();
1187 let result = mfa.emergency_mfa_bypass("user1", "bogus_token").await;
1188 assert!(!result.unwrap_or(false));
1190 }
1191
1192 #[tokio::test]
1193 async fn test_emergency_bypass_non_admin_rejected() {
1194 let _env = crate::testing::test_infrastructure::TestEnvironmentGuard::new()
1195 .with_custom_var(
1196 "AUTHFRAMEWORK_EMERGENCY_BYPASS_SECRET",
1197 "this-is-a-very-long-test-secret-that-is-at-least-32-bytes",
1198 );
1199 let storage: Arc<dyn AuthStorage> = Arc::new(MemoryStorage::new());
1200 let mfa = MfaManager::new(storage.clone());
1201 let result = mfa
1203 .generate_emergency_bypass_token("notadmin", "target1", Duration::from_secs(300))
1204 .await;
1205 assert!(result.is_err());
1206 }
1207
1208 #[tokio::test]
1211 async fn test_totp_generate_secret() {
1212 let mfa = make_mfa();
1213 let secret = mfa.totp.generate_secret("totp1").await.unwrap();
1214 assert!(!secret.is_empty());
1215 assert!(mfa.totp.has_totp_secret("totp1").await.unwrap());
1216 }
1217
1218 #[tokio::test]
1219 async fn test_totp_generate_code() {
1220 let mfa = make_mfa();
1221 let secret = mfa.totp.generate_secret("totp2").await.unwrap();
1222 let code = mfa.totp.generate_code(&secret).await.unwrap();
1223 assert_eq!(code.len(), 6);
1224 assert!(code.chars().all(|c| c.is_ascii_digit()));
1225 }
1226
1227 #[tokio::test]
1228 async fn test_totp_verify_code_success() {
1229 let mfa = make_mfa();
1230 let secret = mfa.totp.generate_secret("totp3").await.unwrap();
1231 let code = mfa.totp.generate_code(&secret).await.unwrap();
1232 assert!(mfa.totp.verify_code("totp3", &code).await.unwrap());
1233 }
1234
1235 #[tokio::test]
1236 async fn test_totp_verify_code_wrong() {
1237 let mfa = make_mfa();
1238 let _secret = mfa.totp.generate_secret("totp4").await.unwrap();
1239 assert!(
1240 !mfa.totp
1241 .verify_code("totp4", "000000")
1242 .await
1243 .unwrap_or(true)
1244 );
1245 }
1246
1247 #[tokio::test]
1248 async fn test_totp_has_no_secret() {
1249 let mfa = make_mfa();
1250 assert!(!mfa.totp.has_totp_secret("nobody").await.unwrap());
1251 }
1252
1253 #[tokio::test]
1254 async fn test_totp_generate_qr_code() {
1255 let mfa = make_mfa();
1256 let secret = mfa.totp.generate_secret("totp5").await.unwrap();
1257 let qr = mfa
1258 .totp
1259 .generate_qr_code("totp5", "AuthFramework", &secret)
1260 .await
1261 .unwrap();
1262 assert!(qr.contains("otpauth://"));
1263 }
1264
1265 #[tokio::test]
1268 async fn test_backup_codes_generate() {
1269 let mfa = make_mfa();
1270 let codes = mfa.backup_codes.generate_codes("bc1", 10).await.unwrap();
1271 assert_eq!(codes.len(), 10);
1272 }
1273
1274 #[tokio::test]
1275 async fn test_backup_codes_verify() {
1276 let mfa = make_mfa();
1277 let codes = mfa.backup_codes.generate_codes("bc2", 5).await.unwrap();
1278 let code = codes[0].clone();
1279 assert!(mfa.backup_codes.verify_code("bc2", &code).await.unwrap());
1280 assert!(!mfa.backup_codes.verify_code("bc2", &code).await.unwrap());
1282 }
1283
1284 #[tokio::test]
1285 async fn test_backup_codes_remaining_count() {
1286 let mfa = make_mfa();
1287 mfa.backup_codes.generate_codes("bc3", 5).await.unwrap();
1288 assert_eq!(
1289 mfa.backup_codes.get_remaining_count("bc3").await.unwrap(),
1290 5
1291 );
1292 }
1293
1294 #[tokio::test]
1295 async fn test_backup_codes_verify_wrong_code() {
1296 let mfa = make_mfa();
1297 mfa.backup_codes.generate_codes("bc4", 5).await.unwrap();
1298 assert!(
1299 !mfa.backup_codes
1300 .verify_code("bc4", "WRONGCODE")
1301 .await
1302 .unwrap()
1303 );
1304 }
1305
1306 #[tokio::test]
1307 async fn test_backup_codes_regenerate() {
1308 let mfa = make_mfa();
1309 let old = mfa.backup_codes.generate_codes("bc5", 5).await.unwrap();
1310 let new = mfa.backup_codes.regenerate_codes("bc5", 5).await.unwrap();
1311 assert_ne!(old, new);
1312 }
1313
1314 #[tokio::test]
1315 async fn test_backup_codes_has_codes() {
1316 let mfa = make_mfa();
1317 assert!(!mfa.backup_codes.has_backup_codes("nobody").await.unwrap());
1318 mfa.backup_codes.generate_codes("bc6", 3).await.unwrap();
1319 assert!(mfa.backup_codes.has_backup_codes("bc6").await.unwrap());
1320 }
1321
1322 #[tokio::test]
1325 async fn test_email_register_and_has_email() {
1326 let mfa = make_mfa();
1327 assert!(!mfa.email.has_email("em1").await.unwrap());
1328 mfa.email
1329 .register_email("em1", "test@example.com")
1330 .await
1331 .unwrap();
1332 assert!(mfa.email.has_email("em1").await.unwrap());
1333 }
1334
1335 #[tokio::test]
1336 async fn test_email_get_user_email() {
1337 let mfa = make_mfa();
1338 mfa.email
1339 .register_email("em2", "em2@example.com")
1340 .await
1341 .unwrap();
1342 let email = mfa.email.get_user_email("em2").await.unwrap();
1343 assert_eq!(email.as_deref(), Some("em2@example.com"));
1344 }
1345
1346 #[tokio::test]
1347 async fn test_email_get_user_email_none() {
1348 let mfa = make_mfa();
1349 let email = mfa.email.get_user_email("nobody").await.unwrap();
1350 assert!(email.is_none());
1351 }
1352
1353 #[tokio::test]
1354 async fn test_email_initiate_challenge() {
1355 let mfa = make_mfa();
1356 mfa.email
1357 .register_email("em3", "em3@example.com")
1358 .await
1359 .unwrap();
1360 let challenge_id = mfa.email.initiate_challenge("em3").await.unwrap();
1361 assert!(!challenge_id.is_empty());
1362 }
1363
1364 #[tokio::test]
1365 async fn test_email_generate_and_verify_code() {
1366 let mfa = make_mfa();
1367 mfa.email
1368 .register_email("em4", "em4@example.com")
1369 .await
1370 .unwrap();
1371 let cid = mfa.email.initiate_challenge("em4").await.unwrap();
1372 let code = mfa.email.generate_code(&cid).await.unwrap();
1373 assert!(mfa.email.verify_code(&cid, &code).await.unwrap());
1374 }
1375
1376 #[tokio::test]
1377 async fn test_email_verify_wrong_code() {
1378 let mfa = make_mfa();
1379 mfa.email
1380 .register_email("em5", "em5@example.com")
1381 .await
1382 .unwrap();
1383 let cid = mfa.email.initiate_challenge("em5").await.unwrap();
1384 let _code = mfa.email.generate_code(&cid).await.unwrap();
1385 assert!(!mfa.email.verify_code(&cid, "000000").await.unwrap());
1386 }
1387
1388 #[tokio::test]
1391 async fn test_sms_register_and_has_phone() {
1392 let mfa = make_mfa();
1393 assert!(!mfa.sms.has_phone_number("sms1").await.unwrap());
1394 mfa.sms
1395 .register_phone_number("sms1", "+1234567890")
1396 .await
1397 .unwrap();
1398 assert!(mfa.sms.has_phone_number("sms1").await.unwrap());
1399 }
1400
1401 #[tokio::test]
1402 async fn test_sms_get_user_phone() {
1403 let mfa = make_mfa();
1404 mfa.sms
1405 .register_phone_number("sms2", "+9876543210")
1406 .await
1407 .unwrap();
1408 let phone = mfa.sms.get_user_phone("sms2").await.unwrap();
1409 assert_eq!(phone.as_deref(), Some("+9876543210"));
1410 }
1411
1412 #[tokio::test]
1413 async fn test_sms_get_user_phone_none() {
1414 let mfa = make_mfa();
1415 let phone = mfa.sms.get_user_phone("nobody").await.unwrap();
1416 assert!(phone.is_none());
1417 }
1418
1419 #[tokio::test]
1420 async fn test_sms_initiate_challenge() {
1421 let mfa = make_mfa();
1422 mfa.sms
1423 .register_phone_number("sms3", "+1111111111")
1424 .await
1425 .unwrap();
1426 let cid = mfa.sms.initiate_challenge("sms3").await.unwrap();
1427 assert!(!cid.is_empty());
1428 }
1429
1430 #[tokio::test]
1431 async fn test_sms_generate_and_verify_code() {
1432 let mfa = make_mfa();
1433 mfa.sms
1434 .register_phone_number("sms4", "+2222222222")
1435 .await
1436 .unwrap();
1437 let cid = mfa.sms.initiate_challenge("sms4").await.unwrap();
1438 let code = mfa.sms.generate_code(&cid).await.unwrap();
1439 assert!(mfa.sms.verify_code(&cid, &code).await.unwrap());
1440 }
1441
1442 #[tokio::test]
1443 async fn test_sms_verify_wrong_code() {
1444 let mfa = make_mfa();
1445 mfa.sms
1446 .register_phone_number("sms5", "+3333333333")
1447 .await
1448 .unwrap();
1449 let cid = mfa.sms.initiate_challenge("sms5").await.unwrap();
1450 let _code = mfa.sms.generate_code(&cid).await.unwrap();
1451 assert!(!mfa.sms.verify_code(&cid, "000000").await.unwrap());
1452 }
1453}