koprogo_api/application/use_cases/
two_factor_use_cases.rs

1use crate::application::dto::{
2    Disable2FADto, Enable2FADto, Enable2FAResponseDto, RegenerateBackupCodesDto,
3    RegenerateBackupCodesResponseDto, Setup2FAResponseDto, TwoFactorStatusDto, Verify2FADto,
4    Verify2FAResponseDto,
5};
6use crate::application::ports::{TwoFactorRepository, UserRepository};
7use crate::domain::entities::TwoFactorSecret;
8use crate::infrastructure::audit::{log_audit_event, AuditEventType};
9use crate::infrastructure::totp::TotpGenerator;
10use std::sync::Arc;
11use uuid::Uuid;
12
13/// Use cases for two-factor authentication (TOTP)
14pub struct TwoFactorUseCases {
15    two_factor_repo: Arc<dyn TwoFactorRepository>,
16    user_repo: Arc<dyn UserRepository>,
17    encryption_key: [u8; 32],
18}
19
20impl TwoFactorUseCases {
21    pub fn new(
22        two_factor_repo: Arc<dyn TwoFactorRepository>,
23        user_repo: Arc<dyn UserRepository>,
24        encryption_key: [u8; 32],
25    ) -> Self {
26        Self {
27            two_factor_repo,
28            user_repo,
29            encryption_key,
30        }
31    }
32
33    /// Setup 2FA for a user (returns QR code + backup codes)
34    /// This does NOT enable 2FA yet - user must verify with TOTP code first
35    pub async fn setup_2fa(
36        &self,
37        user_id: Uuid,
38        organization_id: Uuid,
39    ) -> Result<Setup2FAResponseDto, String> {
40        // Check if user exists
41        let user = self
42            .user_repo
43            .find_by_id(user_id)
44            .await?
45            .ok_or("User not found")?;
46
47        // Check if 2FA already exists for this user
48        if let Some(existing) = self.two_factor_repo.find_by_user_id(user_id).await? {
49            if existing.is_enabled {
50                return Err("2FA is already enabled for this user".to_string());
51            }
52            // Delete existing setup if not enabled (allow re-setup)
53            self.two_factor_repo.delete(user_id).await?;
54        }
55
56        // Generate TOTP secret
57        let secret = Self::generate_totp_secret();
58        let secret_encrypted = self.encrypt_secret(&secret)?;
59
60        // Generate 10 backup codes
61        let backup_codes = Self::generate_backup_codes();
62        let backup_codes_encrypted: Vec<String> = backup_codes
63            .iter()
64            .map(|code| Self::hash_backup_code(code))
65            .collect::<Result<Vec<_>, _>>()?;
66
67        // Create TwoFactorSecret entity
68        let two_factor_secret = TwoFactorSecret::new(user_id, secret_encrypted)?
69            .with_backup_codes(backup_codes_encrypted)?;
70
71        // Save to database
72        self.two_factor_repo.create(&two_factor_secret).await?;
73
74        // Generate QR code
75        let issuer = "KoproGo".to_string();
76        let account_name = user.email.clone();
77        let qr_code_data_url = Self::generate_qr_code(&secret, &issuer, &account_name)?;
78
79        // Audit log
80        log_audit_event(
81            AuditEventType::TwoFactorSetupInitiated,
82            Some(user_id),
83            Some(organization_id),
84            Some(format!("User {} initiated 2FA setup", user.email)),
85            None,
86        )
87        .await;
88
89        Ok(Setup2FAResponseDto {
90            secret: secret.clone(), // ONLY returned during setup (never again)
91            qr_code_data_url,
92            backup_codes: backup_codes.clone(), // ONLY shown once (user must save)
93            issuer,
94            account_name,
95        })
96    }
97
98    /// Enable 2FA after user verifies TOTP code
99    pub async fn enable_2fa(
100        &self,
101        user_id: Uuid,
102        organization_id: Uuid,
103        dto: Enable2FADto,
104    ) -> Result<Enable2FAResponseDto, String> {
105        // Find 2FA secret
106        let mut secret = self
107            .two_factor_repo
108            .find_by_user_id(user_id)
109            .await?
110            .ok_or("2FA setup not found. Please run setup first.")?;
111
112        if secret.is_enabled {
113            return Err("2FA is already enabled".to_string());
114        }
115
116        // Decrypt secret
117        let decrypted_secret = self.decrypt_secret(&secret.secret_encrypted)?;
118
119        // Verify TOTP code
120        if !Self::verify_totp_code(&decrypted_secret, &dto.totp_code)? {
121            // Audit log failed verification
122            log_audit_event(
123                AuditEventType::TwoFactorVerificationFailed,
124                Some(user_id),
125                Some(organization_id),
126                Some("Failed TOTP verification during enable".to_string()),
127                None,
128            )
129            .await;
130
131            return Err("Invalid TOTP code".to_string());
132        }
133
134        // Enable 2FA
135        secret.enable()?;
136        secret.mark_used(); // Mark as used immediately
137
138        // Update database
139        self.two_factor_repo.update(&secret).await?;
140
141        // Audit log successful enable
142        log_audit_event(
143            AuditEventType::TwoFactorEnabled,
144            Some(user_id),
145            Some(organization_id),
146            Some("2FA successfully enabled".to_string()),
147            None,
148        )
149        .await;
150
151        Ok(Enable2FAResponseDto {
152            success: true,
153            message: "2FA successfully enabled".to_string(),
154            enabled_at: secret.verified_at.unwrap(),
155        })
156    }
157
158    /// Verify TOTP code or backup code during login
159    pub async fn verify_2fa(
160        &self,
161        user_id: Uuid,
162        organization_id: Uuid,
163        dto: Verify2FADto,
164    ) -> Result<Verify2FAResponseDto, String> {
165        // Find 2FA secret
166        let mut secret = self
167            .two_factor_repo
168            .find_by_user_id(user_id)
169            .await?
170            .ok_or("2FA not enabled for this user")?;
171
172        if !secret.is_enabled {
173            return Err("2FA is not enabled".to_string());
174        }
175
176        // Decrypt secret
177        let decrypted_secret = self.decrypt_secret(&secret.secret_encrypted)?;
178
179        // Try TOTP code first
180        if Self::verify_totp_code(&decrypted_secret, &dto.totp_code)? {
181            // TOTP verification successful
182            secret.mark_used();
183            self.two_factor_repo.update(&secret).await?;
184
185            // Audit log
186            log_audit_event(
187                AuditEventType::TwoFactorVerified,
188                Some(user_id),
189                Some(organization_id),
190                Some("TOTP verification successful".to_string()),
191                None,
192            )
193            .await;
194
195            return Ok(Verify2FAResponseDto {
196                success: true,
197                message: "2FA verification successful".to_string(),
198                backup_code_used: false,
199                backup_codes_remaining: None,
200            });
201        }
202
203        // TOTP failed, try backup codes
204        if let Some(code_index) = Self::find_matching_backup_code(&secret, &dto.totp_code)? {
205            // Backup code matched
206            secret.remove_backup_code(code_index)?;
207            secret.mark_used();
208            self.two_factor_repo.update(&secret).await?;
209
210            let backup_codes_remaining = secret.backup_codes_encrypted.len();
211
212            // Audit log
213            log_audit_event(
214                AuditEventType::BackupCodeUsed,
215                Some(user_id),
216                Some(organization_id),
217                Some(format!(
218                    "Backup code used. {} codes remaining",
219                    backup_codes_remaining
220                )),
221                None,
222            )
223            .await;
224
225            // Warn if backup codes are low
226            if secret.backup_codes_low() {
227                log_audit_event(
228                    AuditEventType::TwoFactorReverificationRequired,
229                    Some(user_id),
230                    Some(organization_id),
231                    Some(format!(
232                        "Warning: Only {} backup codes remaining",
233                        backup_codes_remaining
234                    )),
235                    None,
236                )
237                .await;
238            }
239
240            return Ok(Verify2FAResponseDto {
241                success: true,
242                message: "Backup code verification successful".to_string(),
243                backup_code_used: true,
244                backup_codes_remaining: Some(backup_codes_remaining),
245            });
246        }
247
248        // Both TOTP and backup code failed
249        log_audit_event(
250            AuditEventType::TwoFactorVerificationFailed,
251            Some(user_id),
252            Some(organization_id),
253            Some("Invalid TOTP code and no matching backup code".to_string()),
254            None,
255        )
256        .await;
257
258        Err("Invalid TOTP code or backup code".to_string())
259    }
260
261    /// Disable 2FA (requires current password verification)
262    pub async fn disable_2fa(
263        &self,
264        user_id: Uuid,
265        organization_id: Uuid,
266        dto: Disable2FADto,
267    ) -> Result<(), String> {
268        // Verify user password first (implementation depends on password hashing)
269        let user = self
270            .user_repo
271            .find_by_id(user_id)
272            .await?
273            .ok_or("User not found")?;
274
275        if !Self::verify_password(&user.password_hash, &dto.current_password)? {
276            return Err("Invalid password".to_string());
277        }
278
279        // Delete 2FA configuration
280        self.two_factor_repo.delete(user_id).await?;
281
282        // Audit log
283        log_audit_event(
284            AuditEventType::TwoFactorDisabled,
285            Some(user_id),
286            Some(organization_id),
287            Some("2FA disabled by user".to_string()),
288            None,
289        )
290        .await;
291
292        Ok(())
293    }
294
295    /// Regenerate backup codes (requires TOTP verification)
296    pub async fn regenerate_backup_codes(
297        &self,
298        user_id: Uuid,
299        organization_id: Uuid,
300        dto: RegenerateBackupCodesDto,
301    ) -> Result<RegenerateBackupCodesResponseDto, String> {
302        // Find 2FA secret
303        let mut secret = self
304            .two_factor_repo
305            .find_by_user_id(user_id)
306            .await?
307            .ok_or("2FA not enabled")?;
308
309        if !secret.is_enabled {
310            return Err("2FA is not enabled".to_string());
311        }
312
313        // Verify TOTP code
314        let decrypted_secret = self.decrypt_secret(&secret.secret_encrypted)?;
315        if !Self::verify_totp_code(&decrypted_secret, &dto.totp_code)? {
316            return Err("Invalid TOTP code".to_string());
317        }
318
319        // Generate new backup codes
320        let backup_codes = Self::generate_backup_codes();
321        let backup_codes_encrypted: Vec<String> = backup_codes
322            .iter()
323            .map(|code| Self::hash_backup_code(code))
324            .collect::<Result<Vec<_>, _>>()?;
325
326        // Update secret
327        secret.regenerate_backup_codes(backup_codes_encrypted)?;
328        self.two_factor_repo.update(&secret).await?;
329
330        // Audit log
331        log_audit_event(
332            AuditEventType::BackupCodesRegenerated,
333            Some(user_id),
334            Some(organization_id),
335            Some("Backup codes regenerated".to_string()),
336            None,
337        )
338        .await;
339
340        Ok(RegenerateBackupCodesResponseDto {
341            backup_codes: backup_codes.clone(),
342            regenerated_at: chrono::Utc::now(),
343        })
344    }
345
346    /// Get 2FA status for a user
347    pub async fn get_2fa_status(&self, user_id: Uuid) -> Result<TwoFactorStatusDto, String> {
348        match self.two_factor_repo.find_by_user_id(user_id).await? {
349            Some(secret) => Ok(secret.into()),
350            None => Ok(TwoFactorStatusDto {
351                is_enabled: false,
352                verified_at: None,
353                last_used_at: None,
354                backup_codes_remaining: 0,
355                backup_codes_low: false,
356                needs_reverification: false,
357            }),
358        }
359    }
360
361    // ========================================
362    // Private helper methods
363    // ========================================
364
365    /// Generate TOTP secret (Base32 encoded)
366    fn generate_totp_secret() -> String {
367        TotpGenerator::generate_secret()
368    }
369
370    /// Encrypt TOTP secret (AES-256-GCM)
371    fn encrypt_secret(&self, secret: &str) -> Result<String, String> {
372        TotpGenerator::encrypt_secret(secret, &self.encryption_key)
373    }
374
375    /// Decrypt TOTP secret (AES-256-GCM)
376    fn decrypt_secret(&self, encrypted: &str) -> Result<String, String> {
377        TotpGenerator::decrypt_secret(encrypted, &self.encryption_key)
378    }
379
380    /// Generate QR code data URL
381    fn generate_qr_code(secret: &str, issuer: &str, account_name: &str) -> Result<String, String> {
382        TotpGenerator::generate_qr_code(secret, issuer, account_name)
383    }
384
385    /// Verify TOTP code (6 digits)
386    fn verify_totp_code(secret: &str, code: &str) -> Result<bool, String> {
387        TotpGenerator::verify_code(secret, code)
388    }
389
390    /// Generate 10 backup codes (8-char alphanumeric, uppercase)
391    fn generate_backup_codes() -> Vec<String> {
392        TotpGenerator::generate_backup_codes()
393    }
394
395    /// Hash backup code (bcrypt)
396    fn hash_backup_code(code: &str) -> Result<String, String> {
397        TotpGenerator::hash_backup_code(code)
398    }
399
400    /// Find matching backup code in array
401    fn find_matching_backup_code(
402        secret: &TwoFactorSecret,
403        code: &str,
404    ) -> Result<Option<usize>, String> {
405        for (index, stored_hash) in secret.backup_codes_encrypted.iter().enumerate() {
406            if TotpGenerator::verify_backup_code(code, stored_hash)? {
407                return Ok(Some(index));
408            }
409        }
410        Ok(None)
411    }
412
413    /// Verify user password (bcrypt)
414    fn verify_password(password_hash: &str, password: &str) -> Result<bool, String> {
415        bcrypt::verify(password, password_hash)
416            .map_err(|e| format!("Password verification failed: {}", e))
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_generate_backup_codes() {
426        let codes = TwoFactorUseCases::generate_backup_codes();
427
428        assert_eq!(codes.len(), 10);
429
430        // All codes should be unique
431        let unique_codes: std::collections::HashSet<_> = codes.iter().collect();
432        assert_eq!(unique_codes.len(), 10);
433    }
434
435    #[test]
436    fn test_verify_totp_code_invalid_format() {
437        let result = TwoFactorUseCases::verify_totp_code("SECRET", "12345"); // 5 digits
438        assert!(result.is_ok());
439        assert!(!result.unwrap());
440
441        let result2 = TwoFactorUseCases::verify_totp_code("SECRET", "1234567"); // 7 digits
442        assert!(result2.is_ok());
443        assert!(!result2.unwrap());
444
445        let result3 = TwoFactorUseCases::verify_totp_code("SECRET", "ABCDEF"); // Non-digits
446        assert!(result3.is_ok());
447        assert!(!result3.unwrap());
448    }
449}