1use async_trait::async_trait;
2use chrono::{DateTime, Duration, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use thiserror::Error;
7use tokio::sync::RwLock;
8use tracing::{info, warn};
9
10use uvb_core::TenantId;
11
12#[derive(Debug, Error)]
13pub enum BruteForceError {
14 #[error("account locked: {0}")]
15 AccountLocked(String),
16 #[error("too many attempts: {0}")]
17 TooManyAttempts(String),
18 #[error("storage error: {0}")]
19 StorageError(String),
20 #[error("internal error: {0}")]
21 Internal(String),
22}
23
24#[derive(Clone, Debug)]
26pub struct BruteForceConfig {
27 pub max_failed_attempts: u32,
29
30 pub lockout_duration_seconds: i64,
32
33 pub progressive_delay_enabled: bool,
35
36 pub progressive_delay_base_ms: u64,
38
39 pub exponential_lockout: bool,
41
42 pub max_lockout_duration_seconds: i64,
44
45 pub ip_based_tracking: bool,
47
48 pub max_failed_attempts_per_ip: u32,
50
51 pub ip_lockout_duration_seconds: i64,
53
54 pub reset_on_success: bool,
56
57 pub auto_unlock: bool,
59}
60
61impl Default for BruteForceConfig {
62 fn default() -> Self {
63 Self {
64 max_failed_attempts: 5,
65 lockout_duration_seconds: 900, progressive_delay_enabled: true,
67 progressive_delay_base_ms: 1000, exponential_lockout: true,
69 max_lockout_duration_seconds: 86400, ip_based_tracking: true,
71 max_failed_attempts_per_ip: 20,
72 ip_lockout_duration_seconds: 3600, reset_on_success: true,
74 auto_unlock: true,
75 }
76 }
77}
78
79impl BruteForceConfig {
80 pub fn strict() -> Self {
82 Self {
83 max_failed_attempts: 3,
84 lockout_duration_seconds: 1800, progressive_delay_enabled: true,
86 progressive_delay_base_ms: 2000, exponential_lockout: true,
88 max_lockout_duration_seconds: 172800, ip_based_tracking: true,
90 max_failed_attempts_per_ip: 10,
91 ip_lockout_duration_seconds: 7200, reset_on_success: true,
93 auto_unlock: true,
94 }
95 }
96
97 pub fn lenient() -> Self {
99 Self {
100 max_failed_attempts: 10,
101 lockout_duration_seconds: 300, progressive_delay_enabled: false,
103 progressive_delay_base_ms: 500,
104 exponential_lockout: false,
105 max_lockout_duration_seconds: 3600, ip_based_tracking: false,
107 max_failed_attempts_per_ip: 50,
108 ip_lockout_duration_seconds: 600, reset_on_success: true,
110 auto_unlock: true,
111 }
112 }
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
116pub struct FailedAttempt {
117 pub timestamp: DateTime<Utc>,
118 pub ip_address: Option<String>,
119 pub user_agent: Option<String>,
120 pub reason: Option<String>,
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct LockoutRecord {
125 pub locked_at: DateTime<Utc>,
126 pub unlock_at: DateTime<Utc>,
127 pub lockout_count: u32, pub failed_attempts: Vec<FailedAttempt>,
129 pub reason: String,
130}
131
132#[derive(Clone, Debug)]
133pub struct BruteForceCheckResult {
134 pub allowed: bool,
135 pub delay_ms: Option<u64>,
136 pub remaining_attempts: Option<u32>,
137 pub locked_until: Option<DateTime<Utc>>,
138 pub reason: Option<String>,
139}
140
141#[async_trait]
143pub trait BruteForceStore: Send + Sync {
144 async fn record_failed_attempt(
146 &self,
147 tenant_id: &TenantId,
148 user_id: &str,
149 ip_address: Option<&str>,
150 user_agent: Option<&str>,
151 ) -> Result<(), BruteForceError>;
152
153 async fn get_failed_attempts(
155 &self,
156 tenant_id: &TenantId,
157 user_id: &str,
158 ) -> Result<Vec<FailedAttempt>, BruteForceError>;
159
160 async fn clear_failed_attempts(
162 &self,
163 tenant_id: &TenantId,
164 user_id: &str,
165 ) -> Result<(), BruteForceError>;
166
167 async fn set_lockout(
169 &self,
170 tenant_id: &TenantId,
171 user_id: &str,
172 lockout: LockoutRecord,
173 ) -> Result<(), BruteForceError>;
174
175 async fn get_lockout(
177 &self,
178 tenant_id: &TenantId,
179 user_id: &str,
180 ) -> Result<Option<LockoutRecord>, BruteForceError>;
181
182 async fn remove_lockout(
184 &self,
185 tenant_id: &TenantId,
186 user_id: &str,
187 ) -> Result<(), BruteForceError>;
188
189 async fn record_failed_attempt_by_ip(&self, ip_address: &str) -> Result<(), BruteForceError>;
191
192 async fn get_failed_attempts_by_ip(
194 &self,
195 ip_address: &str,
196 ) -> Result<Vec<FailedAttempt>, BruteForceError>;
197
198 async fn is_ip_locked(&self, ip_address: &str) -> Result<bool, BruteForceError>;
200}
201
202pub struct MemoryBruteForceStore {
204 user_attempts: Arc<RwLock<HashMap<String, Vec<FailedAttempt>>>>,
205 user_lockouts: Arc<RwLock<HashMap<String, LockoutRecord>>>,
206 ip_attempts: Arc<RwLock<HashMap<String, Vec<FailedAttempt>>>>,
207 ip_lockouts: Arc<RwLock<HashMap<String, DateTime<Utc>>>>,
208}
209
210impl MemoryBruteForceStore {
211 pub fn new() -> Self {
212 Self {
213 user_attempts: Arc::new(RwLock::new(HashMap::new())),
214 user_lockouts: Arc::new(RwLock::new(HashMap::new())),
215 ip_attempts: Arc::new(RwLock::new(HashMap::new())),
216 ip_lockouts: Arc::new(RwLock::new(HashMap::new())),
217 }
218 }
219
220 fn user_key(tenant_id: &TenantId, user_id: &str) -> String {
221 format!("{}:{}", tenant_id, user_id)
222 }
223}
224
225impl Default for MemoryBruteForceStore {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[async_trait]
232impl BruteForceStore for MemoryBruteForceStore {
233 async fn record_failed_attempt(
234 &self,
235 tenant_id: &TenantId,
236 user_id: &str,
237 ip_address: Option<&str>,
238 user_agent: Option<&str>,
239 ) -> Result<(), BruteForceError> {
240 let key = Self::user_key(tenant_id, user_id);
241 let attempt = FailedAttempt {
242 timestamp: Utc::now(),
243 ip_address: ip_address.map(|s| s.to_string()),
244 user_agent: user_agent.map(|s| s.to_string()),
245 reason: None,
246 };
247
248 let mut attempts = self.user_attempts.write().await;
249 attempts.entry(key).or_insert_with(Vec::new).push(attempt);
250
251 Ok(())
252 }
253
254 async fn get_failed_attempts(
255 &self,
256 tenant_id: &TenantId,
257 user_id: &str,
258 ) -> Result<Vec<FailedAttempt>, BruteForceError> {
259 let key = Self::user_key(tenant_id, user_id);
260 let attempts = self.user_attempts.read().await;
261 Ok(attempts.get(&key).cloned().unwrap_or_default())
262 }
263
264 async fn clear_failed_attempts(
265 &self,
266 tenant_id: &TenantId,
267 user_id: &str,
268 ) -> Result<(), BruteForceError> {
269 let key = Self::user_key(tenant_id, user_id);
270 let mut attempts = self.user_attempts.write().await;
271 attempts.remove(&key);
272 Ok(())
273 }
274
275 async fn set_lockout(
276 &self,
277 tenant_id: &TenantId,
278 user_id: &str,
279 lockout: LockoutRecord,
280 ) -> Result<(), BruteForceError> {
281 let key = Self::user_key(tenant_id, user_id);
282 let mut lockouts = self.user_lockouts.write().await;
283 lockouts.insert(key, lockout);
284 Ok(())
285 }
286
287 async fn get_lockout(
288 &self,
289 tenant_id: &TenantId,
290 user_id: &str,
291 ) -> Result<Option<LockoutRecord>, BruteForceError> {
292 let key = Self::user_key(tenant_id, user_id);
293 let lockouts = self.user_lockouts.read().await;
294 Ok(lockouts.get(&key).cloned())
295 }
296
297 async fn remove_lockout(
298 &self,
299 tenant_id: &TenantId,
300 user_id: &str,
301 ) -> Result<(), BruteForceError> {
302 let key = Self::user_key(tenant_id, user_id);
303 let mut lockouts = self.user_lockouts.write().await;
304 lockouts.remove(&key);
305 Ok(())
306 }
307
308 async fn record_failed_attempt_by_ip(&self, ip_address: &str) -> Result<(), BruteForceError> {
309 let attempt = FailedAttempt {
310 timestamp: Utc::now(),
311 ip_address: Some(ip_address.to_string()),
312 user_agent: None,
313 reason: None,
314 };
315
316 let mut attempts = self.ip_attempts.write().await;
317 attempts
318 .entry(ip_address.to_string())
319 .or_insert_with(Vec::new)
320 .push(attempt);
321
322 Ok(())
323 }
324
325 async fn get_failed_attempts_by_ip(
326 &self,
327 ip_address: &str,
328 ) -> Result<Vec<FailedAttempt>, BruteForceError> {
329 let attempts = self.ip_attempts.read().await;
330 Ok(attempts.get(ip_address).cloned().unwrap_or_default())
331 }
332
333 async fn is_ip_locked(&self, ip_address: &str) -> Result<bool, BruteForceError> {
334 let lockouts = self.ip_lockouts.read().await;
335 if let Some(unlock_at) = lockouts.get(ip_address) {
336 Ok(Utc::now() < *unlock_at)
337 } else {
338 Ok(false)
339 }
340 }
341}
342
343pub struct BruteForceProtection {
345 config: BruteForceConfig,
346 store: Arc<dyn BruteForceStore>,
347}
348
349impl BruteForceProtection {
350 pub fn new(config: BruteForceConfig, store: Arc<dyn BruteForceStore>) -> Self {
351 Self { config, store }
352 }
353
354 pub async fn check_authentication_allowed(
356 &self,
357 tenant_id: &TenantId,
358 user_id: &str,
359 ip_address: Option<&str>,
360 ) -> Result<BruteForceCheckResult, BruteForceError> {
361 if self.config.ip_based_tracking {
363 if let Some(ip) = ip_address {
364 if self.store.is_ip_locked(ip).await? {
365 return Ok(BruteForceCheckResult {
366 allowed: false,
367 delay_ms: None,
368 remaining_attempts: None,
369 locked_until: None,
370 reason: Some("IP address is temporarily blocked".to_string()),
371 });
372 }
373
374 let ip_attempts = self.store.get_failed_attempts_by_ip(ip).await?;
375 let recent_ip_attempts = self.count_recent_attempts(&ip_attempts);
376
377 if recent_ip_attempts >= self.config.max_failed_attempts_per_ip {
378 warn!("IP {} exceeded maximum attempts", ip);
379 return Ok(BruteForceCheckResult {
380 allowed: false,
381 delay_ms: None,
382 remaining_attempts: None,
383 locked_until: None,
384 reason: Some("Too many failed attempts from this IP address".to_string()),
385 });
386 }
387 }
388 }
389
390 if let Some(lockout) = self.store.get_lockout(tenant_id, user_id).await? {
392 if self.config.auto_unlock && Utc::now() >= lockout.unlock_at {
393 self.store.remove_lockout(tenant_id, user_id).await?;
395 info!("Auto-unlocked account for user {}", user_id);
396 } else {
397 warn!("Account locked for user {}", user_id);
398 return Ok(BruteForceCheckResult {
399 allowed: false,
400 delay_ms: None,
401 remaining_attempts: None,
402 locked_until: Some(lockout.unlock_at),
403 reason: Some(lockout.reason),
404 });
405 }
406 }
407
408 let attempts = self.store.get_failed_attempts(tenant_id, user_id).await?;
410 let recent_attempts = self.count_recent_attempts(&attempts);
411
412 if recent_attempts >= self.config.max_failed_attempts {
413 self.lock_account(tenant_id, user_id, attempts).await?;
415
416 return Ok(BruteForceCheckResult {
417 allowed: false,
418 delay_ms: None,
419 remaining_attempts: Some(0),
420 locked_until: None,
421 reason: Some("Account locked due to too many failed attempts".to_string()),
422 });
423 }
424
425 let delay_ms = if self.config.progressive_delay_enabled && recent_attempts > 0 {
427 Some(self.calculate_progressive_delay(recent_attempts))
428 } else {
429 None
430 };
431
432 Ok(BruteForceCheckResult {
433 allowed: true,
434 delay_ms,
435 remaining_attempts: Some(self.config.max_failed_attempts - recent_attempts),
436 locked_until: None,
437 reason: None,
438 })
439 }
440
441 pub async fn record_failed_attempt(
443 &self,
444 tenant_id: &TenantId,
445 user_id: &str,
446 ip_address: Option<&str>,
447 user_agent: Option<&str>,
448 ) -> Result<(), BruteForceError> {
449 self.store
450 .record_failed_attempt(tenant_id, user_id, ip_address, user_agent)
451 .await?;
452
453 if self.config.ip_based_tracking {
454 if let Some(ip) = ip_address {
455 self.store.record_failed_attempt_by_ip(ip).await?;
456 }
457 }
458
459 info!("Recorded failed attempt for user {}", user_id);
460
461 Ok(())
462 }
463
464 pub async fn record_successful_attempt(
466 &self,
467 tenant_id: &TenantId,
468 user_id: &str,
469 ) -> Result<(), BruteForceError> {
470 if self.config.reset_on_success {
471 self.store.clear_failed_attempts(tenant_id, user_id).await?;
472 info!(
473 "Cleared failed attempts for user {} after successful login",
474 user_id
475 );
476 }
477
478 Ok(())
479 }
480
481 pub async fn unlock_account(
483 &self,
484 tenant_id: &TenantId,
485 user_id: &str,
486 ) -> Result<(), BruteForceError> {
487 self.store.remove_lockout(tenant_id, user_id).await?;
488 self.store.clear_failed_attempts(tenant_id, user_id).await?;
489 info!("Manually unlocked account for user {}", user_id);
490 Ok(())
491 }
492
493 fn count_recent_attempts(&self, attempts: &[FailedAttempt]) -> u32 {
496 let cutoff = Utc::now() - Duration::seconds(self.config.lockout_duration_seconds);
497 attempts.iter().filter(|a| a.timestamp > cutoff).count() as u32
498 }
499
500 fn calculate_progressive_delay(&self, attempt_count: u32) -> u64 {
501 let multiplier = 2u64.pow(attempt_count.saturating_sub(1));
503 (self.config.progressive_delay_base_ms * multiplier).min(30000) }
505
506 async fn lock_account(
507 &self,
508 tenant_id: &TenantId,
509 user_id: &str,
510 attempts: Vec<FailedAttempt>,
511 ) -> Result<(), BruteForceError> {
512 let existing_lockout = self.store.get_lockout(tenant_id, user_id).await?;
513 let lockout_count = existing_lockout.map(|l| l.lockout_count).unwrap_or(0) + 1;
514
515 let lockout_duration = if self.config.exponential_lockout {
516 let duration = self.config.lockout_duration_seconds * (2i64.pow(lockout_count - 1));
518 duration.min(self.config.max_lockout_duration_seconds)
519 } else {
520 self.config.lockout_duration_seconds
521 };
522
523 let lockout = LockoutRecord {
524 locked_at: Utc::now(),
525 unlock_at: Utc::now() + Duration::seconds(lockout_duration),
526 lockout_count,
527 failed_attempts: attempts,
528 reason: format!(
529 "Account locked for {} seconds due to {} failed login attempts",
530 lockout_duration, self.config.max_failed_attempts
531 ),
532 };
533
534 self.store.set_lockout(tenant_id, user_id, lockout).await?;
535
536 warn!(
537 "Locked account for user {} (lockout #{}, duration: {}s)",
538 user_id, lockout_count, lockout_duration
539 );
540
541 Ok(())
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 fn test_tenant_id() -> TenantId {
550 TenantId::new("test_tenant")
551 }
552
553 #[tokio::test]
554 async fn test_default_config() {
555 let config = BruteForceConfig::default();
556 assert_eq!(config.max_failed_attempts, 5);
557 assert_eq!(config.lockout_duration_seconds, 900);
558 assert!(config.progressive_delay_enabled);
559 }
560
561 #[tokio::test]
562 async fn test_allow_authentication_no_attempts() {
563 let config = BruteForceConfig::default();
564 let store = Arc::new(MemoryBruteForceStore::new());
565 let protection = BruteForceProtection::new(config, store);
566
567 let result = protection
568 .check_authentication_allowed(&test_tenant_id(), "user1", None)
569 .await
570 .unwrap();
571
572 assert!(result.allowed);
573 assert_eq!(result.remaining_attempts, Some(5));
574 }
575
576 #[tokio::test]
577 async fn test_failed_attempts_counting() {
578 let config = BruteForceConfig::default();
579 let store = Arc::new(MemoryBruteForceStore::new());
580 let protection = BruteForceProtection::new(config, store);
581
582 let tenant_id = test_tenant_id();
583 let user_id = "user1";
584
585 for _ in 0..3 {
587 protection
588 .record_failed_attempt(&tenant_id, user_id, Some("192.168.1.1"), None)
589 .await
590 .unwrap();
591 }
592
593 let result = protection
594 .check_authentication_allowed(&tenant_id, user_id, Some("192.168.1.1"))
595 .await
596 .unwrap();
597
598 assert!(result.allowed);
599 assert_eq!(result.remaining_attempts, Some(2)); }
601
602 #[tokio::test]
603 async fn test_account_lockout() {
604 let config = BruteForceConfig::default();
605 let store = Arc::new(MemoryBruteForceStore::new());
606 let protection = BruteForceProtection::new(config, store);
607
608 let tenant_id = test_tenant_id();
609 let user_id = "user1";
610
611 for _ in 0..5 {
613 protection
614 .record_failed_attempt(&tenant_id, user_id, Some("192.168.1.1"), None)
615 .await
616 .unwrap();
617 }
618
619 let result = protection
620 .check_authentication_allowed(&tenant_id, user_id, Some("192.168.1.1"))
621 .await
622 .unwrap();
623
624 assert!(!result.allowed);
625 assert_eq!(result.remaining_attempts, Some(0));
626 assert!(result.locked_until.is_some() || result.reason.is_some());
627 }
628
629 #[tokio::test]
630 async fn test_progressive_delay() {
631 let config = BruteForceConfig::default();
632 let protection = BruteForceProtection::new(config, Arc::new(MemoryBruteForceStore::new()));
633
634 assert_eq!(protection.calculate_progressive_delay(1), 1000); assert_eq!(protection.calculate_progressive_delay(2), 2000); assert_eq!(protection.calculate_progressive_delay(3), 4000); assert_eq!(protection.calculate_progressive_delay(4), 8000); }
640
641 #[tokio::test]
642 async fn test_reset_on_success() {
643 let config = BruteForceConfig {
644 reset_on_success: true,
645 ..Default::default()
646 };
647 let store = Arc::new(MemoryBruteForceStore::new());
648 let protection = BruteForceProtection::new(config, store.clone());
649
650 let tenant_id = test_tenant_id();
651 let user_id = "user1";
652
653 for _ in 0..3 {
655 protection
656 .record_failed_attempt(&tenant_id, user_id, None, None)
657 .await
658 .unwrap();
659 }
660
661 let attempts = store
663 .get_failed_attempts(&tenant_id, user_id)
664 .await
665 .unwrap();
666 assert_eq!(attempts.len(), 3);
667
668 protection
670 .record_successful_attempt(&tenant_id, user_id)
671 .await
672 .unwrap();
673
674 let attempts = store
676 .get_failed_attempts(&tenant_id, user_id)
677 .await
678 .unwrap();
679 assert_eq!(attempts.len(), 0);
680 }
681
682 #[tokio::test]
683 async fn test_manual_unlock() {
684 let config = BruteForceConfig::default();
685 let store = Arc::new(MemoryBruteForceStore::new());
686 let protection = BruteForceProtection::new(config, store.clone());
687
688 let tenant_id = test_tenant_id();
689 let user_id = "user1";
690
691 for _ in 0..5 {
693 protection
694 .record_failed_attempt(&tenant_id, user_id, None, None)
695 .await
696 .unwrap();
697 }
698
699 let result = protection
701 .check_authentication_allowed(&tenant_id, user_id, None)
702 .await
703 .unwrap();
704 assert!(!result.allowed);
705
706 protection
708 .unlock_account(&tenant_id, user_id)
709 .await
710 .unwrap();
711
712 let result = protection
714 .check_authentication_allowed(&tenant_id, user_id, None)
715 .await
716 .unwrap();
717 assert!(result.allowed);
718 }
719
720 #[tokio::test]
721 async fn test_ip_based_tracking() {
722 let config = BruteForceConfig {
723 ip_based_tracking: true,
724 max_failed_attempts_per_ip: 3,
725 ..Default::default()
726 };
727 let store = Arc::new(MemoryBruteForceStore::new());
728 let protection = BruteForceProtection::new(config, store);
729
730 let tenant_id = test_tenant_id();
731
732 for i in 0..3 {
734 protection
735 .record_failed_attempt(&tenant_id, &format!("user{}", i), Some("192.168.1.1"), None)
736 .await
737 .unwrap();
738 }
739
740 let result = protection
742 .check_authentication_allowed(&tenant_id, "user999", Some("192.168.1.1"))
743 .await
744 .unwrap();
745
746 assert!(!result.allowed);
747 assert!(result.reason.is_some());
748 }
749
750 #[tokio::test]
755 async fn test_distributed_attack_from_multiple_ips() {
756 let config = BruteForceConfig::default();
759 let store = Arc::new(MemoryBruteForceStore::new());
760 let protection = BruteForceProtection::new(config, store);
761
762 let tenant_id = test_tenant_id();
763 let user_id = "target_user";
764
765 let ips = vec![
767 "192.168.1.1",
768 "192.168.1.2",
769 "192.168.1.3",
770 "192.168.1.4",
771 "192.168.1.5",
772 "10.0.0.1",
773 "10.0.0.2",
774 "10.0.0.3",
775 "10.0.0.4",
776 "10.0.0.5",
777 ];
778
779 for ip in &ips {
780 for _ in 0..2 {
781 protection
782 .record_failed_attempt(&tenant_id, user_id, Some(ip), Some("Attack-UA"))
783 .await
784 .unwrap();
785 }
786 }
787
788 let result = protection
790 .check_authentication_allowed(&tenant_id, user_id, Some("192.168.1.100"))
791 .await
792 .unwrap();
793
794 assert!(
795 !result.allowed,
796 "User account should be locked despite distributed IPs"
797 );
798 assert_eq!(result.remaining_attempts, Some(0));
799
800 let result_ip = protection
802 .check_authentication_allowed(&tenant_id, "different_user", Some("192.168.1.1"))
803 .await
804 .unwrap();
805
806 assert!(
807 result_ip.allowed,
808 "IP should not be blocked (only 2 attempts from this IP)"
809 );
810 }
811
812 #[tokio::test]
813 async fn test_credential_stuffing_pattern() {
814 let config = BruteForceConfig {
816 ip_based_tracking: true,
817 max_failed_attempts_per_ip: 10,
818 max_failed_attempts: 3,
819 ..Default::default()
820 };
821 let store = Arc::new(MemoryBruteForceStore::new());
822 let protection = BruteForceProtection::new(config, store);
823
824 let tenant_id = test_tenant_id();
825 let attacker_ip = "203.0.113.50";
826
827 for i in 0..15 {
829 let user_id = format!("victim_{}", i);
830 protection
831 .record_failed_attempt(
832 &tenant_id,
833 &user_id,
834 Some(attacker_ip),
835 Some("Credential-Stuffer"),
836 )
837 .await
838 .unwrap();
839 }
840
841 let result = protection
843 .check_authentication_allowed(&tenant_id, "new_victim", Some(attacker_ip))
844 .await
845 .unwrap();
846
847 assert!(
848 !result.allowed,
849 "IP should be blocked after credential stuffing attempts"
850 );
851 assert!(result.reason.unwrap().contains("IP"));
852 }
853
854 #[tokio::test]
855 async fn test_password_spraying_attack() {
856 let config = BruteForceConfig {
859 ip_based_tracking: true,
860 max_failed_attempts_per_ip: 20,
861 max_failed_attempts: 5,
862 ..Default::default()
863 };
864 let store = Arc::new(MemoryBruteForceStore::new());
865 let protection = BruteForceProtection::new(config, store);
866
867 let tenant_id = test_tenant_id();
868 let attacker_ip = "198.51.100.25";
869
870 for i in 0..25 {
872 let user_id = format!("employee_{}", i);
873 protection
874 .record_failed_attempt(
875 &tenant_id,
876 &user_id,
877 Some(attacker_ip),
878 Some("Password-Sprayer"),
879 )
880 .await
881 .unwrap();
882 }
883
884 let result = protection
886 .check_authentication_allowed(&tenant_id, "employee_26", Some(attacker_ip))
887 .await
888 .unwrap();
889
890 assert!(
891 !result.allowed,
892 "Password spraying should be detected via IP tracking"
893 );
894
895 let result_user = protection
897 .check_authentication_allowed(&tenant_id, "employee_5", Some("192.168.1.1"))
898 .await
899 .unwrap();
900
901 assert!(
902 result_user.allowed,
903 "Individual accounts should not be locked with only 1 attempt"
904 );
905 assert_eq!(result_user.remaining_attempts, Some(4));
906 }
907
908 #[tokio::test]
909 async fn test_account_enumeration_attempt() {
910 let config = BruteForceConfig {
913 ip_based_tracking: true,
914 max_failed_attempts_per_ip: 50,
915 ..Default::default()
916 };
917 let store = Arc::new(MemoryBruteForceStore::new());
918 let protection = BruteForceProtection::new(config, store);
919
920 let tenant_id = test_tenant_id();
921 let scanner_ip = "192.0.2.100";
922
923 for i in 0..60 {
925 let user_id = format!("test_user_{}", i);
926 protection
927 .record_failed_attempt(&tenant_id, &user_id, Some(scanner_ip), Some("Scanner"))
928 .await
929 .unwrap();
930 }
931
932 let result = protection
934 .check_authentication_allowed(&tenant_id, "admin", Some(scanner_ip))
935 .await
936 .unwrap();
937
938 assert!(
939 !result.allowed,
940 "Account enumeration should be blocked via IP rate limiting"
941 );
942 }
943
944 #[tokio::test]
945 async fn test_timing_attack_resistance() {
946 let config = BruteForceConfig {
948 progressive_delay_enabled: true,
949 progressive_delay_base_ms: 1000,
950 ..Default::default()
951 };
952 let store = Arc::new(MemoryBruteForceStore::new());
953 let protection = BruteForceProtection::new(config, store);
954
955 let tenant_id = test_tenant_id();
956 let user_id = "timing_target";
957
958 let mut previous_delay = 0u64;
960
961 for attempt in 1..=4 {
962 protection
963 .record_failed_attempt(&tenant_id, user_id, Some("192.168.1.1"), None)
964 .await
965 .unwrap();
966
967 let result = protection
968 .check_authentication_allowed(&tenant_id, user_id, Some("192.168.1.1"))
969 .await
970 .unwrap();
971
972 if let Some(delay) = result.delay_ms {
973 assert!(
974 delay > previous_delay,
975 "Delay should increase progressively (attempt {}: {}ms vs previous {}ms)",
976 attempt,
977 delay,
978 previous_delay
979 );
980 previous_delay = delay;
981 }
982 }
983
984 assert!(
986 previous_delay >= 4000,
987 "After 4 attempts, delay should be at least 4 seconds"
988 );
989 }
990
991 #[tokio::test]
992 async fn test_slow_distributed_attack() {
993 let config = BruteForceConfig {
996 ip_based_tracking: true,
997 max_failed_attempts_per_ip: 5,
998 max_failed_attempts: 8,
999 ..Default::default()
1000 };
1001 let store = Arc::new(MemoryBruteForceStore::new());
1002 let protection = BruteForceProtection::new(config, store);
1003
1004 let tenant_id = test_tenant_id();
1005 let user_id = "high_value_target";
1006
1007 let ips = vec!["10.1.1.1", "10.2.2.2", "10.3.3.3"];
1009
1010 for ip in &ips {
1011 for _ in 0..4 {
1012 protection
1013 .record_failed_attempt(&tenant_id, user_id, Some(ip), None)
1014 .await
1015 .unwrap();
1016 }
1017 }
1018
1019 let result = protection
1021 .check_authentication_allowed(&tenant_id, user_id, Some("10.4.4.4"))
1022 .await
1023 .unwrap();
1024
1025 assert!(
1026 !result.allowed,
1027 "User should be locked despite distributed slow attack"
1028 );
1029
1030 for ip in &ips {
1032 let result_ip = protection
1033 .check_authentication_allowed(&tenant_id, "other_user", Some(ip))
1034 .await
1035 .unwrap();
1036
1037 assert!(result_ip.allowed, "IP {} should not be blocked", ip);
1038 }
1039 }
1040
1041 #[tokio::test]
1042 async fn test_exponential_lockout_escalation() {
1043 let config = BruteForceConfig {
1046 max_failed_attempts: 3,
1047 exponential_lockout: true,
1048 lockout_duration_seconds: 60, max_lockout_duration_seconds: 480, auto_unlock: true,
1051 ..Default::default()
1052 };
1053 let store = Arc::new(MemoryBruteForceStore::new());
1054 let protection = BruteForceProtection::new(config, store.clone());
1055
1056 let tenant_id = test_tenant_id();
1057 let user_id = "repeat_offender";
1058
1059 for _ in 0..3 {
1061 protection
1062 .record_failed_attempt(&tenant_id, user_id, None, None)
1063 .await
1064 .unwrap();
1065 }
1066
1067 protection
1069 .check_authentication_allowed(&tenant_id, user_id, None)
1070 .await
1071 .unwrap();
1072
1073 let lockout1 = store.get_lockout(&tenant_id, user_id).await.unwrap();
1074 assert!(lockout1.is_some(), "First lockout should exist");
1075 let lockout1 = lockout1.unwrap();
1076 assert_eq!(lockout1.lockout_count, 1);
1077 let duration1 = (lockout1.unlock_at - lockout1.locked_at).num_seconds();
1078
1079 assert_eq!(duration1, 60, "First lockout should be 60 seconds");
1081
1082 let test_lockout = LockoutRecord {
1086 locked_at: Utc::now(),
1087 unlock_at: Utc::now() + Duration::seconds(120), lockout_count: 2,
1089 failed_attempts: vec![],
1090 reason: "Test".to_string(),
1091 };
1092 store
1093 .set_lockout(&tenant_id, "test_user2", test_lockout)
1094 .await
1095 .unwrap();
1096
1097 let lockout2 = store.get_lockout(&tenant_id, "test_user2").await.unwrap();
1098 assert!(lockout2.is_some());
1099 let lockout2 = lockout2.unwrap();
1100 assert_eq!(lockout2.lockout_count, 2);
1101
1102 }
1105
1106 #[tokio::test]
1107 async fn test_mixed_attack_vectors() {
1108 let config = BruteForceConfig {
1110 ip_based_tracking: true,
1111 max_failed_attempts_per_ip: 15,
1112 max_failed_attempts: 5,
1113 progressive_delay_enabled: true,
1114 ..Default::default()
1115 };
1116 let store = Arc::new(MemoryBruteForceStore::new());
1117 let protection = BruteForceProtection::new(config, store);
1118
1119 let tenant_id = test_tenant_id();
1120
1121 for i in 0..8 {
1123 protection
1124 .record_failed_attempt(
1125 &tenant_id,
1126 &format!("user_{}", i),
1127 Some("203.0.113.1"),
1128 None,
1129 )
1130 .await
1131 .unwrap();
1132 }
1133
1134 for i in 0..12 {
1136 protection
1137 .record_failed_attempt(
1138 &tenant_id,
1139 &format!("admin_{}", i),
1140 Some("203.0.113.2"),
1141 None,
1142 )
1143 .await
1144 .unwrap();
1145 }
1146
1147 for _ in 0..5 {
1149 protection
1150 .record_failed_attempt(&tenant_id, "ceo", Some("203.0.113.3"), None)
1151 .await
1152 .unwrap();
1153 }
1154
1155 let result1 = protection
1158 .check_authentication_allowed(&tenant_id, "new_user", Some("203.0.113.1"))
1159 .await
1160 .unwrap();
1161 assert!(result1.allowed);
1162
1163 let result2 = protection
1165 .check_authentication_allowed(&tenant_id, "new_admin", Some("203.0.113.2"))
1166 .await
1167 .unwrap();
1168 assert!(result2.allowed);
1169
1170 let result3 = protection
1172 .check_authentication_allowed(&tenant_id, "ceo", Some("203.0.113.4"))
1173 .await
1174 .unwrap();
1175 assert!(!result3.allowed, "Targeted account should be locked");
1176 }
1177}