Skip to main content

auth_framework/auth_modular/mfa/
mod.rs

1//! Multi-Factor Authentication management module.
2
3pub 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
35// Export SMSKit manager as the primary SMS interface
36pub use sms_kit::{
37    RateLimitConfig as SmsKitRateLimitConfig, SmsKitConfig, SmsKitManager, SmsKitProvider,
38    SmsKitProviderConfig, WebhookConfig,
39};
40
41// Re-export as SmsManager for backward compatibility
42pub use sms_kit::SmsKitManager as SmsManager;
43
44/// Centralized multi-factor authentication (MFA) manager.
45///
46/// `MfaManager` coordinates all MFA operations across different authentication
47/// factors including TOTP, SMS, email, and backup codes. It provides a unified
48/// interface for MFA setup, challenge generation, and verification while
49/// supporting multiple MFA methods simultaneously.
50///
51/// # Supported MFA Methods
52///
53/// - **TOTP (Time-based OTP)**: RFC 6238 compliant authenticator apps
54/// - **SMS**: Text message-based verification codes
55/// - **Email**: Email-based verification codes
56/// - **Backup Codes**: Single-use recovery codes
57///
58/// # Multi-Method Support
59///
60/// Users can enable multiple MFA methods simultaneously, providing flexibility
61/// and redundancy. The manager handles method coordination and fallback scenarios.
62///
63/// # Security Features
64///
65/// - **Challenge Expiration**: Time-limited challenges prevent replay attacks
66/// - **Rate Limiting**: Prevents brute force attacks on MFA codes
67/// - **Secure Code Generation**: Cryptographically secure random code generation
68/// - **Method Validation**: Validates MFA setup before enabling
69/// - **Audit Logging**: Comprehensive logging of all MFA operations
70///
71/// # Cross-Method Operations
72///
73/// The manager supports advanced scenarios like:
74/// - Method fallback when primary method fails
75/// - Cross-method challenge validation
76/// - Method strength assessment
77/// - Risk-based MFA requirements
78///
79/// # Example
80///
81/// ```rust,no_run
82/// use auth_framework::auth_modular::mfa::MfaManager;
83/// use std::sync::Arc;
84///
85/// # #[tokio::main]
86/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
87/// # let storage: Arc<dyn auth_framework::storage::AuthStorage> = unimplemented!();
88/// // Create MFA manager with storage backend
89/// let mfa_manager = MfaManager::new(storage);
90///
91/// // Generate a TOTP secret for a user (store it and show QR code to the user)
92/// let secret = mfa_manager.totp.generate_secret("user123").await?;
93///
94/// // Verify a TOTP code during an authentication attempt
95/// let is_valid = mfa_manager.totp.verify_code("user123", "123456").await?;
96/// # Ok(())
97/// # }
98/// ```
99///
100/// # Thread Safety
101///
102/// The MFA manager is designed for concurrent use and safely coordinates
103/// access to underlying MFA method implementations.
104///
105/// # Storage Integration
106///
107/// Integrates with the framework's storage system to persist:
108/// - User MFA method configurations
109/// - Active challenges
110/// - Usage statistics and audit logs
111/// - Backup codes and secrets
112pub struct MfaManager {
113    /// TOTP manager
114    pub totp: TotpManager,
115
116    /// SMS manager (using SMSKit)
117    pub sms: SmsKitManager,
118
119    /// Email manager
120    pub email: EmailManager,
121
122    /// Backup codes manager
123    pub backup_codes: BackupCodesManager,
124
125    /// Active MFA challenges
126    challenges: Arc<RwLock<HashMap<String, MfaChallenge>>>,
127
128    /// Storage backend for direct manager operations and fallback scenarios
129    storage: Arc<dyn AuthStorage>,
130}
131
132impl MfaManager {
133    /// Create a new MFA manager
134    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    /// Create a new MFA manager with SMSKit configuration
146    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    /// Store an MFA challenge
161    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    /// Guard the global challenge budget and store the challenge.
171    ///
172    /// Returns an error if more than 10 000 challenges are pending; otherwise
173    /// delegates to [`Self::store_challenge`].
174    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    /// Get an MFA challenge
186    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    /// Remove an MFA challenge
192    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    /// Clean up expired challenges
202    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    /// Get count of active challenges
213    pub async fn get_active_challenge_count(&self) -> usize {
214        self.challenges.read().await.len()
215    }
216
217    /// Verify a code against the given MFA challenge, dispatching to the appropriate sub-manager.
218    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    /// MFA CROSS-METHOD OPERATIONS: Step-up authentication with multiple factors
287    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        // Determine required methods based on risk level
300        let adaptive_methods = self
301            .adapt_required_methods(required_methods, risk_level.clone())
302            .await?;
303
304        // Generate challenge ID
305        let challenge_id = uuid::Uuid::new_v4().to_string();
306
307        // Create individual challenges for each method
308        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        // Store the cross-method challenge
349        {
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    /// Complete a specific method within a cross-method challenge
383    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        // Retrieve and update the cross-method challenge
396        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        // Verify the specific method response
409        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            // Mark method as completed
436            cross_challenge
437                .completion_status
438                .insert(method.clone(), true);
439
440            // Update stored challenge
441            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            // Clean up the challenge
461            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    /// Get available MFA methods for a user
481    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        // Check TOTP availability
487        if self.totp.has_totp_secret(user_id).await.unwrap_or(false) {
488            available_methods.push(MfaMethod::Totp);
489        }
490
491        // Check SMS availability
492        if self.sms.has_phone_number(user_id).await.unwrap_or(false) {
493            available_methods.push(MfaMethod::Sms);
494        }
495
496        // Check email availability
497        if self.email.has_email(user_id).await.unwrap_or(false) {
498            available_methods.push(MfaMethod::Email);
499        }
500
501        // Check backup codes availability
502        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    /// Perform method fallback when primary method fails
520    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        // Find the first available fallback method
535        for fallback_method in fallback_order {
536            if available_methods.contains(fallback_method) && fallback_method != &failed_method {
537                // Create challenge for fallback method
538                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    /// Adaptive MFA: Adjust required methods based on risk level
574    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                // Low risk: single factor is sufficient
584                adapted_methods.truncate(1);
585            }
586            RiskLevel::Medium => {
587                // Medium risk: use base methods as-is
588                // No changes needed
589            }
590            RiskLevel::High => {
591                // High risk: require additional verification
592                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                // Critical risk: require all available methods
601                adapted_methods = vec![MfaMethod::Totp, MfaMethod::Sms, MfaMethod::Email];
602            }
603        }
604
605        Ok(adapted_methods)
606    }
607
608    /// Helper methods for cross-method operations
609    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    /// Create individual method challenges
655    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        // Try to look up the user's phone number from storage
700        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        // Fallback: no phone data in storage
712        Ok("Phone on file".to_string())
713    }
714
715    async fn get_email_hint(&self, user_id: &str) -> Result<String> {
716        // Try to look up the user's email from storage
717        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        // Fallback: no email data in storage
734        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    /// Generate a signed emergency MFA bypass token for a specific target user.
820    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    /// Emergency MFA bypass using direct storage access
863    /// This method provides a way to recover when all MFA methods fail
864    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/// MFA method types
922#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
923pub enum MfaMethod {
924    Totp,
925    Sms,
926    Email,
927    BackupCode,
928}
929
930/// Risk levels for adaptive MFA
931#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
932pub enum RiskLevel {
933    Low,
934    Medium,
935    High,
936    Critical,
937}
938
939/// Cross-method challenge combining multiple MFA factors
940#[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/// Individual method challenge
953#[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/// Result of cross-method completion attempt
976#[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/// Result of method fallback operation
986#[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    // ── store / get / remove challenge ──────────────────────────────────
1019
1020    #[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    // ── get_available_methods ───────────────────────────────────────────
1074
1075    #[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        // Only backup codes might be default-available or might be empty
1080        // Either way this shouldn't error
1081        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    // ── step-up authentication ──────────────────────────────────────────
1113
1114    #[tokio::test]
1115    async fn test_initiate_step_up_authentication() {
1116        let mfa = make_mfa();
1117        // Setup TOTP for user
1118        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    /// Helper to create an admin user profile in storage so `user_has_admin_role` passes.
1129    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    // ── emergency bypass ────────────────────────────────────────────────
1143
1144    #[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        // Should return Ok(false) or Err — either way not Ok(true)
1189        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        // Don't set up admin — should fail
1202        let result = mfa
1203            .generate_emergency_bypass_token("notadmin", "target1", Duration::from_secs(300))
1204            .await;
1205        assert!(result.is_err());
1206    }
1207
1208    // ── TOTP sub-module ─────────────────────────────────────────────────
1209
1210    #[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    // ── Backup codes sub-module ─────────────────────────────────────────
1266
1267    #[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        // Code should be consumed — second verify should fail
1281        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    // ── Email MFA sub-module ────────────────────────────────────────────
1323
1324    #[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    // ── SMS MFA sub-module ──────────────────────────────────────────────
1389
1390    #[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}