Skip to main content

uvb_brute_force/
lib.rs

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/// Brute force protection configuration
25#[derive(Clone, Debug)]
26pub struct BruteForceConfig {
27    /// Maximum failed attempts before lockout
28    pub max_failed_attempts: u32,
29
30    /// Lockout duration in seconds
31    pub lockout_duration_seconds: i64,
32
33    /// Enable progressive delays after each failed attempt
34    pub progressive_delay_enabled: bool,
35
36    /// Base delay in milliseconds for progressive delays
37    pub progressive_delay_base_ms: u64,
38
39    /// Enable exponential lockout (doubles duration with each lockout)
40    pub exponential_lockout: bool,
41
42    /// Maximum lockout duration in seconds
43    pub max_lockout_duration_seconds: i64,
44
45    /// Enable IP-based tracking in addition to user-based
46    pub ip_based_tracking: bool,
47
48    /// Maximum failed attempts per IP
49    pub max_failed_attempts_per_ip: u32,
50
51    /// IP lockout duration in seconds
52    pub ip_lockout_duration_seconds: i64,
53
54    /// Reset failed attempts counter after successful login
55    pub reset_on_success: bool,
56
57    /// Enable automatic unlocking after lockout duration expires
58    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, // 15 minutes
66            progressive_delay_enabled: true,
67            progressive_delay_base_ms: 1000, // 1 second
68            exponential_lockout: true,
69            max_lockout_duration_seconds: 86400, // 24 hours
70            ip_based_tracking: true,
71            max_failed_attempts_per_ip: 20,
72            ip_lockout_duration_seconds: 3600, // 1 hour
73            reset_on_success: true,
74            auto_unlock: true,
75        }
76    }
77}
78
79impl BruteForceConfig {
80    /// Strict configuration for high-security environments
81    pub fn strict() -> Self {
82        Self {
83            max_failed_attempts: 3,
84            lockout_duration_seconds: 1800, // 30 minutes
85            progressive_delay_enabled: true,
86            progressive_delay_base_ms: 2000, // 2 seconds
87            exponential_lockout: true,
88            max_lockout_duration_seconds: 172800, // 48 hours
89            ip_based_tracking: true,
90            max_failed_attempts_per_ip: 10,
91            ip_lockout_duration_seconds: 7200, // 2 hours
92            reset_on_success: true,
93            auto_unlock: true,
94        }
95    }
96
97    /// Lenient configuration for development
98    pub fn lenient() -> Self {
99        Self {
100            max_failed_attempts: 10,
101            lockout_duration_seconds: 300, // 5 minutes
102            progressive_delay_enabled: false,
103            progressive_delay_base_ms: 500,
104            exponential_lockout: false,
105            max_lockout_duration_seconds: 3600, // 1 hour
106            ip_based_tracking: false,
107            max_failed_attempts_per_ip: 50,
108            ip_lockout_duration_seconds: 600, // 10 minutes
109            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, // Number of times this account has been locked
128    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/// Trait for brute force protection storage
142#[async_trait]
143pub trait BruteForceStore: Send + Sync {
144    /// Record a failed authentication attempt
145    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    /// Get failed attempts for a user
154    async fn get_failed_attempts(
155        &self,
156        tenant_id: &TenantId,
157        user_id: &str,
158    ) -> Result<Vec<FailedAttempt>, BruteForceError>;
159
160    /// Clear failed attempts for a user
161    async fn clear_failed_attempts(
162        &self,
163        tenant_id: &TenantId,
164        user_id: &str,
165    ) -> Result<(), BruteForceError>;
166
167    /// Set lockout for a user
168    async fn set_lockout(
169        &self,
170        tenant_id: &TenantId,
171        user_id: &str,
172        lockout: LockoutRecord,
173    ) -> Result<(), BruteForceError>;
174
175    /// Get lockout status for a user
176    async fn get_lockout(
177        &self,
178        tenant_id: &TenantId,
179        user_id: &str,
180    ) -> Result<Option<LockoutRecord>, BruteForceError>;
181
182    /// Remove lockout for a user
183    async fn remove_lockout(
184        &self,
185        tenant_id: &TenantId,
186        user_id: &str,
187    ) -> Result<(), BruteForceError>;
188
189    /// Record failed attempt by IP
190    async fn record_failed_attempt_by_ip(&self, ip_address: &str) -> Result<(), BruteForceError>;
191
192    /// Get failed attempts by IP
193    async fn get_failed_attempts_by_ip(
194        &self,
195        ip_address: &str,
196    ) -> Result<Vec<FailedAttempt>, BruteForceError>;
197
198    /// Check if IP is locked
199    async fn is_ip_locked(&self, ip_address: &str) -> Result<bool, BruteForceError>;
200}
201
202/// In-memory implementation of brute force store for testing
203pub 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
343/// Brute force protection service
344pub 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    /// Check if authentication should be allowed
355    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        // Check IP-based lockout first
362        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        // Check user-based lockout
391        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                // Lockout expired, remove it
394                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        // Check failed attempts
409        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            // Lock the account
414            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        // Calculate progressive delay if enabled
426        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    /// Record a failed authentication attempt
442    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    /// Record a successful authentication
465    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    /// Manually unlock an account
482    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    // Private helper methods
494
495    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        // Exponential backoff: base_delay * 2^(attempt_count - 1)
502        let multiplier = 2u64.pow(attempt_count.saturating_sub(1));
503        (self.config.progressive_delay_base_ms * multiplier).min(30000) // Max 30 seconds
504    }
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            // Double duration with each lockout
517            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        // Record 3 failed attempts
586        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)); // 5 - 3 = 2
600    }
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        // Record 5 failed attempts (max)
612        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        // Test exponential backoff
635        assert_eq!(protection.calculate_progressive_delay(1), 1000); // 1s
636        assert_eq!(protection.calculate_progressive_delay(2), 2000); // 2s
637        assert_eq!(protection.calculate_progressive_delay(3), 4000); // 4s
638        assert_eq!(protection.calculate_progressive_delay(4), 8000); // 8s
639    }
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        // Record 3 failed attempts
654        for _ in 0..3 {
655            protection
656                .record_failed_attempt(&tenant_id, user_id, None, None)
657                .await
658                .unwrap();
659        }
660
661        // Verify attempts recorded
662        let attempts = store
663            .get_failed_attempts(&tenant_id, user_id)
664            .await
665            .unwrap();
666        assert_eq!(attempts.len(), 3);
667
668        // Record successful attempt
669        protection
670            .record_successful_attempt(&tenant_id, user_id)
671            .await
672            .unwrap();
673
674        // Verify attempts cleared
675        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        // Lock account
692        for _ in 0..5 {
693            protection
694                .record_failed_attempt(&tenant_id, user_id, None, None)
695                .await
696                .unwrap();
697        }
698
699        // Verify locked
700        let result = protection
701            .check_authentication_allowed(&tenant_id, user_id, None)
702            .await
703            .unwrap();
704        assert!(!result.allowed);
705
706        // Manual unlock
707        protection
708            .unlock_account(&tenant_id, user_id)
709            .await
710            .unwrap();
711
712        // Verify unlocked
713        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        // Record failed attempts from same IP, different users
733        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        // Check if next attempt from same IP is blocked
741        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    // ========================================================================
751    // ATTACK SCENARIO TESTS
752    // ========================================================================
753
754    #[tokio::test]
755    async fn test_distributed_attack_from_multiple_ips() {
756        // Simulate a distributed brute force attack targeting a single user
757        // from multiple IP addresses
758        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        // Attack from 10 different IPs, 2 attempts each (total 20 attempts)
766        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        // User account should be locked after max_failed_attempts (5)
789        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        // Individual IPs should still be allowed (no IP-level lockout)
801        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        // Simulate credential stuffing: testing stolen credentials against multiple accounts
815        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        // Attacker tries 15 different accounts from same IP
828        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        // IP should be blocked after 10 attempts
842        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        // Simulate password spraying: testing common password against many accounts
857        // with rate limiting to avoid detection
858        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        // Spray one password attempt across 25 different accounts
871        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        // IP should eventually be blocked
885        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        // Individual accounts should not be locked (only 1 attempt each)
896        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        // Simulate account enumeration: trying to discover valid usernames
911        // by testing many potential usernames
912        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        // Enumerate 60 different potential usernames
924        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        // IP should be blocked after 50 attempts
933        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        // Test that progressive delays make timing attacks harder
947        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        // Record failed attempts and check progressive delays
959        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        // Verify delays are exponential
985        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        // Simulate a slow, distributed attack that stays under rate limits per IP
994        // but still accumulates attempts on the target account
995        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        // Attack from 3 IPs, 4 attempts each (under IP limit but exceeds user limit)
1008        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        // User should be locked (12 total attempts > 8 max)
1020        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        // All IPs should still be allowed individually (4 attempts < 5 max per IP)
1031        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        // Test that repeated lockouts increase duration exponentially
1044        // Note: This tests the concept even though unlock_account clears history
1045        let config = BruteForceConfig {
1046            max_failed_attempts: 3,
1047            exponential_lockout: true,
1048            lockout_duration_seconds: 60,      // 1 minute base
1049            max_lockout_duration_seconds: 480, // 8 minutes max
1050            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        // First lockout - trigger by recording attempts and checking
1060        for _ in 0..3 {
1061            protection
1062                .record_failed_attempt(&tenant_id, user_id, None, None)
1063                .await
1064                .unwrap();
1065        }
1066
1067        // Trigger lockout by checking authentication
1068        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        // Verify first lockout duration is base duration
1080        assert_eq!(duration1, 60, "First lockout should be 60 seconds");
1081
1082        // Test that if lockout persists and user triggers another lockout
1083        // without manual unlock, the duration would escalate
1084        // Simulate this by manually creating a lockout with count=2
1085        let test_lockout = LockoutRecord {
1086            locked_at: Utc::now(),
1087            unlock_at: Utc::now() + Duration::seconds(120), // Would be 2x base
1088            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        // This demonstrates exponential lockout works in the code,
1103        // even if manual unlock resets the counter (which is correct behavior)
1104    }
1105
1106    #[tokio::test]
1107    async fn test_mixed_attack_vectors() {
1108        // Simulate a sophisticated attack combining multiple techniques
1109        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        // Credential stuffing from IP1
1122        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        // Password spraying from IP2
1135        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        // Targeted attack on specific account from IP3
1148        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        // Check all attack vectors are mitigated
1156        // IP1 should be allowed (8 < 15)
1157        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        // IP2 should be allowed (12 < 15)
1164        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        // CEO account should be locked (5 = 5)
1171        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}