1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use parking_lot::RwLock;
11use thiserror::Error;
12
13use super::config::{
14 ApiKeyConfig, AuthConfig, AuthMethod, Identity, JwtConfig, LdapConfig, OAuthConfig,
15};
16use super::jwt::{JwtError, JwtValidator};
17
18#[derive(Debug, Error)]
20pub enum AuthError {
21 #[error("Authentication required")]
22 AuthenticationRequired,
23
24 #[error("Invalid credentials")]
25 InvalidCredentials,
26
27 #[error("Token expired")]
28 TokenExpired,
29
30 #[error("Insufficient permissions: {0}")]
31 InsufficientPermissions(String),
32
33 #[error("Rate limited: retry after {0} seconds")]
34 RateLimited(u64),
35
36 #[error("Authentication provider unavailable: {0}")]
37 ProviderUnavailable(String),
38
39 #[error("Invalid authentication method: {0}")]
40 InvalidMethod(String),
41
42 #[error("JWT error: {0}")]
43 Jwt(#[from] JwtError),
44
45 #[error("OAuth error: {0}")]
46 OAuth(String),
47
48 #[error("LDAP error: {0}")]
49 Ldap(String),
50
51 #[error("API key error: {0}")]
52 ApiKey(String),
53
54 #[error("Session error: {0}")]
55 Session(String),
56
57 #[error("Configuration error: {0}")]
58 Configuration(String),
59}
60
61#[derive(Debug, Clone)]
63pub struct AuthRequest {
64 pub headers: HashMap<String, String>,
66
67 pub username: Option<String>,
69
70 pub password: Option<String>,
72
73 pub client_ip: Option<std::net::IpAddr>,
75
76 pub database: Option<String>,
78
79 pub timestamp: chrono::DateTime<chrono::Utc>,
81}
82
83impl AuthRequest {
84 pub fn new() -> Self {
86 Self {
87 headers: HashMap::new(),
88 username: None,
89 password: None,
90 client_ip: None,
91 database: None,
92 timestamp: chrono::Utc::now(),
93 }
94 }
95
96 pub fn with_username(mut self, username: impl Into<String>) -> Self {
98 self.username = Some(username.into());
99 self
100 }
101
102 pub fn with_password(mut self, password: impl Into<String>) -> Self {
104 self.password = Some(password.into());
105 self
106 }
107
108 pub fn with_client_ip(mut self, ip: std::net::IpAddr) -> Self {
110 self.client_ip = Some(ip);
111 self
112 }
113
114 pub fn with_database(mut self, database: impl Into<String>) -> Self {
116 self.database = Some(database.into());
117 self
118 }
119
120 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
122 self.headers.insert(key.into(), value.into());
123 self
124 }
125
126 pub fn authorization_header(&self) -> Option<&str> {
128 self.headers
129 .get("authorization")
130 .or_else(|| self.headers.get("Authorization"))
131 .map(|s| s.as_str())
132 }
133
134 pub fn bearer_token(&self) -> Option<&str> {
136 self.authorization_header()
137 .and_then(|h| h.strip_prefix("Bearer "))
138 .or_else(|| self.authorization_header()?.strip_prefix("bearer "))
139 }
140
141 pub fn api_key(&self, header_name: &str) -> Option<&str> {
143 self.headers
144 .get(header_name)
145 .or_else(|| self.headers.get(&header_name.to_lowercase()))
146 .map(|s| s.as_str())
147 }
148}
149
150impl Default for AuthRequest {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct AuthResult {
159 pub identity: Identity,
161
162 pub session_token: Option<String>,
164
165 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
167
168 pub metadata: HashMap<String, String>,
170}
171
172impl AuthResult {
173 pub fn new(identity: Identity) -> Self {
175 Self {
176 identity,
177 session_token: None,
178 expires_at: None,
179 metadata: HashMap::new(),
180 }
181 }
182
183 pub fn with_session_token(mut self, token: String) -> Self {
185 self.session_token = Some(token);
186 self
187 }
188
189 pub fn with_expiration(mut self, expires_at: chrono::DateTime<chrono::Utc>) -> Self {
191 self.expires_at = Some(expires_at);
192 self
193 }
194
195 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
197 self.metadata.insert(key.into(), value.into());
198 self
199 }
200}
201
202pub struct AuthenticationHandler {
204 config: AuthConfig,
206
207 jwt_validator: Option<JwtValidator>,
209
210 oauth_enabled: bool,
213
214 ldap_enabled: bool,
218
219 api_keys: Arc<RwLock<HashMap<String, ApiKeyEntry>>>,
221
222 rate_limiter: Arc<RwLock<RateLimiterState>>,
224
225 auth_cache: Arc<RwLock<AuthCache>>,
227}
228
229#[derive(Debug, Clone)]
231struct ApiKeyEntry {
232 #[allow(dead_code)]
234 key_id: String,
235
236 key_hash: String,
238
239 identity: Identity,
241
242 #[allow(dead_code)]
244 created_at: chrono::DateTime<chrono::Utc>,
245
246 expires_at: Option<chrono::DateTime<chrono::Utc>>,
248
249 active: bool,
251
252 #[allow(dead_code)]
254 scopes: Vec<String>,
255
256 #[allow(dead_code)]
258 rate_limit: Option<u32>,
259}
260
261struct RateLimiterState {
263 by_ip: HashMap<std::net::IpAddr, RateLimitBucket>,
265
266 by_user: HashMap<String, RateLimitBucket>,
268
269 last_cleanup: Instant,
271}
272
273struct RateLimitBucket {
275 count: u32,
277
278 window_start: Instant,
280}
281
282impl RateLimitBucket {
283 fn new() -> Self {
284 Self {
285 count: 0,
286 window_start: Instant::now(),
287 }
288 }
289
290 fn increment(&mut self, window: Duration) -> u32 {
291 if self.window_start.elapsed() > window {
292 self.count = 1;
293 self.window_start = Instant::now();
294 } else {
295 self.count += 1;
296 }
297 self.count
298 }
299}
300
301struct AuthCache {
303 entries: HashMap<String, CachedAuth>,
305
306 max_size: usize,
308
309 ttl: Duration,
311}
312
313struct CachedAuth {
315 result: AuthResult,
317
318 cached_at: Instant,
320}
321
322impl AuthCache {
323 fn new(max_size: usize, ttl: Duration) -> Self {
324 Self {
325 entries: HashMap::new(),
326 max_size,
327 ttl,
328 }
329 }
330
331 fn get(&self, key: &str) -> Option<&AuthResult> {
332 self.entries.get(key).and_then(|cached| {
333 if cached.cached_at.elapsed() < self.ttl {
334 Some(&cached.result)
335 } else {
336 None
337 }
338 })
339 }
340
341 fn insert(&mut self, key: String, result: AuthResult) {
342 if self.entries.len() >= self.max_size {
343 self.evict_expired();
344 }
345 self.entries.insert(
346 key,
347 CachedAuth {
348 result,
349 cached_at: Instant::now(),
350 },
351 );
352 }
353
354 fn evict_expired(&mut self) {
355 self.entries
356 .retain(|_, cached| cached.cached_at.elapsed() < self.ttl);
357 }
358}
359
360impl AuthenticationHandler {
361 pub fn new(config: AuthConfig) -> Self {
363 let jwt_validator = config
364 .jwt
365 .as_ref()
366 .map(|jwt_config| JwtValidator::new(jwt_config.clone()));
367
368 let oauth_enabled = config.oauth.is_some();
369 let ldap_enabled = config.ldap.is_some();
370
371 Self {
372 config,
373 jwt_validator,
374 oauth_enabled,
375 ldap_enabled,
376 api_keys: Arc::new(RwLock::new(HashMap::new())),
377 rate_limiter: Arc::new(RwLock::new(RateLimiterState {
378 by_ip: HashMap::new(),
379 by_user: HashMap::new(),
380 last_cleanup: Instant::now(),
381 })),
382 auth_cache: Arc::new(RwLock::new(AuthCache::new(1000, Duration::from_secs(60)))),
383 }
384 }
385
386 pub fn builder() -> AuthenticationHandlerBuilder {
388 AuthenticationHandlerBuilder::new()
389 }
390
391 pub async fn authenticate(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
393 if !self.config.enabled {
395 return Ok(AuthResult::new(Identity::anonymous()));
397 }
398
399 self.check_rate_limit(request)?;
401
402 let methods = &self.config.auth_methods;
404
405 for method in methods {
406 match self.try_authenticate(request, method).await {
407 Ok(result) => return Ok(result),
408 Err(AuthError::AuthenticationRequired) => continue,
409 Err(e) => return Err(e),
410 }
411 }
412
413 Err(AuthError::AuthenticationRequired)
415 }
416
417 async fn try_authenticate(
419 &self,
420 request: &AuthRequest,
421 method: &AuthMethod,
422 ) -> Result<AuthResult, AuthError> {
423 match method {
424 AuthMethod::Jwt => self.authenticate_jwt(request).await,
425 AuthMethod::OAuth => self.authenticate_oauth(request).await,
426 AuthMethod::Ldap => self.authenticate_ldap(request).await,
427 AuthMethod::ApiKey => self.authenticate_api_key(request).await,
428 AuthMethod::Basic => self.authenticate_basic(request).await,
429 AuthMethod::Trust => self.authenticate_trust(request),
430 AuthMethod::AgentToken | AuthMethod::Session | AuthMethod::Anonymous => {
431 self.authenticate_trust(request)
432 }
433 }
434 }
435
436 async fn authenticate_jwt(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
438 let validator = self
439 .jwt_validator
440 .as_ref()
441 .ok_or(AuthError::Configuration("JWT not configured".to_string()))?;
442
443 let token = request
444 .bearer_token()
445 .ok_or(AuthError::AuthenticationRequired)?;
446
447 if let Some(cached) = self.auth_cache.read().get(token) {
449 return Ok(cached.clone());
450 }
451
452 let identity = validator.validate_to_identity(token)?;
454 let result = AuthResult::new(identity);
455
456 self.auth_cache
458 .write()
459 .insert(token.to_string(), result.clone());
460
461 Ok(result)
462 }
463
464 async fn authenticate_oauth(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
466 if !self.oauth_enabled {
467 return Err(AuthError::Configuration("OAuth not configured".to_string()));
468 }
469
470 let token = request
471 .bearer_token()
472 .ok_or(AuthError::AuthenticationRequired)?;
473
474 if let Some(cached) = self.auth_cache.read().get(token) {
476 return Ok(cached.clone());
477 }
478
479 let cfg = self
480 .config
481 .oauth
482 .as_ref()
483 .ok_or_else(|| AuthError::Configuration("OAuth not configured".to_string()))?;
484
485 let body = self.oauth_introspect(cfg, token).await?;
490
491 if body.get("active").and_then(|v| v.as_bool()) != Some(true) {
493 return Err(AuthError::InvalidCredentials);
494 }
495
496 if !cfg.issuer.is_empty() {
498 if let Some(iss) = body.get("iss").and_then(|v| v.as_str()) {
499 if iss != cfg.issuer {
500 return Err(AuthError::InvalidCredentials);
501 }
502 }
503 }
504
505 if let Some(expected) = &cfg.audience {
507 let ok = match body.get("aud") {
508 Some(serde_json::Value::String(s)) => s == expected,
509 Some(serde_json::Value::Array(a)) => {
510 a.iter().any(|v| v.as_str() == Some(expected.as_str()))
511 }
512 _ => false,
513 };
514 if !ok {
515 return Err(AuthError::InvalidCredentials);
516 }
517 }
518
519 let scopes: Vec<String> = body
522 .get("scope")
523 .and_then(|v| v.as_str())
524 .map(|s| s.split_whitespace().map(String::from).collect())
525 .unwrap_or_default();
526 for required in &cfg.required_scopes {
527 if !scopes.contains(required) {
528 return Err(AuthError::InsufficientPermissions(format!(
529 "missing required scope: {}",
530 required
531 )));
532 }
533 }
534
535 let subject = body
537 .get("sub")
538 .and_then(|v| v.as_str())
539 .or_else(|| body.get("username").and_then(|v| v.as_str()))
540 .ok_or_else(|| {
541 AuthError::OAuth("introspection response missing sub/username".to_string())
542 })?;
543
544 let mut identity = Identity::new(subject, "oauth");
545 identity.name = body
546 .get("username")
547 .and_then(|v| v.as_str())
548 .map(String::from);
549 identity.roles = scopes;
550
551 let mut result = AuthResult::new(identity);
552 if let Some(exp) = body
553 .get("exp")
554 .and_then(|v| v.as_i64())
555 .and_then(|e| chrono::DateTime::from_timestamp(e, 0))
556 {
557 result = result.with_expiration(exp);
558 }
559
560 self.auth_cache
562 .write()
563 .insert(token.to_string(), result.clone());
564
565 Ok(result)
566 }
567
568 async fn oauth_introspect(
572 &self,
573 cfg: &OAuthConfig,
574 token: &str,
575 ) -> Result<serde_json::Value, AuthError> {
576 let client = reqwest::Client::builder()
577 .timeout(std::time::Duration::from_secs(5))
578 .build()
579 .map_err(|e| AuthError::ProviderUnavailable(format!("http client: {}", e)))?;
580
581 let resp = client
582 .post(&cfg.introspection_url)
583 .basic_auth(&cfg.client_id, Some(&cfg.client_secret))
584 .form(&[("token", token), ("token_type_hint", "access_token")])
585 .send()
586 .await
587 .map_err(|e| AuthError::ProviderUnavailable(format!("introspection request: {}", e)))?;
588
589 if !resp.status().is_success() {
590 return Err(AuthError::ProviderUnavailable(format!(
591 "introspection endpoint returned HTTP {}",
592 resp.status()
593 )));
594 }
595
596 resp.json::<serde_json::Value>()
597 .await
598 .map_err(|e| AuthError::OAuth(format!("introspection response body: {}", e)))
599 }
600
601 async fn authenticate_ldap(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
603 if !self.ldap_enabled {
604 return Err(AuthError::Configuration("LDAP not configured".to_string()));
605 }
606
607 let username = request
608 .username
609 .as_ref()
610 .ok_or(AuthError::AuthenticationRequired)?;
611 let password = request
612 .password
613 .as_ref()
614 .ok_or(AuthError::AuthenticationRequired)?;
615
616 #[cfg(feature = "ldap-auth")]
617 {
618 let cfg = self
619 .config
620 .ldap
621 .as_ref()
622 .ok_or_else(|| AuthError::Configuration("LDAP not configured".to_string()))?;
623 let groups = self.ldap_search_and_bind(cfg, username, password).await?;
624 let mut identity = Identity::new(username.clone(), "ldap");
625 identity.groups = groups;
626 Ok(AuthResult::new(identity))
627 }
628
629 #[cfg(not(feature = "ldap-auth"))]
632 {
633 let _ = (username, password);
634 Err(AuthError::Configuration(
635 "LDAP bind not implemented (build with the `ldap-auth` feature)".to_string(),
636 ))
637 }
638 }
639
640 #[cfg(feature = "ldap-auth")]
654 async fn ldap_search_and_bind(
655 &self,
656 cfg: &LdapConfig,
657 username: &str,
658 password: &str,
659 ) -> Result<Vec<String>, AuthError> {
660 use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
661
662 static LDAP_TLS_PROVIDER: std::sync::Once = std::sync::Once::new();
667 LDAP_TLS_PROVIDER.call_once(|| {
668 let _ = rustls::crypto::ring::default_provider().install_default();
669 });
670
671 let settings = LdapConnSettings::new()
672 .set_starttls(cfg.starttls)
673 .set_conn_timeout(cfg.timeout);
674
675 let (conn, mut ldap) = LdapConnAsync::with_settings(settings.clone(), &cfg.server_url)
677 .await
678 .map_err(|e| AuthError::Ldap(format!("connect {}: {}", cfg.server_url, e)))?;
679 ldap3::drive!(conn);
680
681 if !cfg.bind_dn.is_empty() {
682 ldap.simple_bind(&cfg.bind_dn, &cfg.bind_password)
683 .await
684 .map_err(|e| AuthError::Ldap(format!("service bind: {}", e)))?
685 .success()
686 .map_err(|e| AuthError::Ldap(format!("service bind rejected: {}", e)))?;
687 }
688
689 let filter = cfg
690 .user_filter
691 .replace("{0}", &ldap3::ldap_escape(username));
692 let (entries, _res) = ldap
693 .search(
694 &cfg.user_search_base,
695 Scope::Subtree,
696 &filter,
697 vec![cfg.group_attribute.as_str()],
698 )
699 .await
700 .map_err(|e| AuthError::Ldap(format!("search: {}", e)))?
701 .success()
702 .map_err(|e| AuthError::Ldap(format!("search rejected: {}", e)))?;
703
704 let entry = match entries.into_iter().next() {
705 Some(e) => SearchEntry::construct(e),
706 None => {
708 let _ = ldap.unbind().await;
709 return Err(AuthError::InvalidCredentials);
710 }
711 };
712 let user_dn = entry.dn.clone();
713 let groups = entry
714 .attrs
715 .get(&cfg.group_attribute)
716 .cloned()
717 .unwrap_or_default();
718 let _ = ldap.unbind().await;
719
720 if user_dn.is_empty() {
721 return Err(AuthError::InvalidCredentials);
722 }
723 if password.is_empty() {
726 return Err(AuthError::InvalidCredentials);
727 }
728
729 let (conn2, mut ldap2) = LdapConnAsync::with_settings(settings, &cfg.server_url)
731 .await
732 .map_err(|e| AuthError::Ldap(format!("connect (user bind): {}", e)))?;
733 ldap3::drive!(conn2);
734 let bind = ldap2
735 .simple_bind(&user_dn, password)
736 .await
737 .map_err(|e| AuthError::Ldap(format!("user bind: {}", e)))?;
738 let _ = ldap2.unbind().await;
739 bind.success().map_err(|_| AuthError::InvalidCredentials)?;
740
741 Ok(groups)
742 }
743
744 async fn authenticate_api_key(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
746 let api_key_config = self
747 .config
748 .api_keys
749 .as_ref()
750 .ok_or(AuthError::Configuration(
751 "API keys not configured".to_string(),
752 ))?;
753
754 let header_name = &api_key_config.header_name;
755 let key = request
756 .api_key(header_name)
757 .ok_or(AuthError::AuthenticationRequired)?;
758
759 if let Some(cached) = self.auth_cache.read().get(key) {
761 return Ok(cached.clone());
762 }
763
764 let api_keys = self.api_keys.read();
766 let entry = api_keys
767 .values()
768 .find(|e| self.verify_api_key(key, &e.key_hash) && e.active)
769 .ok_or(AuthError::InvalidCredentials)?;
770
771 if let Some(expires_at) = entry.expires_at {
773 if chrono::Utc::now() > expires_at {
774 return Err(AuthError::TokenExpired);
775 }
776 }
777
778 let result = AuthResult::new(entry.identity.clone());
779 self.auth_cache
780 .write()
781 .insert(key.to_string(), result.clone());
782
783 Ok(result)
784 }
785
786 async fn authenticate_basic(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
788 let auth_header = request
789 .authorization_header()
790 .ok_or(AuthError::AuthenticationRequired)?;
791
792 if !auth_header.starts_with("Basic ") {
793 return Err(AuthError::AuthenticationRequired);
794 }
795
796 let encoded = &auth_header[6..];
797 let decoded = base64_decode(encoded).map_err(|_| AuthError::InvalidCredentials)?;
798 let credentials = String::from_utf8(decoded).map_err(|_| AuthError::InvalidCredentials)?;
799
800 let parts: Vec<&str> = credentials.splitn(2, ':').collect();
801 if parts.len() != 2 {
802 return Err(AuthError::InvalidCredentials);
803 }
804
805 let username = parts[0];
806 let password = parts[1];
807
808 let _ = (username, password);
811 Err(AuthError::InvalidCredentials)
812 }
813
814 fn authenticate_trust(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
816 let username = request
818 .username
819 .as_ref()
820 .unwrap_or(&"anonymous".to_string())
821 .clone();
822
823 let identity = Identity {
824 user_id: username.clone(),
825 name: Some(username),
826 email: None,
827 roles: vec!["trusted".to_string()],
828 groups: Vec::new(),
829 tenant_id: None,
830 claims: HashMap::new(),
831 auth_method: "trust".to_string(),
832 authenticated_at: chrono::Utc::now(),
833 };
834
835 Ok(AuthResult::new(identity))
836 }
837
838 fn check_rate_limit(&self, request: &AuthRequest) -> Result<(), AuthError> {
840 let config = &self.config.rate_limit;
841 if !config.enabled {
842 return Ok(());
843 }
844
845 let mut limiter = self.rate_limiter.write();
846
847 if limiter.last_cleanup.elapsed() > Duration::from_secs(60) {
849 let window = Duration::from_secs(config.window_seconds);
850 limiter
851 .by_ip
852 .retain(|_, b| b.window_start.elapsed() < window);
853 limiter
854 .by_user
855 .retain(|_, b| b.window_start.elapsed() < window);
856 limiter.last_cleanup = Instant::now();
857 }
858
859 let window = Duration::from_secs(config.window_seconds);
860
861 if let Some(ip) = request.client_ip {
863 let bucket = limiter.by_ip.entry(ip).or_insert_with(RateLimitBucket::new);
864 let count = bucket.increment(window);
865 if count > config.max_requests_per_ip {
866 let retry_after = window
867 .as_secs()
868 .saturating_sub(bucket.window_start.elapsed().as_secs());
869 return Err(AuthError::RateLimited(retry_after));
870 }
871 }
872
873 if let Some(username) = &request.username {
875 let bucket = limiter
876 .by_user
877 .entry(username.clone())
878 .or_insert_with(RateLimitBucket::new);
879 let count = bucket.increment(window);
880 if count > config.max_requests_per_user {
881 let retry_after = window
882 .as_secs()
883 .saturating_sub(bucket.window_start.elapsed().as_secs());
884 return Err(AuthError::RateLimited(retry_after));
885 }
886 }
887
888 Ok(())
889 }
890
891 fn verify_api_key(&self, key: &str, hash: &str) -> bool {
895 let computed = self.hash_api_key(key);
896 let a = computed.as_bytes();
897 let b = hash.as_bytes();
898 if a.len() != b.len() {
899 return false;
900 }
901 let mut diff = 0u8;
902 for (x, y) in a.iter().zip(b.iter()) {
903 diff |= x ^ y;
904 }
905 diff == 0
906 }
907
908 fn hash_api_key(&self, key: &str) -> String {
912 use sha2::{Digest, Sha256};
913 let digest = Sha256::digest(key.as_bytes());
914 let mut out = String::with_capacity(64);
915 for b in digest {
916 use std::fmt::Write;
917 let _ = write!(out, "{:02x}", b);
918 }
919 out
920 }
921
922 pub fn register_api_key(
924 &self,
925 key_id: String,
926 key_value: String,
927 identity: Identity,
928 expires_at: Option<chrono::DateTime<chrono::Utc>>,
929 scopes: Vec<String>,
930 ) {
931 let entry = ApiKeyEntry {
932 key_id: key_id.clone(),
933 key_hash: self.hash_api_key(&key_value),
934 identity,
935 created_at: chrono::Utc::now(),
936 expires_at,
937 active: true,
938 scopes,
939 rate_limit: None,
940 };
941
942 self.api_keys.write().insert(key_id, entry);
943 }
944
945 pub fn revoke_api_key(&self, key_id: &str) -> bool {
947 if let Some(entry) = self.api_keys.write().get_mut(key_id) {
948 entry.active = false;
949 true
950 } else {
951 false
952 }
953 }
954
955 pub async fn refresh_jwks_if_needed(&self) -> Result<(), AuthError> {
957 if let Some(validator) = &self.jwt_validator {
958 if validator.needs_refresh() {
959 validator.refresh_jwks().await?;
960 }
961 }
962 Ok(())
963 }
964
965 pub fn clear_cache(&self) {
967 self.auth_cache.write().entries.clear();
968 }
969
970 pub fn cache_stats(&self) -> CacheStats {
972 let cache = self.auth_cache.read();
973 CacheStats {
974 entries: cache.entries.len(),
975 max_size: cache.max_size,
976 ttl_seconds: cache.ttl.as_secs(),
977 }
978 }
979
980 pub fn is_enabled(&self) -> bool {
982 self.config.enabled
983 }
984}
985
986#[derive(Debug, Clone)]
988pub struct CacheStats {
989 pub entries: usize,
991
992 pub max_size: usize,
994
995 pub ttl_seconds: u64,
997}
998
999pub struct AuthenticationHandlerBuilder {
1001 config: AuthConfig,
1002}
1003
1004impl AuthenticationHandlerBuilder {
1005 pub fn new() -> Self {
1007 Self {
1008 config: AuthConfig::default(),
1009 }
1010 }
1011
1012 pub fn enabled(mut self, enabled: bool) -> Self {
1014 self.config.enabled = enabled;
1015 self
1016 }
1017
1018 pub fn with_jwt(mut self, config: JwtConfig) -> Self {
1020 self.config.jwt = Some(config);
1021 self.config.auth_methods.push(AuthMethod::Jwt);
1022 self
1023 }
1024
1025 pub fn with_oauth(mut self, config: OAuthConfig) -> Self {
1027 self.config.oauth = Some(config);
1028 self.config.auth_methods.push(AuthMethod::OAuth);
1029 self
1030 }
1031
1032 pub fn with_ldap(mut self, config: LdapConfig) -> Self {
1034 self.config.ldap = Some(config);
1035 self.config.auth_methods.push(AuthMethod::Ldap);
1036 self
1037 }
1038
1039 pub fn with_api_keys(mut self, config: ApiKeyConfig) -> Self {
1041 self.config.api_keys = Some(config);
1042 self.config.auth_methods.push(AuthMethod::ApiKey);
1043 self
1044 }
1045
1046 pub fn with_basic_auth(mut self) -> Self {
1048 self.config.auth_methods.push(AuthMethod::Basic);
1049 self
1050 }
1051
1052 pub fn with_trust_auth(mut self) -> Self {
1054 self.config.auth_methods.push(AuthMethod::Trust);
1055 self
1056 }
1057
1058 pub fn default_role(mut self, role: impl Into<String>) -> Self {
1060 self.config.default_role = Some(role.into());
1061 self
1062 }
1063
1064 pub fn build(self) -> AuthenticationHandler {
1066 AuthenticationHandler::new(self.config)
1067 }
1068}
1069
1070impl Default for AuthenticationHandlerBuilder {
1071 fn default() -> Self {
1072 Self::new()
1073 }
1074}
1075
1076fn base64_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1078 use base64::{engine::general_purpose::STANDARD, Engine};
1079 STANDARD.decode(input)
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 fn test_config() -> AuthConfig {
1087 let mut config = AuthConfig::default();
1088 config.enabled = true;
1089 config.auth_methods = vec![AuthMethod::Trust];
1090 config
1091 }
1092
1093 #[tokio::test]
1094 async fn test_authentication_disabled() {
1095 let mut config = AuthConfig::default();
1096 config.enabled = false;
1097 let handler = AuthenticationHandler::new(config);
1098
1099 let request = AuthRequest::new();
1100 let result = handler.authenticate(&request).await.unwrap();
1101
1102 assert_eq!(result.identity.auth_method, "anonymous");
1103 }
1104
1105 #[tokio::test]
1106 async fn test_trust_authentication() {
1107 let handler = AuthenticationHandler::new(test_config());
1108
1109 let request = AuthRequest::new().with_username("testuser");
1110 let result = handler.authenticate(&request).await.unwrap();
1111
1112 assert_eq!(result.identity.user_id, "testuser");
1113 assert_eq!(result.identity.auth_method, "trust");
1114 }
1115
1116 #[test]
1117 fn test_auth_request_builder() {
1118 let request = AuthRequest::new()
1119 .with_username("user")
1120 .with_password("pass")
1121 .with_database("mydb")
1122 .with_header("Authorization", "Bearer token123");
1123
1124 assert_eq!(request.username, Some("user".to_string()));
1125 assert_eq!(request.password, Some("pass".to_string()));
1126 assert_eq!(request.database, Some("mydb".to_string()));
1127 assert_eq!(request.bearer_token(), Some("token123"));
1128 }
1129
1130 #[test]
1131 fn test_bearer_token_extraction() {
1132 let request = AuthRequest::new().with_header("Authorization", "Bearer my-jwt-token");
1133
1134 assert_eq!(request.bearer_token(), Some("my-jwt-token"));
1135 }
1136
1137 #[test]
1138 fn test_api_key_extraction() {
1139 let request = AuthRequest::new().with_header("X-API-Key", "secret-key-123");
1140
1141 assert_eq!(request.api_key("X-API-Key"), Some("secret-key-123"));
1142 }
1143
1144 #[tokio::test]
1145 async fn test_api_key_registration_and_auth() {
1146 let mut config = AuthConfig::default();
1147 config.enabled = true;
1148 config.api_keys = Some(ApiKeyConfig {
1149 header_name: "X-API-Key".to_string(),
1150 query_param: None,
1151 prefix: None,
1152 hash_algorithm: "sha256".to_string(),
1153 });
1154 config.auth_methods = vec![AuthMethod::ApiKey];
1155
1156 let handler = AuthenticationHandler::new(config);
1157
1158 let identity = Identity {
1160 user_id: "api_user".to_string(),
1161 name: Some("API User".to_string()),
1162 email: None,
1163 roles: vec!["api".to_string()],
1164 groups: Vec::new(),
1165 tenant_id: None,
1166 claims: HashMap::new(),
1167 auth_method: "api_key".to_string(),
1168 authenticated_at: chrono::Utc::now(),
1169 };
1170
1171 handler.register_api_key(
1172 "key1".to_string(),
1173 "secret123".to_string(),
1174 identity,
1175 None,
1176 vec!["read".to_string()],
1177 );
1178
1179 let request = AuthRequest::new().with_header("X-API-Key", "secret123");
1181
1182 let result = handler.authenticate(&request).await.unwrap();
1183 assert_eq!(result.identity.user_id, "api_user");
1184 }
1185
1186 #[test]
1187 fn test_cache_stats() {
1188 let handler = AuthenticationHandler::new(test_config());
1189 let stats = handler.cache_stats();
1190
1191 assert_eq!(stats.entries, 0);
1192 assert_eq!(stats.max_size, 1000);
1193 }
1194
1195 #[test]
1196 fn test_handler_builder() {
1197 let handler = AuthenticationHandler::builder()
1198 .enabled(true)
1199 .with_trust_auth()
1200 .default_role("user")
1201 .build();
1202
1203 assert!(handler.is_enabled());
1204 }
1205
1206 #[tokio::test]
1207 async fn basic_auth_denies_without_user_store() {
1208 let mut config = AuthConfig::default();
1211 config.enabled = true;
1212 config.auth_methods = vec![AuthMethod::Basic];
1213 let handler = AuthenticationHandler::new(config);
1214
1215 use base64::{engine::general_purpose::STANDARD, Engine};
1216 let creds = STANDARD.encode("alice:any-password");
1217 let request = AuthRequest::new().with_header("Authorization", format!("Basic {creds}"));
1218 let result = handler.authenticate(&request).await;
1219 assert!(
1220 result.is_err(),
1221 "basic auth must deny without a user store, got {result:?}"
1222 );
1223 }
1224
1225 async fn spawn_introspection_mock(body: &'static str) -> String {
1230 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1231 use tokio::net::TcpListener;
1232 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1233 let addr = listener.local_addr().unwrap();
1234 tokio::spawn(async move {
1235 while let Ok((mut sock, _)) = listener.accept().await {
1236 let mut buf = [0u8; 4096];
1238 let _ = sock.read(&mut buf).await;
1239 let resp = format!(
1240 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
1241 Content-Length: {}\r\nConnection: close\r\n\r\n{}",
1242 body.len(),
1243 body
1244 );
1245 let _ = sock.write_all(resp.as_bytes()).await;
1246 let _ = sock.shutdown().await;
1247 }
1248 });
1249 format!("http://{}/introspect", addr)
1250 }
1251
1252 fn oauth_cfg(introspection_url: String, required_scopes: Vec<String>) -> OAuthConfig {
1253 OAuthConfig {
1254 introspection_url,
1255 client_id: "proxy".into(),
1256 client_secret: "secret".into(),
1257 token_url: None,
1258 scopes: Vec::new(),
1259 cache_ttl: std::time::Duration::from_secs(60),
1260 required_scopes,
1261 issuer: String::new(),
1262 authorization_url: None,
1263 audience: None,
1264 }
1265 }
1266
1267 #[tokio::test]
1268 async fn oauth_introspection_accepts_active_token() {
1269 let url = spawn_introspection_mock(
1270 r#"{"active":true,"sub":"alice","username":"alice","scope":"read write"}"#,
1271 )
1272 .await;
1273 let handler = AuthenticationHandlerBuilder::new()
1274 .enabled(true)
1275 .with_oauth(oauth_cfg(url, Vec::new()))
1276 .build();
1277 let req = AuthRequest::new().with_header("Authorization", "Bearer tok-abc");
1278 let result = handler
1279 .authenticate_oauth(&req)
1280 .await
1281 .expect("active token must authenticate");
1282 assert_eq!(result.identity.user_id, "alice");
1283 assert!(result.identity.roles.contains(&"read".to_string()));
1284 assert!(result.identity.roles.contains(&"write".to_string()));
1285 }
1286
1287 #[tokio::test]
1288 async fn oauth_introspection_denies_inactive_token() {
1289 let url = spawn_introspection_mock(r#"{"active":false}"#).await;
1290 let handler = AuthenticationHandlerBuilder::new()
1291 .enabled(true)
1292 .with_oauth(oauth_cfg(url, Vec::new()))
1293 .build();
1294 let req = AuthRequest::new().with_header("Authorization", "Bearer dead");
1295 let err = handler.authenticate_oauth(&req).await.unwrap_err();
1296 assert!(
1297 matches!(err, AuthError::InvalidCredentials),
1298 "inactive token must be denied, got {err:?}"
1299 );
1300 }
1301
1302 #[tokio::test]
1303 async fn oauth_introspection_enforces_required_scopes() {
1304 let url = spawn_introspection_mock(r#"{"active":true,"sub":"bob","scope":"read"}"#).await;
1305 let handler = AuthenticationHandlerBuilder::new()
1306 .enabled(true)
1307 .with_oauth(oauth_cfg(url, vec!["admin".to_string()]))
1308 .build();
1309 let req = AuthRequest::new().with_header("Authorization", "Bearer tok");
1310 let err = handler.authenticate_oauth(&req).await.unwrap_err();
1311 assert!(
1312 matches!(err, AuthError::InsufficientPermissions(_)),
1313 "missing required scope must deny, got {err:?}"
1314 );
1315 }
1316
1317 #[cfg(feature = "ldap-auth")]
1327 #[tokio::test]
1328 async fn ldap_live_search_and_bind() {
1329 use std::time::Duration;
1330
1331 let url = match std::env::var("HELIOS_LDAP_URL") {
1332 Ok(u) if !u.is_empty() => u,
1333 _ => {
1334 eprintln!("skipping ldap_live_search_and_bind: set HELIOS_LDAP_URL");
1335 return;
1336 }
1337 };
1338 let env = |k: &str, d: &str| std::env::var(k).unwrap_or_else(|_| d.to_string());
1339 let cfg = LdapConfig {
1340 server_url: url,
1341 bind_dn: env("HELIOS_LDAP_BIND_DN", ""),
1342 bind_password: env("HELIOS_LDAP_BIND_PW", ""),
1343 user_search_base: env("HELIOS_LDAP_BASE", "ou=users,dc=example,dc=org"),
1344 user_filter: env("HELIOS_LDAP_FILTER", "(uid={0})"),
1345 group_search_base: None,
1346 group_attribute: "memberOf".to_string(),
1347 timeout: Duration::from_secs(5),
1348 starttls: false,
1349 };
1350 let user = env("HELIOS_LDAP_USER", "alice");
1351 let pass = env("HELIOS_LDAP_PASS", "alicepw");
1352
1353 let handler = AuthenticationHandlerBuilder::new()
1354 .enabled(true)
1355 .with_ldap(cfg)
1356 .build();
1357
1358 let ok_req = AuthRequest::new()
1360 .with_username(user.clone())
1361 .with_password(pass.clone());
1362 let result = handler
1363 .authenticate_ldap(&ok_req)
1364 .await
1365 .expect("valid LDAP credentials must authenticate");
1366 assert_eq!(result.identity.user_id, user);
1367 assert_eq!(result.identity.auth_method, "ldap");
1368
1369 let bad_pw = AuthRequest::new()
1371 .with_username(user.clone())
1372 .with_password("definitely-wrong");
1373 assert!(
1374 matches!(
1375 handler.authenticate_ldap(&bad_pw).await,
1376 Err(AuthError::InvalidCredentials)
1377 ),
1378 "wrong password must be denied"
1379 );
1380
1381 let unknown = AuthRequest::new()
1383 .with_username("nosuchuser")
1384 .with_password("whatever");
1385 assert!(
1386 matches!(
1387 handler.authenticate_ldap(&unknown).await,
1388 Err(AuthError::InvalidCredentials)
1389 ),
1390 "unknown user must be denied"
1391 );
1392 }
1393}