1use 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#[derive(Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "secure-memory", derive(Zeroize, ZeroizeOnDrop))]
34pub struct Credentials {
35 #[cfg_attr(feature = "secure-memory", zeroize(skip))]
37 pub username: String,
38 pub password: String,
40}
41
42impl Credentials {
43 pub fn new(username: String, password: String) -> Self {
45 Self { username, password }
46 }
47
48 #[cfg(feature = "cache")]
50 pub fn cache_key(&self) -> [u8; 32] {
51 let mut hasher = Sha256::new();
52 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 #[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 #[cfg(not(feature = "timing-safe"))]
82 pub fn verify_password(&self, expected: &str) -> bool {
83 self.password == expected
84 }
85
86 pub fn username_ref(&self) -> &str {
88 &self.username
89 }
90
91 pub fn is_valid_format(&self) -> bool {
93 is_valid_username(&self.username) && !self.password.chars().any(|c| c.is_control())
94 }
95}
96
97pub trait UserValidator: Send + Sync + Debug {
99 fn validate<'a>(
101 &'a self,
102 credentials: &'a Credentials,
103 ) -> Pin<Box<dyn Future<Output = AuthResult<bool>> + Send + 'a>>;
104
105 fn name(&self) -> &'static str {
107 "UserValidator"
108 }
109
110 fn pre_validate(&self, credentials: &Credentials) -> AuthResult<()> {
112 if !credentials.is_valid_format() {
113 return Err(AuthError::InvalidCredentials);
114 }
115 Ok(())
116 }
117
118 fn user_count(&self) -> usize {
120 0
121 }
122}
123
124#[derive(Debug)]
126pub struct StaticUserValidator {
127 users: HashMap<String, String>,
128 case_sensitive: bool,
129}
130
131impl StaticUserValidator {
132 pub fn new() -> Self {
134 Self {
135 users: HashMap::new(),
136 case_sensitive: true,
137 }
138 }
139
140 pub fn case_insensitive(mut self) -> Self {
142 self.case_sensitive = false;
143 self
144 }
145
146 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 pub fn from_map(users: HashMap<String, String>) -> Self {
159 Self {
160 users,
161 case_sensitive: true,
162 }
163 }
164
165 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 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 fn user_count(&self) -> usize {
220 self.users.len()
221 }
222}
223
224#[cfg(feature = "bcrypt")]
226#[derive(Debug)]
227pub struct BcryptUserValidator {
228 users: HashMap<String, String>, cost: u32,
230}
231
232#[cfg(feature = "bcrypt")]
233impl BcryptUserValidator {
234 pub fn new() -> Self {
236 Self {
237 users: HashMap::new(),
238 cost: bcrypt::DEFAULT_COST,
239 }
240 }
241
242 pub fn with_cost(mut self, cost: u32) -> Self {
244 self.cost = cost;
245 self
246 }
247
248 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 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 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 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#[derive(Debug, Default)]
323pub struct AuthMetrics {
324 pub total_requests: AtomicU64,
326 pub successful_auths: AtomicU64,
328 pub failed_auths: AtomicU64,
330 pub cached_hits: AtomicU64,
332 pub validation_time_ms: AtomicU64,
334}
335
336impl AuthMetrics {
337 pub fn new() -> Self {
339 Self::default()
340 }
341
342 pub fn total_requests(&self) -> u64 {
344 self.total_requests.load(Ordering::Relaxed)
345 }
346
347 pub fn successful_auths(&self) -> u64 {
349 self.successful_auths.load(Ordering::Relaxed)
350 }
351
352 pub fn failed_auths(&self) -> u64 {
354 self.failed_auths.load(Ordering::Relaxed)
355 }
356
357 pub fn cached_hits(&self) -> u64 {
359 self.cached_hits.load(Ordering::Relaxed)
360 }
361
362 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 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 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 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 pub fn incr_total_requests(&self) {
406 self.total_requests.fetch_add(1, Ordering::Relaxed);
407 }
408
409 pub fn incr_successful_auths(&self) {
411 self.successful_auths.fetch_add(1, Ordering::Relaxed);
412 }
413
414 pub fn incr_failed_auths(&self) {
416 self.failed_auths.fetch_add(1, Ordering::Relaxed);
417 }
418
419 pub fn incr_cached_hits(&self) {
421 self.cached_hits.fetch_add(1, Ordering::Relaxed);
422 }
423
424 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
431pub type CustomErrorHandler = Arc<dyn Fn(&AuthError, &str) -> web::HttpResponse + Send + Sync>;
433
434pub struct BasicAuthConfig {
436 pub realm: String,
438 pub validator: Arc<dyn UserValidator>,
440 #[cfg(feature = "cache")]
441 pub cache: Option<Arc<AuthCache>>,
443 pub path_filter: Option<Arc<crate::utils::PathFilter>>,
445 pub max_header_size: usize,
447 pub log_failures: bool,
449 pub custom_error_handler: Option<CustomErrorHandler>,
451 pub max_concurrent_validations: Option<usize>,
453 pub validation_timeout: Option<Duration>,
455 pub rate_limit_per_ip: Option<(usize, Duration)>,
457 pub client_ip_header: Option<String>,
462 pub enable_metrics: bool,
464 pub log_usernames_in_production: bool,
466}
467
468impl BasicAuthConfig {
469 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, 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 pub fn realm(mut self, realm: String) -> Self {
491 self.realm = realm;
492 self
493 }
494
495 #[cfg(feature = "cache")]
496 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 pub fn disable_cache(mut self) -> Self {
505 self.cache = None;
506 self
507 }
508
509 pub fn path_filter(mut self, filter: crate::utils::PathFilter) -> Self {
511 self.path_filter = Some(Arc::new(filter));
512 self
513 }
514
515 pub fn max_header_size(mut self, size: usize) -> Self {
517 self.max_header_size = size;
518 self
519 }
520
521 pub fn log_failures(mut self, enabled: bool) -> Self {
523 self.log_failures = enabled;
524 self
525 }
526
527 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 pub fn max_concurrent_validations(mut self, max: usize) -> Self {
538 self.max_concurrent_validations = Some(max);
539 self
540 }
541
542 pub fn validation_timeout(mut self, timeout: Duration) -> Self {
544 self.validation_timeout = Some(timeout);
545 self
546 }
547
548 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 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 pub fn enable_metrics(mut self, enabled: bool) -> Self {
564 self.enable_metrics = enabled;
565 self
566 }
567
568 pub fn log_usernames_in_production(mut self, enabled: bool) -> Self {
570 self.log_usernames_in_production = enabled;
571 self
572 }
573
574 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 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 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 eprintln!(
637 "Warning: Cache has {} entries, consider reducing TTL",
638 stats.total_entries
639 );
640 }
641 }
642
643 Ok(())
644 }
645}
646
647pub 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 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 pub fn metrics(&self) -> &AuthMetrics {
675 &self.metrics
676 }
677
678 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 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 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 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..]; 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 let decoded_str = std::str::from_utf8(&decoded).map_err(|_| AuthError::InvalidBase64)?;
719
720 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 if !credentials.is_valid_format() {
729 return Err(AuthError::InvalidCredentials);
730 }
731
732 Ok(credentials)
733 }
734
735 async fn authenticate(&self, credentials: &Credentials) -> AuthResult<bool> {
737 self.config.validator.pre_validate(credentials)?;
739
740 #[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 if let Err(e) = cache.insert(cache_key, result) {
761 eprintln!("Failed to cache authentication result: {}", e);
763 }
764
765 return Ok(result);
766 }
767 }
768
769 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 async fn run_validation(&self, credentials: &Credentials) -> AuthResult<bool> {
791 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 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 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 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 fn log_auth_failure(&self, error: &AuthError, username: Option<&str>) {
860 if self.config.log_failures {
861 let safe_username = if self.config.log_usernames_in_production || cfg!(debug_assertions)
863 {
864 username
865 } else {
866 None };
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 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 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 let auth_header = req
954 .headers()
955 .get("authorization")
956 .and_then(|h| h.to_str().ok());
957
958 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 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 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 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 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()); 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()); 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 assert_eq!(key1, key2);
1091
1092 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 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 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 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 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 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 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 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}