Skip to main content

ntex_basicauth/
auth.rs

1//! Core implementation of basic authentication
2
3use crate::{
4    error::{AuthError, AuthResult},
5    is_valid_username,
6    limiter::{ConcurrencyLimiter, RateLimiter},
7};
8use base64::{Engine, engine::general_purpose::STANDARD};
9use ntex::time::timeout;
10use ntex::{Middleware, Service, ServiceCtx, web};
11use std::collections::HashMap;
12use std::fmt::Debug;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::{Duration, Instant};
18
19#[cfg(feature = "timing-safe")]
20use subtle::ConstantTimeEq;
21
22#[cfg(feature = "secure-memory")]
23use zeroize::{Zeroize, ZeroizeOnDrop};
24
25#[cfg(feature = "cache")]
26use {
27    crate::cache::{AuthCache, CacheConfig},
28    sha2::{Digest, Sha256},
29};
30
31/// User credentials
32#[derive(Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "secure-memory", derive(Zeroize, ZeroizeOnDrop))]
34pub struct Credentials {
35    /// Username
36    #[cfg_attr(feature = "secure-memory", zeroize(skip))]
37    pub username: String,
38    /// Password
39    pub password: String,
40}
41
42impl Credentials {
43    /// Create new credentials instance
44    pub fn new(username: String, password: String) -> Self {
45        Self { username, password }
46    }
47
48    /// Generate secure cache key (using SHA256 hash with application-specific salt)
49    #[cfg(feature = "cache")]
50    pub fn cache_key(&self) -> [u8; 32] {
51        let mut hasher = Sha256::new();
52        // Add application-specific salt to prevent rainbow table attacks
53        hasher.update(b"ntex-basicauth-v1:");
54        hasher.update(self.username.as_bytes());
55        hasher.update(b":");
56        hasher.update(self.password.as_bytes());
57        hasher.finalize().into()
58    }
59
60    /// Timing-safe password verification.
61    ///
62    /// Both passwords are reduced to fixed-length SHA-256 digests before the
63    /// constant-time comparison, so the comparison always runs over equal-length
64    /// buffers and does not leak whether the plaintext lengths match (a raw
65    /// `ct_eq` skips comparison on length mismatch, leaking that information).
66    #[cfg(feature = "timing-safe")]
67    pub fn verify_password(&self, expected: &str) -> bool {
68        use sha2::{Digest, Sha256};
69
70        let mut ha = Sha256::new();
71        ha.update(self.password.as_bytes());
72        let mut hb = Sha256::new();
73        hb.update(expected.as_bytes());
74
75        let a = ha.finalize();
76        let b = hb.finalize();
77        a.as_slice().ct_eq(b.as_slice()).into()
78    }
79
80    /// Non-timing-safe password verification (fallback if timing-safe feature is off)
81    #[cfg(not(feature = "timing-safe"))]
82    pub fn verify_password(&self, expected: &str) -> bool {
83        self.password == expected
84    }
85
86    /// Get username reference (avoid clone)
87    pub fn username_ref(&self) -> &str {
88        &self.username
89    }
90
91    /// Validate credentials format
92    pub fn is_valid_format(&self) -> bool {
93        is_valid_username(&self.username) && !self.password.chars().any(|c| c.is_control())
94    }
95}
96
97/// User validator trait for custom authentication logic
98pub trait UserValidator: Send + Sync + Debug {
99    /// Validate user credentials
100    fn validate<'a>(
101        &'a self,
102        credentials: &'a Credentials,
103    ) -> Pin<Box<dyn Future<Output = AuthResult<bool>> + Send + 'a>>;
104
105    /// Get validator name (for logging)
106    fn name(&self) -> &'static str {
107        "UserValidator"
108    }
109
110    /// Pre-validation check (optional)
111    fn pre_validate(&self, credentials: &Credentials) -> AuthResult<()> {
112        if !credentials.is_valid_format() {
113            return Err(AuthError::InvalidCredentials);
114        }
115        Ok(())
116    }
117
118    /// Get user count
119    fn user_count(&self) -> usize {
120        0
121    }
122}
123
124/// Static user list validator
125#[derive(Debug)]
126pub struct StaticUserValidator {
127    users: HashMap<String, String>,
128    case_sensitive: bool,
129}
130
131impl StaticUserValidator {
132    /// Create a new static user validator
133    pub fn new() -> Self {
134        Self {
135            users: HashMap::new(),
136            case_sensitive: true,
137        }
138    }
139
140    /// Set to case insensitive
141    pub fn case_insensitive(mut self) -> Self {
142        self.case_sensitive = false;
143        self
144    }
145
146    /// Add a user
147    pub fn add_user(&mut self, username: String, password: String) -> &mut Self {
148        let key = if self.case_sensitive {
149            username
150        } else {
151            username.to_lowercase()
152        };
153        self.users.insert(key, password);
154        self
155    }
156
157    /// Create validator from HashMap
158    pub fn from_map(users: HashMap<String, String>) -> Self {
159        Self {
160            users,
161            case_sensitive: true,
162        }
163    }
164
165    /// Create validator from HashMap (case insensitive)
166    pub fn from_map_case_insensitive(users: HashMap<String, String>) -> Self {
167        let normalized_users: HashMap<String, String> = users
168            .into_iter()
169            .map(|(k, v)| (k.to_lowercase(), v))
170            .collect();
171
172        Self {
173            users: normalized_users,
174            case_sensitive: false,
175        }
176    }
177
178    /// Check if user exists
179    pub fn contains_user(&self, username: &str) -> bool {
180        let key = if self.case_sensitive {
181            username
182        } else {
183            &username.to_lowercase()
184        };
185        self.users.contains_key(key)
186    }
187}
188
189impl Default for StaticUserValidator {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl UserValidator for StaticUserValidator {
196    fn validate<'a>(
197        &'a self,
198        credentials: &'a Credentials,
199    ) -> Pin<Box<dyn Future<Output = AuthResult<bool>> + Send + 'a>> {
200        Box::pin(async move {
201            let username = if self.case_sensitive {
202                &credentials.username
203            } else {
204                &credentials.username.to_lowercase()
205            };
206
207            match self.users.get(username) {
208                Some(stored_password) => Ok(credentials.verify_password(stored_password)),
209                None => Ok(false),
210            }
211        })
212    }
213
214    fn name(&self) -> &'static str {
215        "StaticUserValidator"
216    }
217
218    /// Get user count
219    fn user_count(&self) -> usize {
220        self.users.len()
221    }
222}
223
224/// BCrypt password validator (requires bcrypt feature)
225#[cfg(feature = "bcrypt")]
226#[derive(Debug)]
227pub struct BcryptUserValidator {
228    users: HashMap<String, String>, // username -> bcrypt hash
229    cost: u32,
230}
231
232#[cfg(feature = "bcrypt")]
233impl BcryptUserValidator {
234    /// Create a new BCrypt user validator
235    pub fn new() -> Self {
236        Self {
237            users: HashMap::new(),
238            cost: bcrypt::DEFAULT_COST,
239        }
240    }
241
242    /// Set BCrypt cost factor
243    pub fn with_cost(mut self, cost: u32) -> Self {
244        self.cost = cost;
245        self
246    }
247
248    /// Add a user with precomputed BCrypt hash
249    pub fn add_user(&mut self, username: String, bcrypt_hash: String) -> &mut Self {
250        self.users.insert(username, bcrypt_hash);
251        self
252    }
253
254    /// Add a user with password, automatically hashing it with BCrypt
255    pub fn add_user_with_password(
256        &mut self,
257        username: String,
258        password: &str,
259    ) -> AuthResult<&mut Self> {
260        let hash = bcrypt::hash(password, self.cost)
261            .map_err(|e| AuthError::ValidationFailed(format!("BCrypt hash failed: {}", e)))?;
262        self.users.insert(username, hash);
263        Ok(self)
264    }
265
266    /// Create validator from HashMap of usernames and BCrypt hashes
267    pub fn from_hashes(users: HashMap<String, String>) -> Self {
268        Self {
269            users,
270            cost: bcrypt::DEFAULT_COST,
271        }
272    }
273}
274
275#[cfg(feature = "bcrypt")]
276impl Default for BcryptUserValidator {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(feature = "bcrypt")]
283impl UserValidator for BcryptUserValidator {
284    fn validate<'a>(
285        &'a self,
286        credentials: &'a Credentials,
287    ) -> Pin<Box<dyn Future<Output = AuthResult<bool>> + Send + 'a>> {
288        Box::pin(async move {
289            match self.users.get(&credentials.username) {
290                Some(stored_hash) => {
291                    // Bcrypt verification is blocking, so we use spawn_blocking
292                    let password = credentials.password.clone();
293                    let hash = stored_hash.clone();
294
295                    let result =
296                        ntex::rt::spawn_blocking(move || bcrypt::verify(&password, &hash)).await;
297
298                    match result {
299                        Ok(Ok(is_valid)) => Ok(is_valid),
300                        Ok(Err(e)) => Err(AuthError::ValidationFailed(format!(
301                            "BCrypt verify failed: {}",
302                            e
303                        ))),
304                        Err(e) => Err(AuthError::InternalError(format!("Task join failed: {}", e))),
305                    }
306                }
307                None => Ok(false),
308            }
309        })
310    }
311
312    fn name(&self) -> &'static str {
313        "BcryptUserValidator"
314    }
315
316    fn user_count(&self) -> usize {
317        self.users.len()
318    }
319}
320
321/// Authentication metrics for monitoring
322#[derive(Debug, Default)]
323pub struct AuthMetrics {
324    /// Total authentication requests
325    pub total_requests: AtomicU64,
326    /// Successful authentications
327    pub successful_auths: AtomicU64,
328    /// Failed authentications
329    pub failed_auths: AtomicU64,
330    /// Cached authentication hits
331    pub cached_hits: AtomicU64,
332    /// Total validation time in milliseconds
333    pub validation_time_ms: AtomicU64,
334}
335
336impl AuthMetrics {
337    /// Create new metrics instance
338    pub fn new() -> Self {
339        Self::default()
340    }
341
342    /// Get total requests
343    pub fn total_requests(&self) -> u64 {
344        self.total_requests.load(Ordering::Relaxed)
345    }
346
347    /// Get successful authentications
348    pub fn successful_auths(&self) -> u64 {
349        self.successful_auths.load(Ordering::Relaxed)
350    }
351
352    /// Get failed authentications
353    pub fn failed_auths(&self) -> u64 {
354        self.failed_auths.load(Ordering::Relaxed)
355    }
356
357    /// Get cache hits
358    pub fn cached_hits(&self) -> u64 {
359        self.cached_hits.load(Ordering::Relaxed)
360    }
361
362    /// Get average validation time in milliseconds
363    pub fn avg_validation_time_ms(&self) -> f64 {
364        let total_time = self.validation_time_ms.load(Ordering::Relaxed);
365        let total_requests = self.total_requests.load(Ordering::Relaxed);
366        if total_requests > 0 {
367            total_time as f64 / total_requests as f64
368        } else {
369            0.0
370        }
371    }
372
373    /// Get success rate as percentage
374    pub fn success_rate(&self) -> f64 {
375        let successful = self.successful_auths.load(Ordering::Relaxed);
376        let total = self.total_requests.load(Ordering::Relaxed);
377        if total > 0 {
378            (successful as f64 / total as f64) * 100.0
379        } else {
380            0.0
381        }
382    }
383
384    /// Get cache hit rate as percentage
385    pub fn cache_hit_rate(&self) -> f64 {
386        let hits = self.cached_hits.load(Ordering::Relaxed);
387        let total = self.total_requests.load(Ordering::Relaxed);
388        if total > 0 {
389            (hits as f64 / total as f64) * 100.0
390        } else {
391            0.0
392        }
393    }
394
395    /// Reset all metrics
396    pub fn reset(&self) {
397        self.total_requests.store(0, Ordering::Relaxed);
398        self.successful_auths.store(0, Ordering::Relaxed);
399        self.failed_auths.store(0, Ordering::Relaxed);
400        self.cached_hits.store(0, Ordering::Relaxed);
401        self.validation_time_ms.store(0, Ordering::Relaxed);
402    }
403
404    /// Increment total authentication request counter
405    pub fn incr_total_requests(&self) {
406        self.total_requests.fetch_add(1, Ordering::Relaxed);
407    }
408
409    /// Increment successful authentication counter
410    pub fn incr_successful_auths(&self) {
411        self.successful_auths.fetch_add(1, Ordering::Relaxed);
412    }
413
414    /// Increment failed authentication counter
415    pub fn incr_failed_auths(&self) {
416        self.failed_auths.fetch_add(1, Ordering::Relaxed);
417    }
418
419    /// Increment cached authentication hit counter
420    pub fn incr_cached_hits(&self) {
421        self.cached_hits.fetch_add(1, Ordering::Relaxed);
422    }
423
424    /// Add validation time in milliseconds
425    pub fn add_validation_time(&self, duration: Duration) {
426        let ms = duration.as_millis() as u64;
427        self.validation_time_ms.fetch_add(ms, Ordering::Relaxed);
428    }
429}
430
431/// Custom error handler type for authentication failures
432pub type CustomErrorHandler = Arc<dyn Fn(&AuthError, &str) -> web::HttpResponse + Send + Sync>;
433
434/// Basic authentication config
435pub struct BasicAuthConfig {
436    /// Authentication realm (for WWW-Authenticate header)
437    pub realm: String,
438    /// User validator
439    pub validator: Arc<dyn UserValidator>,
440    #[cfg(feature = "cache")]
441    /// Auth result cache (optional)
442    pub cache: Option<Arc<AuthCache>>,
443    /// Path filter (optional)
444    pub path_filter: Option<Arc<crate::utils::PathFilter>>,
445    /// Request header size limit (bytes)
446    pub max_header_size: usize,
447    /// Log details on authentication failure
448    pub log_failures: bool,
449    /// Custom error handler
450    pub custom_error_handler: Option<CustomErrorHandler>,
451    /// Maximum concurrent authentication validations
452    pub max_concurrent_validations: Option<usize>,
453    /// Validation timeout
454    pub validation_timeout: Option<Duration>,
455    /// Rate limiting: (max_requests, time_window)
456    pub rate_limit_per_ip: Option<(usize, Duration)>,
457    /// Header to read the client IP from for rate limiting (e.g.
458    /// `"x-forwarded-for"`). When set, the first address in the header is used
459    /// instead of the transport peer address. Only enable behind a trusted
460    /// proxy, since clients can spoof this header.
461    pub client_ip_header: Option<String>,
462    /// Enable metrics collection
463    pub enable_metrics: bool,
464    /// Log usernames in production (security risk)
465    pub log_usernames_in_production: bool,
466}
467
468impl BasicAuthConfig {
469    /// Create new basic auth config
470    pub fn new(validator: Arc<dyn UserValidator>) -> Self {
471        Self {
472            realm: "Restricted Area".to_string(),
473            validator,
474            #[cfg(feature = "cache")]
475            cache: None,
476            path_filter: None,
477            max_header_size: 8192, // 8KB
478            log_failures: false,
479            custom_error_handler: None,
480            max_concurrent_validations: None,
481            validation_timeout: Some(Duration::from_secs(30)),
482            rate_limit_per_ip: None,
483            client_ip_header: None,
484            enable_metrics: true,
485            log_usernames_in_production: false,
486        }
487    }
488
489    /// Set authentication realm
490    pub fn realm(mut self, realm: String) -> Self {
491        self.realm = realm;
492        self
493    }
494
495    #[cfg(feature = "cache")]
496    /// Create auth config with cache config
497    pub fn with_cache(mut self, cache_config: CacheConfig) -> AuthResult<Self> {
498        self.cache = Some(Arc::new(AuthCache::new(cache_config)?));
499        Ok(self)
500    }
501
502    #[cfg(feature = "cache")]
503    /// Disable cache
504    pub fn disable_cache(mut self) -> Self {
505        self.cache = None;
506        self
507    }
508
509    /// Set path filter
510    pub fn path_filter(mut self, filter: crate::utils::PathFilter) -> Self {
511        self.path_filter = Some(Arc::new(filter));
512        self
513    }
514
515    /// Set max request header size
516    pub fn max_header_size(mut self, size: usize) -> Self {
517        self.max_header_size = size;
518        self
519    }
520
521    /// Enable or disable logging on authentication failure
522    pub fn log_failures(mut self, enabled: bool) -> Self {
523        self.log_failures = enabled;
524        self
525    }
526
527    /// Set custom error handler function
528    pub fn custom_error_handler<F>(mut self, handler: F) -> Self
529    where
530        F: Fn(&AuthError, &str) -> web::HttpResponse + Send + Sync + 'static,
531    {
532        self.custom_error_handler = Some(Arc::new(handler));
533        self
534    }
535
536    /// Set maximum concurrent validations
537    pub fn max_concurrent_validations(mut self, max: usize) -> Self {
538        self.max_concurrent_validations = Some(max);
539        self
540    }
541
542    /// Set validation timeout
543    pub fn validation_timeout(mut self, timeout: Duration) -> Self {
544        self.validation_timeout = Some(timeout);
545        self
546    }
547
548    /// Set rate limiting per IP
549    pub fn rate_limit_per_ip(mut self, max_requests: usize, window: Duration) -> Self {
550        self.rate_limit_per_ip = Some((max_requests, window));
551        self
552    }
553
554    /// Set the header to read the client IP from for rate limiting (e.g.
555    /// `"x-forwarded-for"`). Only enable this behind a trusted proxy, since
556    /// clients can otherwise spoof the header to evade or forge rate limits.
557    pub fn client_ip_header(mut self, header: impl Into<String>) -> Self {
558        self.client_ip_header = Some(header.into());
559        self
560    }
561
562    /// Enable or disable metrics collection
563    pub fn enable_metrics(mut self, enabled: bool) -> Self {
564        self.enable_metrics = enabled;
565        self
566    }
567
568    /// Enable or disable logging usernames in production (security risk)
569    pub fn log_usernames_in_production(mut self, enabled: bool) -> Self {
570        self.log_usernames_in_production = enabled;
571        self
572    }
573
574    /// Enhanced config validation
575    pub fn validate(&self) -> AuthResult<()> {
576        if self.realm.is_empty() {
577            return Err(AuthError::ConfigError("realm cannot be empty".to_string()));
578        }
579        if self.max_header_size == 0 {
580            return Err(AuthError::ConfigError(
581                "max_header_size must be greater than 0".to_string(),
582            ));
583        }
584        if self.max_header_size > 1024 * 1024 {
585            // 1MB limit
586            return Err(AuthError::ConfigError(
587                "max_header_size too large (max 1MB)".to_string(),
588            ));
589        }
590
591        if let Some(max_concurrent) = self.max_concurrent_validations {
592            if max_concurrent == 0 {
593                return Err(AuthError::ConfigError(
594                    "max_concurrent_validations must be greater than 0".to_string(),
595                ));
596            }
597            if max_concurrent > 10000 {
598                return Err(AuthError::ConfigError(
599                    "max_concurrent_validations too large (max 10000)".to_string(),
600                ));
601            }
602        }
603
604        if let Some(timeout) = self.validation_timeout {
605            if timeout.is_zero() {
606                return Err(AuthError::ConfigError(
607                    "validation_timeout must be greater than 0".to_string(),
608                ));
609            }
610            if timeout > Duration::from_secs(300) {
611                // 5 minutes
612                return Err(AuthError::ConfigError(
613                    "validation_timeout too large (max 5 minutes)".to_string(),
614                ));
615            }
616        }
617
618        if let Some((max_requests, window)) = self.rate_limit_per_ip {
619            if max_requests == 0 {
620                return Err(AuthError::ConfigError(
621                    "rate_limit max_requests must be greater than 0".to_string(),
622                ));
623            }
624            if window.is_zero() {
625                return Err(AuthError::ConfigError(
626                    "rate_limit window must be greater than 0".to_string(),
627                ));
628            }
629        }
630
631        #[cfg(feature = "cache")]
632        if let Some(cache) = &self.cache {
633            let stats = cache.stats();
634            if stats.total_entries > 100000 {
635                // Reasonable cache size limit
636                eprintln!(
637                    "Warning: Cache has {} entries, consider reducing TTL",
638                    stats.total_entries
639                );
640            }
641        }
642
643        Ok(())
644    }
645}
646
647/// Basic authentication middleware
648pub struct BasicAuth {
649    pub(crate) config: BasicAuthConfig,
650    pub(crate) metrics: Arc<AuthMetrics>,
651    pub(crate) concurrency_limiter: Option<Arc<ConcurrencyLimiter>>,
652    pub(crate) rate_limiter: Option<Arc<RateLimiter>>,
653}
654
655impl BasicAuth {
656    /// Create new BasicAuth instance
657    pub fn new(config: BasicAuthConfig) -> AuthResult<Self> {
658        config.validate()?;
659        let concurrency_limiter = config
660            .max_concurrent_validations
661            .map(|max| Arc::new(ConcurrencyLimiter::new(max)));
662        let rate_limiter = config
663            .rate_limit_per_ip
664            .map(|(max_requests, window)| Arc::new(RateLimiter::new(max_requests, window)));
665        Ok(Self {
666            config,
667            metrics: Arc::new(AuthMetrics::new()),
668            concurrency_limiter,
669            rate_limiter,
670        })
671    }
672
673    /// Get metrics reference
674    pub fn metrics(&self) -> &AuthMetrics {
675        &self.metrics
676    }
677
678    /// Create BasicAuth with static user list
679    pub fn with_users(users: HashMap<String, String>) -> AuthResult<Self> {
680        let validator = Arc::new(StaticUserValidator::from_map(users));
681        let config = BasicAuthConfig::new(validator);
682        Self::new(config)
683    }
684
685    /// Create BasicAuth with a single user
686    pub fn with_user(username: String, password: String) -> AuthResult<Self> {
687        let mut users = HashMap::new();
688        users.insert(username, password);
689        Self::with_users(users)
690    }
691
692    /// Parse Authorization header and extract credentials
693    /// Supports colons in password
694    fn parse_credentials(auth_header: &str, max_size: usize) -> AuthResult<Credentials> {
695        if auth_header.len() > max_size {
696            return Err(AuthError::InvalidFormat);
697        }
698
699        // Safe slicing: `get(..6)` returns None when the cut is not on a UTF-8
700        // boundary, which prevents a panic on malformed non-ASCII headers.
701        let scheme = auth_header.get(..6).ok_or(AuthError::InvalidFormat)?;
702        if !scheme.eq_ignore_ascii_case("Basic ") {
703            return Err(AuthError::InvalidFormat);
704        }
705
706        let encoded = &auth_header[6..]; // Remove "Basic " prefix
707
708        // Check Base64 string length
709        if encoded.len() > (max_size * 3 / 4) {
710            return Err(AuthError::InvalidFormat);
711        }
712
713        let decoded = STANDARD
714            .decode(encoded)
715            .map_err(|_| AuthError::InvalidBase64)?;
716
717        // Validate UTF-8 without an intermediate String allocation
718        let decoded_str = std::str::from_utf8(&decoded).map_err(|_| AuthError::InvalidBase64)?;
719
720        // Split only at the first colon, support colons in password
721        let (username, password) = decoded_str
722            .split_once(':')
723            .ok_or(AuthError::InvalidFormat)?;
724
725        let credentials = Credentials::new(username.to_string(), password.to_string());
726
727        // Validate credentials format
728        if !credentials.is_valid_format() {
729            return Err(AuthError::InvalidCredentials);
730        }
731
732        Ok(credentials)
733    }
734
735    /// Authenticate user credentials
736    async fn authenticate(&self, credentials: &Credentials) -> AuthResult<bool> {
737        // Pre-validation check
738        self.config.validator.pre_validate(credentials)?;
739
740        // Check cache and cache result (compute key only once)
741        #[cfg(feature = "cache")]
742        {
743            if let Some(cache) = &self.config.cache {
744                let cache_key = credentials.cache_key();
745                if let Some(cached_result) = cache.get(&cache_key) {
746                    if self.config.enable_metrics {
747                        self.metrics.incr_cached_hits();
748                    }
749                    return Ok(cached_result);
750                }
751
752                let start = Instant::now();
753                let result = self.run_validation(credentials).await?;
754
755                if self.config.enable_metrics {
756                    self.metrics.add_validation_time(start.elapsed());
757                }
758
759                // Cache result using the same key
760                if let Err(e) = cache.insert(cache_key, result) {
761                    // Cache failure should not affect authentication result, just log error
762                    eprintln!("Failed to cache authentication result: {}", e);
763                }
764
765                return Ok(result);
766            }
767        }
768
769        // Validate using configured validator (when cache is disabled)
770        let start = Instant::now();
771        let result = self.run_validation(credentials).await?;
772
773        if self.config.enable_metrics {
774            self.metrics.add_validation_time(start.elapsed());
775        }
776        Ok(result)
777    }
778
779    /// Run the configured validator, applying the optional concurrency limit
780    /// and validation timeout.
781    ///
782    /// When a concurrency limiter is configured, validation runs inside a
783    /// spawned task so the permit is held until the validator actually finishes
784    /// — even if *this* call times out. That keeps `max_concurrent_validations`
785    /// effective for validators that cannot be cancelled (e.g. bcrypt on a
786    /// blocking thread): a timed-out request no longer releases its permit early
787    /// and lets work pile up past the limit. Permit acquisition is also covered
788    /// by the timeout, so a saturated limiter cannot make a request wait longer
789    /// than `validation_timeout`.
790    async fn run_validation(&self, credentials: &Credentials) -> AuthResult<bool> {
791        // Without a concurrency limiter there is no permit to protect, so we can
792        // validate directly (optionally under a timeout).
793        let Some(limiter) = &self.concurrency_limiter else {
794            let validate = self.config.validator.validate(credentials);
795            return match self.config.validation_timeout {
796                Some(timeout_dur) => timeout(timeout_dur, validate)
797                    .await
798                    .map_err(|_| AuthError::InternalError("Validation timed out".to_string()))?,
799                None => validate.await,
800            };
801        };
802
803        let validator = Arc::clone(&self.config.validator);
804        let limiter = Arc::clone(limiter);
805        let credentials = credentials.clone();
806
807        let task = ntex::rt::spawn(async move {
808            // Held until validation completes, even if the caller times out below.
809            let _permit = limiter.acquire().await;
810            validator.validate(&credentials).await
811        });
812
813        match self.config.validation_timeout {
814            Some(timeout_dur) => match timeout(timeout_dur, task).await {
815                Ok(join_result) => join_result
816                    .map_err(|_| AuthError::InternalError("Validation task failed".to_string()))?,
817                Err(_) => Err(AuthError::InternalError("Validation timed out".to_string())),
818            },
819            None => task
820                .await
821                .map_err(|_| AuthError::InternalError("Validation task failed".to_string()))?,
822        }
823    }
824
825    /// Resolve the client IP used for rate limiting.
826    ///
827    /// When `client_ip_header` is configured (trusted-proxy deployments), the
828    /// first address in that header wins; otherwise the transport peer address
829    /// is used. Only enable the header when running behind a trusted proxy,
830    /// since clients can otherwise spoof it to evade or forge rate limits.
831    fn client_ip<Err>(&self, req: &web::WebRequest<Err>) -> String
832    where
833        Err: web::ErrorRenderer,
834    {
835        if let Some(header) = &self.config.client_ip_header
836            && let Some(value) = req.headers().get(header).and_then(|v| v.to_str().ok())
837            && let Some(first) = value.split(',').next()
838        {
839            let ip = first.trim();
840            if !ip.is_empty() {
841                return ip.to_string();
842            }
843        }
844        req.peer_addr()
845            .map(|addr| addr.ip().to_string())
846            .unwrap_or_default()
847    }
848
849    /// Handle authentication error
850    fn handle_auth_error(&self, error: &AuthError) -> web::HttpResponse {
851        if let Some(handler) = &self.config.custom_error_handler {
852            handler(error, &self.config.realm)
853        } else {
854            error.to_response(&self.config.realm)
855        }
856    }
857
858    /// Log authentication failure (if enabled) with enhanced security
859    fn log_auth_failure(&self, error: &AuthError, username: Option<&str>) {
860        if self.config.log_failures {
861            // In production, avoid logging usernames unless explicitly configured
862            let safe_username = if self.config.log_usernames_in_production || cfg!(debug_assertions)
863            {
864                username
865            } else {
866                None // Don't log usernames in production for security
867            };
868
869            match safe_username {
870                Some(user) => eprintln!("Authentication failed - user: {}, error: {}", user, error),
871                None => eprintln!("Authentication failed - error: {}", error),
872            }
873        }
874    }
875}
876
877impl<S, Cfg> Middleware<S, Cfg> for BasicAuth {
878    type Service = BasicAuthMiddlewareService<S>;
879
880    fn create(&self, service: S, _cfg: Cfg) -> Self::Service {
881        BasicAuthMiddlewareService {
882            service,
883            auth: BasicAuth {
884                config: BasicAuthConfig {
885                    realm: self.config.realm.clone(),
886                    validator: Arc::clone(&self.config.validator),
887                    #[cfg(feature = "cache")]
888                    cache: self.config.cache.clone(),
889                    path_filter: self.config.path_filter.clone(),
890                    max_header_size: self.config.max_header_size,
891                    log_failures: self.config.log_failures,
892                    custom_error_handler: self.config.custom_error_handler.clone(),
893                    max_concurrent_validations: self.config.max_concurrent_validations,
894                    validation_timeout: self.config.validation_timeout,
895                    rate_limit_per_ip: self.config.rate_limit_per_ip,
896                    client_ip_header: self.config.client_ip_header.clone(),
897                    enable_metrics: self.config.enable_metrics,
898                    log_usernames_in_production: self.config.log_usernames_in_production,
899                },
900                metrics: Arc::clone(&self.metrics),
901                concurrency_limiter: self.concurrency_limiter.clone(),
902                rate_limiter: self.rate_limiter.clone(),
903            },
904        }
905    }
906}
907
908pub struct BasicAuthMiddlewareService<S> {
909    service: S,
910    auth: BasicAuth,
911}
912
913impl<S, Err> Service<web::WebRequest<Err>> for BasicAuthMiddlewareService<S>
914where
915    S: Service<web::WebRequest<Err>, Response = web::WebResponse, Error = web::Error> + 'static,
916    Err: web::ErrorRenderer,
917{
918    type Response = web::WebResponse;
919    type Error = web::Error;
920
921    async fn call(
922        &self,
923        req: web::WebRequest<Err>,
924        ctx: ServiceCtx<'_, Self>,
925    ) -> Result<Self::Response, Self::Error> {
926        let metrics_enabled = self.auth.config.enable_metrics;
927
928        // Check if path filter is configured and should skip authentication
929        if let Some(filter) = &self.auth.config.path_filter
930            && filter.should_skip(req.path())
931        {
932            return ctx.call(&self.service, req).await;
933        }
934
935        if metrics_enabled {
936            self.auth.metrics.incr_total_requests();
937        }
938
939        // Per-IP rate limiting (checked before credential parsing)
940        if let Some(rate_limiter) = &self.auth.rate_limiter {
941            let ip = self.auth.client_ip(&req);
942            if let Err(err) = rate_limiter.check(&ip) {
943                self.auth.log_auth_failure(&err, None);
944                let response = self.auth.handle_auth_error(&err);
945                if metrics_enabled {
946                    self.auth.metrics.incr_failed_auths();
947                }
948                return Ok(req.into_response(response));
949            }
950        }
951
952        // Extract authorization header
953        let auth_header = req
954            .headers()
955            .get("authorization")
956            .and_then(|h| h.to_str().ok());
957
958        // Handle missing or malformed Authorization header
959        let auth_header = match auth_header {
960            Some(header) => header,
961            None => {
962                let error = AuthError::MissingHeader;
963                self.auth.log_auth_failure(&error, None);
964                let response = self.auth.handle_auth_error(&error);
965                if metrics_enabled {
966                    self.auth.metrics.incr_failed_auths();
967                }
968                return Ok(req.into_response(response));
969            }
970        };
971
972        // Parse credentials from Authorization header
973        let credentials =
974            match BasicAuth::parse_credentials(auth_header, self.auth.config.max_header_size) {
975                Ok(creds) => creds,
976                Err(err) => {
977                    self.auth.log_auth_failure(&err, None);
978                    let response = self.auth.handle_auth_error(&err);
979                    if metrics_enabled {
980                        self.auth.metrics.incr_failed_auths();
981                    }
982                    return Ok(req.into_response(response));
983                }
984            };
985
986        // Authenticate user credentials
987        let is_authenticated = match self.auth.authenticate(&credentials).await {
988            Ok(result) => result,
989            Err(err) => {
990                self.auth
991                    .log_auth_failure(&err, Some(&credentials.username));
992                let response = self.auth.handle_auth_error(&err);
993                if metrics_enabled {
994                    self.auth.metrics.incr_failed_auths();
995                }
996                return Ok(req.into_response(response));
997            }
998        };
999
1000        if !is_authenticated {
1001            let error = AuthError::InvalidCredentials;
1002            self.auth
1003                .log_auth_failure(&error, Some(&credentials.username));
1004            let response = self.auth.handle_auth_error(&error);
1005            if metrics_enabled {
1006                self.auth.metrics.incr_failed_auths();
1007            }
1008            return Ok(req.into_response(response));
1009        }
1010
1011        if metrics_enabled {
1012            self.auth.metrics.incr_successful_auths();
1013        }
1014
1015        // Add credentials to request extensions for further processing
1016        req.extensions_mut().insert(credentials);
1017        ctx.call(&self.service, req).await
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024    use std::collections::HashMap;
1025
1026    #[tokio::test]
1027    async fn test_static_validator() {
1028        let mut users = HashMap::new();
1029        users.insert("admin".to_string(), "secret".to_string());
1030        users.insert("user".to_string(), "password:with:colons".to_string());
1031
1032        let validator = StaticUserValidator::from_map(users);
1033
1034        let valid_creds = Credentials::new("admin".to_string(), "secret".to_string());
1035        let colon_password_creds =
1036            Credentials::new("user".to_string(), "password:with:colons".to_string());
1037        let invalid_creds = Credentials::new("admin".to_string(), "wrong".to_string());
1038
1039        assert!(validator.validate(&valid_creds).await.unwrap());
1040        assert!(validator.validate(&colon_password_creds).await.unwrap());
1041        assert!(!validator.validate(&invalid_creds).await.unwrap());
1042    }
1043
1044    #[test]
1045    fn test_parse_credentials_with_colons() {
1046        use base64::Engine;
1047        let credentials = "admin:pass:word:with:colons";
1048        let encoded = STANDARD.encode(credentials.as_bytes());
1049        let auth_header = format!("Basic {}", encoded);
1050
1051        let creds = BasicAuth::parse_credentials(&auth_header, 8192).unwrap();
1052        assert_eq!(creds.username, "admin");
1053        assert_eq!(creds.password, "pass:word:with:colons");
1054    }
1055
1056    #[test]
1057    fn test_parse_credentials_multibyte_no_panic() {
1058        // "abc" + a 4-byte emoji: byte index 6 falls inside the emoji, which
1059        // previously caused `&auth_header[..6]` to panic. Must be rejected
1060        // gracefully instead of crashing the worker (DoS).
1061        let malicious = "abc😀garbage";
1062        let result = BasicAuth::parse_credentials(malicious, 8192);
1063        assert!(matches!(result, Err(AuthError::InvalidFormat)));
1064    }
1065
1066    #[test]
1067    fn test_credentials_validation() {
1068        let valid_creds = Credentials::new("user".to_string(), "pass".to_string());
1069        let valid_empty_user = Credentials::new("".to_string(), "pass".to_string()); // Now valid per RFC 7617
1070        let invalid_creds1 = Credentials::new("user:name".to_string(), "pass".to_string());
1071        let invalid_creds2 = Credentials::new("user".to_string(), "pass\nword".to_string());
1072        let invalid_creds3 = Credentials::new("user".to_string(), "pass\tword".to_string()); // Tab is control character
1073
1074        assert!(valid_creds.is_valid_format());
1075        assert!(valid_empty_user.is_valid_format());
1076        assert!(!invalid_creds1.is_valid_format());
1077        assert!(!invalid_creds2.is_valid_format());
1078        assert!(!invalid_creds3.is_valid_format());
1079    }
1080
1081    #[cfg(feature = "cache")]
1082    #[test]
1083    fn test_secure_cache_key() {
1084        let creds = Credentials::new("admin".to_string(), "secret".to_string());
1085
1086        let key1 = creds.cache_key();
1087        let key2 = creds.cache_key();
1088
1089        // The same credentials should produce the same cache key
1090        assert_eq!(key1, key2);
1091
1092        // Cache key should be a 32-byte array (does not contain sensitive information)
1093        assert_eq!(key1.len(), 32);
1094    }
1095
1096    #[test]
1097    fn test_case_insensitive_validator() {
1098        let mut users = HashMap::new();
1099        users.insert("admin".to_string(), "secret".to_string());
1100
1101        let validator = StaticUserValidator::from_map_case_insensitive(users);
1102
1103        assert!(validator.contains_user("admin"));
1104        assert!(validator.contains_user("ADMIN"));
1105        assert!(validator.contains_user("Admin"));
1106    }
1107
1108    #[tokio::test]
1109    async fn test_validator_pre_validation() {
1110        let validator = StaticUserValidator::new();
1111        let invalid_creds = Credentials::new("user:name".to_string(), "pass".to_string());
1112
1113        assert!(validator.pre_validate(&invalid_creds).is_err());
1114    }
1115
1116    #[test]
1117    fn test_config_validation() {
1118        let validator = Arc::new(StaticUserValidator::new());
1119
1120        let valid_config = BasicAuthConfig::new(validator.clone());
1121        assert!(valid_config.validate().is_ok());
1122
1123        let invalid_config = BasicAuthConfig::new(validator).realm("".to_string());
1124        assert!(invalid_config.validate().is_err());
1125    }
1126
1127    #[cfg(feature = "bcrypt")]
1128    #[tokio::test]
1129    async fn test_bcrypt_validator() {
1130        let mut validator = BcryptUserValidator::new();
1131        validator
1132            .add_user_with_password("admin".to_string(), "secret")
1133            .unwrap();
1134
1135        let valid_creds = Credentials::new("admin".to_string(), "secret".to_string());
1136        let invalid_creds = Credentials::new("admin".to_string(), "wrong".to_string());
1137
1138        assert!(validator.validate(&valid_creds).await.unwrap());
1139        assert!(!validator.validate(&invalid_creds).await.unwrap());
1140    }
1141
1142    #[ntex::test]
1143    async fn test_rate_limit_per_ip() {
1144        use base64::Engine;
1145        use std::time::Duration;
1146
1147        let auth = crate::BasicAuthBuilder::new()
1148            .user("admin", "secret")
1149            .rate_limit_per_ip(1, Duration::from_secs(60))
1150            .build()
1151            .unwrap();
1152        let app = ntex::web::test::init_service(
1153            ntex::web::App::new()
1154                .middleware(auth)
1155                .route("/", ntex::web::get().to(|| async { "ok" })),
1156        )
1157        .await;
1158
1159        let auth_hdr = format!(
1160            "Basic {}",
1161            base64::engine::general_purpose::STANDARD.encode("admin:secret")
1162        );
1163        let ip: std::net::SocketAddr = "1.2.3.4:80".parse().unwrap();
1164
1165        // First request from this IP succeeds.
1166        let req = ntex::web::test::TestRequest::get()
1167            .peer_addr(ip)
1168            .header("authorization", auth_hdr.as_str())
1169            .to_request();
1170        let resp = ntex::web::test::call_service(&app, req).await;
1171        assert!(resp.status().is_success());
1172
1173        // Second request from the same IP exceeds the limit (1 req / 60s).
1174        let req = ntex::web::test::TestRequest::get()
1175            .peer_addr(ip)
1176            .header("authorization", auth_hdr.as_str())
1177            .to_request();
1178        let resp = ntex::web::test::call_service(&app, req).await;
1179        assert_eq!(resp.status(), ntex::http::StatusCode::TOO_MANY_REQUESTS);
1180    }
1181
1182    #[ntex::test]
1183    async fn test_validation_timeout() {
1184        use base64::Engine;
1185        use std::future::Future;
1186        use std::pin::Pin;
1187        use std::sync::Arc;
1188        use std::time::Duration;
1189
1190        #[derive(Debug)]
1191        struct SlowValidator;
1192
1193        impl UserValidator for SlowValidator {
1194            fn validate<'a>(
1195                &'a self,
1196                _: &'a Credentials,
1197            ) -> Pin<Box<dyn Future<Output = AuthResult<bool>> + Send + 'a>> {
1198                Box::pin(async {
1199                    ntex::time::sleep(Duration::from_secs(5)).await;
1200                    Ok(true)
1201                })
1202            }
1203        }
1204
1205        let validator = Arc::new(SlowValidator);
1206        let config = BasicAuthConfig::new(validator).validation_timeout(Duration::from_millis(100));
1207        let auth = BasicAuth::new(config).unwrap();
1208        let app = ntex::web::test::init_service(
1209            ntex::web::App::new()
1210                .middleware(auth)
1211                .route("/", ntex::web::get().to(|| async { "ok" })),
1212        )
1213        .await;
1214
1215        let auth_hdr = format!(
1216            "Basic {}",
1217            base64::engine::general_purpose::STANDARD.encode("admin:secret")
1218        );
1219        let req = ntex::web::test::TestRequest::get()
1220            .header("authorization", auth_hdr.as_str())
1221            .to_request();
1222        let resp = ntex::web::test::call_service(&app, req).await;
1223        // Validation timed out -> InternalError (500).
1224        assert_eq!(resp.status(), ntex::http::StatusCode::INTERNAL_SERVER_ERROR);
1225    }
1226
1227    #[ntex::test]
1228    async fn test_concurrency_limited_auth_succeeds() {
1229        use base64::Engine;
1230
1231        // Exercises the spawned-task validation path (concurrency limiter set +
1232        // default timeout): a normal request must still authenticate.
1233        let auth = crate::BasicAuthBuilder::new()
1234            .user("admin", "secret")
1235            .max_concurrent_validations(2)
1236            .build()
1237            .unwrap();
1238        let app = ntex::web::test::init_service(
1239            ntex::web::App::new()
1240                .middleware(auth)
1241                .route("/", ntex::web::get().to(|| async { "ok" })),
1242        )
1243        .await;
1244
1245        let auth_hdr = format!(
1246            "Basic {}",
1247            base64::engine::general_purpose::STANDARD.encode("admin:secret")
1248        );
1249        let req = ntex::web::test::TestRequest::get()
1250            .header("authorization", auth_hdr.as_str())
1251            .to_request();
1252        let resp = ntex::web::test::call_service(&app, req).await;
1253        assert!(resp.status().is_success());
1254    }
1255
1256    #[ntex::test]
1257    async fn test_rate_limit_uses_forwarded_header() {
1258        use base64::Engine;
1259        use std::time::Duration;
1260
1261        // With client_ip_header configured, rate limiting keys on the forwarded
1262        // client IP rather than the (shared) proxy peer address.
1263        let auth = crate::BasicAuthBuilder::new()
1264            .user("admin", "secret")
1265            .rate_limit_per_ip(1, Duration::from_secs(60))
1266            .client_ip_header("x-forwarded-for")
1267            .build()
1268            .unwrap();
1269        let app = ntex::web::test::init_service(
1270            ntex::web::App::new()
1271                .middleware(auth)
1272                .route("/", ntex::web::get().to(|| async { "ok" })),
1273        )
1274        .await;
1275        let auth_hdr = format!(
1276            "Basic {}",
1277            base64::engine::general_purpose::STANDARD.encode("admin:secret")
1278        );
1279
1280        // Two requests from the same forwarded IP: second exceeds the 1/60s limit.
1281        for (i, expect_ok) in [(0u8, true), (1u8, false)] {
1282            let req = ntex::web::test::TestRequest::get()
1283                .header("authorization", auth_hdr.as_str())
1284                .header("x-forwarded-for", "9.9.9.9, 10.0.0.1")
1285                .to_request();
1286            let resp = ntex::web::test::call_service(&app, req).await;
1287            assert_eq!(
1288                resp.status().is_success(),
1289                expect_ok,
1290                "request {i} unexpected status {}",
1291                resp.status()
1292            );
1293        }
1294
1295        // A different forwarded IP has its own budget even though the peer
1296        // address (absent here) is identical.
1297        let req = ntex::web::test::TestRequest::get()
1298            .header("authorization", auth_hdr.as_str())
1299            .header("x-forwarded-for", "8.8.8.8")
1300            .to_request();
1301        let resp = ntex::web::test::call_service(&app, req).await;
1302        assert!(resp.status().is_success());
1303    }
1304}