koprogo_api/application/use_cases/
two_factor_use_cases.rs1use 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
13pub 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 pub async fn setup_2fa(
36 &self,
37 user_id: Uuid,
38 organization_id: Uuid,
39 ) -> Result<Setup2FAResponseDto, String> {
40 let user = self
42 .user_repo
43 .find_by_id(user_id)
44 .await?
45 .ok_or("User not found")?;
46
47 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 self.two_factor_repo.delete(user_id).await?;
54 }
55
56 let secret = Self::generate_totp_secret();
58 let secret_encrypted = self.encrypt_secret(&secret)?;
59
60 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 let two_factor_secret = TwoFactorSecret::new(user_id, secret_encrypted)?
69 .with_backup_codes(backup_codes_encrypted)?;
70
71 self.two_factor_repo.create(&two_factor_secret).await?;
73
74 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 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(), qr_code_data_url,
92 backup_codes: backup_codes.clone(), issuer,
94 account_name,
95 })
96 }
97
98 pub async fn enable_2fa(
100 &self,
101 user_id: Uuid,
102 organization_id: Uuid,
103 dto: Enable2FADto,
104 ) -> Result<Enable2FAResponseDto, String> {
105 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 let decrypted_secret = self.decrypt_secret(&secret.secret_encrypted)?;
118
119 if !Self::verify_totp_code(&decrypted_secret, &dto.totp_code)? {
121 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 secret.enable()?;
136 secret.mark_used(); self.two_factor_repo.update(&secret).await?;
140
141 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 pub async fn verify_2fa(
160 &self,
161 user_id: Uuid,
162 organization_id: Uuid,
163 dto: Verify2FADto,
164 ) -> Result<Verify2FAResponseDto, String> {
165 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 let decrypted_secret = self.decrypt_secret(&secret.secret_encrypted)?;
178
179 if Self::verify_totp_code(&decrypted_secret, &dto.totp_code)? {
181 secret.mark_used();
183 self.two_factor_repo.update(&secret).await?;
184
185 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 if let Some(code_index) = Self::find_matching_backup_code(&secret, &dto.totp_code)? {
205 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 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 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 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 pub async fn disable_2fa(
263 &self,
264 user_id: Uuid,
265 organization_id: Uuid,
266 dto: Disable2FADto,
267 ) -> Result<(), String> {
268 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 self.two_factor_repo.delete(user_id).await?;
281
282 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 pub async fn regenerate_backup_codes(
297 &self,
298 user_id: Uuid,
299 organization_id: Uuid,
300 dto: RegenerateBackupCodesDto,
301 ) -> Result<RegenerateBackupCodesResponseDto, String> {
302 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 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 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 secret.regenerate_backup_codes(backup_codes_encrypted)?;
328 self.two_factor_repo.update(&secret).await?;
329
330 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 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 fn generate_totp_secret() -> String {
367 TotpGenerator::generate_secret()
368 }
369
370 fn encrypt_secret(&self, secret: &str) -> Result<String, String> {
372 TotpGenerator::encrypt_secret(secret, &self.encryption_key)
373 }
374
375 fn decrypt_secret(&self, encrypted: &str) -> Result<String, String> {
377 TotpGenerator::decrypt_secret(encrypted, &self.encryption_key)
378 }
379
380 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 fn verify_totp_code(secret: &str, code: &str) -> Result<bool, String> {
387 TotpGenerator::verify_code(secret, code)
388 }
389
390 fn generate_backup_codes() -> Vec<String> {
392 TotpGenerator::generate_backup_codes()
393 }
394
395 fn hash_backup_code(code: &str) -> Result<String, String> {
397 TotpGenerator::hash_backup_code(code)
398 }
399
400 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 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 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"); assert!(result.is_ok());
439 assert!(!result.unwrap());
440
441 let result2 = TwoFactorUseCases::verify_totp_code("SECRET", "1234567"); assert!(result2.is_ok());
443 assert!(!result2.unwrap());
444
445 let result3 = TwoFactorUseCases::verify_totp_code("SECRET", "ABCDEF"); assert!(result3.is_ok());
447 assert!(!result3.unwrap());
448 }
449}