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.user_filter.replace("{0}", &ldap3::ldap_escape(username));
690 let (entries, _res) = ldap
691 .search(
692 &cfg.user_search_base,
693 Scope::Subtree,
694 &filter,
695 vec![cfg.group_attribute.as_str()],
696 )
697 .await
698 .map_err(|e| AuthError::Ldap(format!("search: {}", e)))?
699 .success()
700 .map_err(|e| AuthError::Ldap(format!("search rejected: {}", e)))?;
701
702 let entry = match entries.into_iter().next() {
703 Some(e) => SearchEntry::construct(e),
704 None => {
706 let _ = ldap.unbind().await;
707 return Err(AuthError::InvalidCredentials);
708 }
709 };
710 let user_dn = entry.dn.clone();
711 let groups = entry
712 .attrs
713 .get(&cfg.group_attribute)
714 .cloned()
715 .unwrap_or_default();
716 let _ = ldap.unbind().await;
717
718 if user_dn.is_empty() {
719 return Err(AuthError::InvalidCredentials);
720 }
721 if password.is_empty() {
724 return Err(AuthError::InvalidCredentials);
725 }
726
727 let (conn2, mut ldap2) = LdapConnAsync::with_settings(settings, &cfg.server_url)
729 .await
730 .map_err(|e| AuthError::Ldap(format!("connect (user bind): {}", e)))?;
731 ldap3::drive!(conn2);
732 let bind = ldap2
733 .simple_bind(&user_dn, password)
734 .await
735 .map_err(|e| AuthError::Ldap(format!("user bind: {}", e)))?;
736 let _ = ldap2.unbind().await;
737 bind.success().map_err(|_| AuthError::InvalidCredentials)?;
738
739 Ok(groups)
740 }
741
742 async fn authenticate_api_key(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
744 let api_key_config = self
745 .config
746 .api_keys
747 .as_ref()
748 .ok_or(AuthError::Configuration(
749 "API keys not configured".to_string(),
750 ))?;
751
752 let header_name = &api_key_config.header_name;
753 let key = request
754 .api_key(header_name)
755 .ok_or(AuthError::AuthenticationRequired)?;
756
757 if let Some(cached) = self.auth_cache.read().get(key) {
759 return Ok(cached.clone());
760 }
761
762 let api_keys = self.api_keys.read();
764 let entry = api_keys
765 .values()
766 .find(|e| self.verify_api_key(key, &e.key_hash) && e.active)
767 .ok_or(AuthError::InvalidCredentials)?;
768
769 if let Some(expires_at) = entry.expires_at {
771 if chrono::Utc::now() > expires_at {
772 return Err(AuthError::TokenExpired);
773 }
774 }
775
776 let result = AuthResult::new(entry.identity.clone());
777 self.auth_cache
778 .write()
779 .insert(key.to_string(), result.clone());
780
781 Ok(result)
782 }
783
784 async fn authenticate_basic(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
786 let auth_header = request
787 .authorization_header()
788 .ok_or(AuthError::AuthenticationRequired)?;
789
790 if !auth_header.starts_with("Basic ") {
791 return Err(AuthError::AuthenticationRequired);
792 }
793
794 let encoded = &auth_header[6..];
795 let decoded = base64_decode(encoded).map_err(|_| AuthError::InvalidCredentials)?;
796 let credentials = String::from_utf8(decoded).map_err(|_| AuthError::InvalidCredentials)?;
797
798 let parts: Vec<&str> = credentials.splitn(2, ':').collect();
799 if parts.len() != 2 {
800 return Err(AuthError::InvalidCredentials);
801 }
802
803 let username = parts[0];
804 let password = parts[1];
805
806 let _ = (username, password);
809 Err(AuthError::InvalidCredentials)
810 }
811
812 fn authenticate_trust(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
814 let username = request
816 .username
817 .as_ref()
818 .unwrap_or(&"anonymous".to_string())
819 .clone();
820
821 let identity = Identity {
822 user_id: username.clone(),
823 name: Some(username),
824 email: None,
825 roles: vec!["trusted".to_string()],
826 groups: Vec::new(),
827 tenant_id: None,
828 claims: HashMap::new(),
829 auth_method: "trust".to_string(),
830 authenticated_at: chrono::Utc::now(),
831 };
832
833 Ok(AuthResult::new(identity))
834 }
835
836 fn check_rate_limit(&self, request: &AuthRequest) -> Result<(), AuthError> {
838 let config = &self.config.rate_limit;
839 if !config.enabled {
840 return Ok(());
841 }
842
843 let mut limiter = self.rate_limiter.write();
844
845 if limiter.last_cleanup.elapsed() > Duration::from_secs(60) {
847 let window = Duration::from_secs(config.window_seconds);
848 limiter
849 .by_ip
850 .retain(|_, b| b.window_start.elapsed() < window);
851 limiter
852 .by_user
853 .retain(|_, b| b.window_start.elapsed() < window);
854 limiter.last_cleanup = Instant::now();
855 }
856
857 let window = Duration::from_secs(config.window_seconds);
858
859 if let Some(ip) = request.client_ip {
861 let bucket = limiter.by_ip.entry(ip).or_insert_with(RateLimitBucket::new);
862 let count = bucket.increment(window);
863 if count > config.max_requests_per_ip {
864 let retry_after = window
865 .as_secs()
866 .saturating_sub(bucket.window_start.elapsed().as_secs());
867 return Err(AuthError::RateLimited(retry_after));
868 }
869 }
870
871 if let Some(username) = &request.username {
873 let bucket = limiter
874 .by_user
875 .entry(username.clone())
876 .or_insert_with(RateLimitBucket::new);
877 let count = bucket.increment(window);
878 if count > config.max_requests_per_user {
879 let retry_after = window
880 .as_secs()
881 .saturating_sub(bucket.window_start.elapsed().as_secs());
882 return Err(AuthError::RateLimited(retry_after));
883 }
884 }
885
886 Ok(())
887 }
888
889 fn verify_api_key(&self, key: &str, hash: &str) -> bool {
893 let computed = self.hash_api_key(key);
894 let a = computed.as_bytes();
895 let b = hash.as_bytes();
896 if a.len() != b.len() {
897 return false;
898 }
899 let mut diff = 0u8;
900 for (x, y) in a.iter().zip(b.iter()) {
901 diff |= x ^ y;
902 }
903 diff == 0
904 }
905
906 fn hash_api_key(&self, key: &str) -> String {
910 use sha2::{Digest, Sha256};
911 let digest = Sha256::digest(key.as_bytes());
912 let mut out = String::with_capacity(64);
913 for b in digest {
914 use std::fmt::Write;
915 let _ = write!(out, "{:02x}", b);
916 }
917 out
918 }
919
920 pub fn register_api_key(
922 &self,
923 key_id: String,
924 key_value: String,
925 identity: Identity,
926 expires_at: Option<chrono::DateTime<chrono::Utc>>,
927 scopes: Vec<String>,
928 ) {
929 let entry = ApiKeyEntry {
930 key_id: key_id.clone(),
931 key_hash: self.hash_api_key(&key_value),
932 identity,
933 created_at: chrono::Utc::now(),
934 expires_at,
935 active: true,
936 scopes,
937 rate_limit: None,
938 };
939
940 self.api_keys.write().insert(key_id, entry);
941 }
942
943 pub fn revoke_api_key(&self, key_id: &str) -> bool {
945 if let Some(entry) = self.api_keys.write().get_mut(key_id) {
946 entry.active = false;
947 true
948 } else {
949 false
950 }
951 }
952
953 pub async fn refresh_jwks_if_needed(&self) -> Result<(), AuthError> {
955 if let Some(validator) = &self.jwt_validator {
956 if validator.needs_refresh() {
957 validator.refresh_jwks().await?;
958 }
959 }
960 Ok(())
961 }
962
963 pub fn clear_cache(&self) {
965 self.auth_cache.write().entries.clear();
966 }
967
968 pub fn cache_stats(&self) -> CacheStats {
970 let cache = self.auth_cache.read();
971 CacheStats {
972 entries: cache.entries.len(),
973 max_size: cache.max_size,
974 ttl_seconds: cache.ttl.as_secs(),
975 }
976 }
977
978 pub fn is_enabled(&self) -> bool {
980 self.config.enabled
981 }
982}
983
984#[derive(Debug, Clone)]
986pub struct CacheStats {
987 pub entries: usize,
989
990 pub max_size: usize,
992
993 pub ttl_seconds: u64,
995}
996
997pub struct AuthenticationHandlerBuilder {
999 config: AuthConfig,
1000}
1001
1002impl AuthenticationHandlerBuilder {
1003 pub fn new() -> Self {
1005 Self {
1006 config: AuthConfig::default(),
1007 }
1008 }
1009
1010 pub fn enabled(mut self, enabled: bool) -> Self {
1012 self.config.enabled = enabled;
1013 self
1014 }
1015
1016 pub fn with_jwt(mut self, config: JwtConfig) -> Self {
1018 self.config.jwt = Some(config);
1019 self.config.auth_methods.push(AuthMethod::Jwt);
1020 self
1021 }
1022
1023 pub fn with_oauth(mut self, config: OAuthConfig) -> Self {
1025 self.config.oauth = Some(config);
1026 self.config.auth_methods.push(AuthMethod::OAuth);
1027 self
1028 }
1029
1030 pub fn with_ldap(mut self, config: LdapConfig) -> Self {
1032 self.config.ldap = Some(config);
1033 self.config.auth_methods.push(AuthMethod::Ldap);
1034 self
1035 }
1036
1037 pub fn with_api_keys(mut self, config: ApiKeyConfig) -> Self {
1039 self.config.api_keys = Some(config);
1040 self.config.auth_methods.push(AuthMethod::ApiKey);
1041 self
1042 }
1043
1044 pub fn with_basic_auth(mut self) -> Self {
1046 self.config.auth_methods.push(AuthMethod::Basic);
1047 self
1048 }
1049
1050 pub fn with_trust_auth(mut self) -> Self {
1052 self.config.auth_methods.push(AuthMethod::Trust);
1053 self
1054 }
1055
1056 pub fn default_role(mut self, role: impl Into<String>) -> Self {
1058 self.config.default_role = Some(role.into());
1059 self
1060 }
1061
1062 pub fn build(self) -> AuthenticationHandler {
1064 AuthenticationHandler::new(self.config)
1065 }
1066}
1067
1068impl Default for AuthenticationHandlerBuilder {
1069 fn default() -> Self {
1070 Self::new()
1071 }
1072}
1073
1074fn base64_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1076 use base64::{engine::general_purpose::STANDARD, Engine};
1077 STANDARD.decode(input)
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083
1084 fn test_config() -> AuthConfig {
1085 let mut config = AuthConfig::default();
1086 config.enabled = true;
1087 config.auth_methods = vec![AuthMethod::Trust];
1088 config
1089 }
1090
1091 #[tokio::test]
1092 async fn test_authentication_disabled() {
1093 let mut config = AuthConfig::default();
1094 config.enabled = false;
1095 let handler = AuthenticationHandler::new(config);
1096
1097 let request = AuthRequest::new();
1098 let result = handler.authenticate(&request).await.unwrap();
1099
1100 assert_eq!(result.identity.auth_method, "anonymous");
1101 }
1102
1103 #[tokio::test]
1104 async fn test_trust_authentication() {
1105 let handler = AuthenticationHandler::new(test_config());
1106
1107 let request = AuthRequest::new().with_username("testuser");
1108 let result = handler.authenticate(&request).await.unwrap();
1109
1110 assert_eq!(result.identity.user_id, "testuser");
1111 assert_eq!(result.identity.auth_method, "trust");
1112 }
1113
1114 #[test]
1115 fn test_auth_request_builder() {
1116 let request = AuthRequest::new()
1117 .with_username("user")
1118 .with_password("pass")
1119 .with_database("mydb")
1120 .with_header("Authorization", "Bearer token123");
1121
1122 assert_eq!(request.username, Some("user".to_string()));
1123 assert_eq!(request.password, Some("pass".to_string()));
1124 assert_eq!(request.database, Some("mydb".to_string()));
1125 assert_eq!(request.bearer_token(), Some("token123"));
1126 }
1127
1128 #[test]
1129 fn test_bearer_token_extraction() {
1130 let request = AuthRequest::new().with_header("Authorization", "Bearer my-jwt-token");
1131
1132 assert_eq!(request.bearer_token(), Some("my-jwt-token"));
1133 }
1134
1135 #[test]
1136 fn test_api_key_extraction() {
1137 let request = AuthRequest::new().with_header("X-API-Key", "secret-key-123");
1138
1139 assert_eq!(request.api_key("X-API-Key"), Some("secret-key-123"));
1140 }
1141
1142 #[tokio::test]
1143 async fn test_api_key_registration_and_auth() {
1144 let mut config = AuthConfig::default();
1145 config.enabled = true;
1146 config.api_keys = Some(ApiKeyConfig {
1147 header_name: "X-API-Key".to_string(),
1148 query_param: None,
1149 prefix: None,
1150 hash_algorithm: "sha256".to_string(),
1151 });
1152 config.auth_methods = vec![AuthMethod::ApiKey];
1153
1154 let handler = AuthenticationHandler::new(config);
1155
1156 let identity = Identity {
1158 user_id: "api_user".to_string(),
1159 name: Some("API User".to_string()),
1160 email: None,
1161 roles: vec!["api".to_string()],
1162 groups: Vec::new(),
1163 tenant_id: None,
1164 claims: HashMap::new(),
1165 auth_method: "api_key".to_string(),
1166 authenticated_at: chrono::Utc::now(),
1167 };
1168
1169 handler.register_api_key(
1170 "key1".to_string(),
1171 "secret123".to_string(),
1172 identity,
1173 None,
1174 vec!["read".to_string()],
1175 );
1176
1177 let request = AuthRequest::new().with_header("X-API-Key", "secret123");
1179
1180 let result = handler.authenticate(&request).await.unwrap();
1181 assert_eq!(result.identity.user_id, "api_user");
1182 }
1183
1184 #[test]
1185 fn test_cache_stats() {
1186 let handler = AuthenticationHandler::new(test_config());
1187 let stats = handler.cache_stats();
1188
1189 assert_eq!(stats.entries, 0);
1190 assert_eq!(stats.max_size, 1000);
1191 }
1192
1193 #[test]
1194 fn test_handler_builder() {
1195 let handler = AuthenticationHandler::builder()
1196 .enabled(true)
1197 .with_trust_auth()
1198 .default_role("user")
1199 .build();
1200
1201 assert!(handler.is_enabled());
1202 }
1203
1204 #[tokio::test]
1205 async fn basic_auth_denies_without_user_store() {
1206 let mut config = AuthConfig::default();
1209 config.enabled = true;
1210 config.auth_methods = vec![AuthMethod::Basic];
1211 let handler = AuthenticationHandler::new(config);
1212
1213 use base64::{engine::general_purpose::STANDARD, Engine};
1214 let creds = STANDARD.encode("alice:any-password");
1215 let request = AuthRequest::new().with_header("Authorization", format!("Basic {creds}"));
1216 let result = handler.authenticate(&request).await;
1217 assert!(
1218 result.is_err(),
1219 "basic auth must deny without a user store, got {result:?}"
1220 );
1221 }
1222
1223 async fn spawn_introspection_mock(body: &'static str) -> String {
1228 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1229 use tokio::net::TcpListener;
1230 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1231 let addr = listener.local_addr().unwrap();
1232 tokio::spawn(async move {
1233 while let Ok((mut sock, _)) = listener.accept().await {
1234 let mut buf = [0u8; 4096];
1236 let _ = sock.read(&mut buf).await;
1237 let resp = format!(
1238 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
1239 Content-Length: {}\r\nConnection: close\r\n\r\n{}",
1240 body.len(),
1241 body
1242 );
1243 let _ = sock.write_all(resp.as_bytes()).await;
1244 let _ = sock.shutdown().await;
1245 }
1246 });
1247 format!("http://{}/introspect", addr)
1248 }
1249
1250 fn oauth_cfg(introspection_url: String, required_scopes: Vec<String>) -> OAuthConfig {
1251 OAuthConfig {
1252 introspection_url,
1253 client_id: "proxy".into(),
1254 client_secret: "secret".into(),
1255 token_url: None,
1256 scopes: Vec::new(),
1257 cache_ttl: std::time::Duration::from_secs(60),
1258 required_scopes,
1259 issuer: String::new(),
1260 authorization_url: None,
1261 audience: None,
1262 }
1263 }
1264
1265 #[tokio::test]
1266 async fn oauth_introspection_accepts_active_token() {
1267 let url = spawn_introspection_mock(
1268 r#"{"active":true,"sub":"alice","username":"alice","scope":"read write"}"#,
1269 )
1270 .await;
1271 let handler = AuthenticationHandlerBuilder::new()
1272 .enabled(true)
1273 .with_oauth(oauth_cfg(url, Vec::new()))
1274 .build();
1275 let req = AuthRequest::new().with_header("Authorization", "Bearer tok-abc");
1276 let result = handler
1277 .authenticate_oauth(&req)
1278 .await
1279 .expect("active token must authenticate");
1280 assert_eq!(result.identity.user_id, "alice");
1281 assert!(result.identity.roles.contains(&"read".to_string()));
1282 assert!(result.identity.roles.contains(&"write".to_string()));
1283 }
1284
1285 #[tokio::test]
1286 async fn oauth_introspection_denies_inactive_token() {
1287 let url = spawn_introspection_mock(r#"{"active":false}"#).await;
1288 let handler = AuthenticationHandlerBuilder::new()
1289 .enabled(true)
1290 .with_oauth(oauth_cfg(url, Vec::new()))
1291 .build();
1292 let req = AuthRequest::new().with_header("Authorization", "Bearer dead");
1293 let err = handler.authenticate_oauth(&req).await.unwrap_err();
1294 assert!(
1295 matches!(err, AuthError::InvalidCredentials),
1296 "inactive token must be denied, got {err:?}"
1297 );
1298 }
1299
1300 #[tokio::test]
1301 async fn oauth_introspection_enforces_required_scopes() {
1302 let url =
1303 spawn_introspection_mock(r#"{"active":true,"sub":"bob","scope":"read"}"#).await;
1304 let handler = AuthenticationHandlerBuilder::new()
1305 .enabled(true)
1306 .with_oauth(oauth_cfg(url, vec!["admin".to_string()]))
1307 .build();
1308 let req = AuthRequest::new().with_header("Authorization", "Bearer tok");
1309 let err = handler.authenticate_oauth(&req).await.unwrap_err();
1310 assert!(
1311 matches!(err, AuthError::InsufficientPermissions(_)),
1312 "missing required scope must deny, got {err:?}"
1313 );
1314 }
1315
1316 #[cfg(feature = "ldap-auth")]
1326 #[tokio::test]
1327 async fn ldap_live_search_and_bind() {
1328 use std::time::Duration;
1329
1330 let url = match std::env::var("HELIOS_LDAP_URL") {
1331 Ok(u) if !u.is_empty() => u,
1332 _ => {
1333 eprintln!("skipping ldap_live_search_and_bind: set HELIOS_LDAP_URL");
1334 return;
1335 }
1336 };
1337 let env = |k: &str, d: &str| std::env::var(k).unwrap_or_else(|_| d.to_string());
1338 let cfg = LdapConfig {
1339 server_url: url,
1340 bind_dn: env("HELIOS_LDAP_BIND_DN", ""),
1341 bind_password: env("HELIOS_LDAP_BIND_PW", ""),
1342 user_search_base: env("HELIOS_LDAP_BASE", "ou=users,dc=example,dc=org"),
1343 user_filter: env("HELIOS_LDAP_FILTER", "(uid={0})"),
1344 group_search_base: None,
1345 group_attribute: "memberOf".to_string(),
1346 timeout: Duration::from_secs(5),
1347 starttls: false,
1348 };
1349 let user = env("HELIOS_LDAP_USER", "alice");
1350 let pass = env("HELIOS_LDAP_PASS", "alicepw");
1351
1352 let handler = AuthenticationHandlerBuilder::new()
1353 .enabled(true)
1354 .with_ldap(cfg)
1355 .build();
1356
1357 let ok_req = AuthRequest::new()
1359 .with_username(user.clone())
1360 .with_password(pass.clone());
1361 let result = handler
1362 .authenticate_ldap(&ok_req)
1363 .await
1364 .expect("valid LDAP credentials must authenticate");
1365 assert_eq!(result.identity.user_id, user);
1366 assert_eq!(result.identity.auth_method, "ldap");
1367
1368 let bad_pw = AuthRequest::new()
1370 .with_username(user.clone())
1371 .with_password("definitely-wrong");
1372 assert!(
1373 matches!(
1374 handler.authenticate_ldap(&bad_pw).await,
1375 Err(AuthError::InvalidCredentials)
1376 ),
1377 "wrong password must be denied"
1378 );
1379
1380 let unknown = AuthRequest::new()
1382 .with_username("nosuchuser")
1383 .with_password("whatever");
1384 assert!(
1385 matches!(
1386 handler.authenticate_ldap(&unknown).await,
1387 Err(AuthError::InvalidCredentials)
1388 ),
1389 "unknown user must be denied"
1390 );
1391 }
1392}