Skip to main content

heliosdb_proxy/auth/
handler.rs

1//! Authentication Handler
2//!
3//! Main entry point for authentication operations. Coordinates between
4//! different authentication providers (JWT, OAuth, LDAP, API keys).
5
6use 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/// Authentication errors
19#[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/// Authentication request context
62#[derive(Debug, Clone)]
63pub struct AuthRequest {
64    /// HTTP headers
65    pub headers: HashMap<String, String>,
66
67    /// Username (from connection)
68    pub username: Option<String>,
69
70    /// Password (from connection)
71    pub password: Option<String>,
72
73    /// Client IP address
74    pub client_ip: Option<std::net::IpAddr>,
75
76    /// Database being accessed
77    pub database: Option<String>,
78
79    /// Request timestamp
80    pub timestamp: chrono::DateTime<chrono::Utc>,
81}
82
83impl AuthRequest {
84    /// Create a new authentication request
85    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    /// Set username
97    pub fn with_username(mut self, username: impl Into<String>) -> Self {
98        self.username = Some(username.into());
99        self
100    }
101
102    /// Set password
103    pub fn with_password(mut self, password: impl Into<String>) -> Self {
104        self.password = Some(password.into());
105        self
106    }
107
108    /// Set client IP
109    pub fn with_client_ip(mut self, ip: std::net::IpAddr) -> Self {
110        self.client_ip = Some(ip);
111        self
112    }
113
114    /// Set database
115    pub fn with_database(mut self, database: impl Into<String>) -> Self {
116        self.database = Some(database.into());
117        self
118    }
119
120    /// Add header
121    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    /// Get Authorization header
127    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    /// Get Bearer token from Authorization header
135    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    /// Get API key from header
142    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/// Authentication result
157#[derive(Debug, Clone)]
158pub struct AuthResult {
159    /// Authenticated identity
160    pub identity: Identity,
161
162    /// Session token (if created)
163    pub session_token: Option<String>,
164
165    /// Token expiration time
166    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
167
168    /// Additional metadata
169    pub metadata: HashMap<String, String>,
170}
171
172impl AuthResult {
173    /// Create a new authentication result
174    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    /// Set session token
184    pub fn with_session_token(mut self, token: String) -> Self {
185        self.session_token = Some(token);
186        self
187    }
188
189    /// Set expiration
190    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    /// Add metadata
196    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
202/// Main authentication handler
203pub struct AuthenticationHandler {
204    /// Configuration
205    config: AuthConfig,
206
207    /// JWT validator
208    jwt_validator: Option<JwtValidator>,
209
210    /// Whether OAuth is configured (RFC 7662 introspection is performed
211    /// per-request against `config.oauth.introspection_url`).
212    oauth_enabled: bool,
213
214    /// Whether LDAP is configured. With the `ldap-auth` feature,
215    /// `authenticate_ldap` performs a real search + bind against the directory;
216    /// without it, LDAP denies by default.
217    ldap_enabled: bool,
218
219    /// API key store
220    api_keys: Arc<RwLock<HashMap<String, ApiKeyEntry>>>,
221
222    /// Rate limiter state
223    rate_limiter: Arc<RwLock<RateLimiterState>>,
224
225    /// Authentication cache
226    auth_cache: Arc<RwLock<AuthCache>>,
227}
228
229/// API key entry
230#[derive(Debug, Clone)]
231struct ApiKeyEntry {
232    /// Key ID
233    #[allow(dead_code)]
234    key_id: String,
235
236    /// Hashed key value
237    key_hash: String,
238
239    /// Associated identity
240    identity: Identity,
241
242    /// Creation time
243    #[allow(dead_code)]
244    created_at: chrono::DateTime<chrono::Utc>,
245
246    /// Expiration time
247    expires_at: Option<chrono::DateTime<chrono::Utc>>,
248
249    /// Whether key is active
250    active: bool,
251
252    /// Allowed scopes
253    #[allow(dead_code)]
254    scopes: Vec<String>,
255
256    /// Rate limit override
257    #[allow(dead_code)]
258    rate_limit: Option<u32>,
259}
260
261/// Rate limiter state
262struct RateLimiterState {
263    /// Request counts by IP
264    by_ip: HashMap<std::net::IpAddr, RateLimitBucket>,
265
266    /// Request counts by user
267    by_user: HashMap<String, RateLimitBucket>,
268
269    /// Last cleanup time
270    last_cleanup: Instant,
271}
272
273/// Rate limit bucket
274struct RateLimitBucket {
275    /// Request count
276    count: u32,
277
278    /// Window start time
279    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
301/// Authentication cache
302struct AuthCache {
303    /// Cached results by token/key
304    entries: HashMap<String, CachedAuth>,
305
306    /// Max cache size
307    max_size: usize,
308
309    /// TTL for cached entries
310    ttl: Duration,
311}
312
313/// Cached authentication result
314struct CachedAuth {
315    /// Result
316    result: AuthResult,
317
318    /// Cached at
319    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    /// Create a new authentication handler
362    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    /// Create a builder for the handler
387    pub fn builder() -> AuthenticationHandlerBuilder {
388        AuthenticationHandlerBuilder::new()
389    }
390
391    /// Authenticate a request
392    pub async fn authenticate(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
393        // Check if authentication is enabled
394        if !self.config.enabled {
395            // Return anonymous identity
396            return Ok(AuthResult::new(Identity::anonymous()));
397        }
398
399        // Check rate limit
400        self.check_rate_limit(request)?;
401
402        // Try each authentication method in order
403        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        // No method succeeded
414        Err(AuthError::AuthenticationRequired)
415    }
416
417    /// Try a specific authentication method
418    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    /// Authenticate using JWT
437    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        // Check cache first
448        if let Some(cached) = self.auth_cache.read().get(token) {
449            return Ok(cached.clone());
450        }
451
452        // Validate token
453        let identity = validator.validate_to_identity(token)?;
454        let result = AuthResult::new(identity);
455
456        // Cache result
457        self.auth_cache
458            .write()
459            .insert(token.to_string(), result.clone());
460
461        Ok(result)
462    }
463
464    /// Authenticate using OAuth token introspection
465    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        // Check cache first
475        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        // RFC 7662 token introspection: POST the token to the introspection
486        // endpoint with the client's credentials and trust only an explicit
487        // `"active": true`. A bearer token is never accepted without this
488        // round-trip.
489        let body = self.oauth_introspect(cfg, token).await?;
490
491        // `active` is REQUIRED by RFC 7662; anything but true is a denial.
492        if body.get("active").and_then(|v| v.as_bool()) != Some(true) {
493            return Err(AuthError::InvalidCredentials);
494        }
495
496        // Optional issuer pin.
497        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        // Optional audience check (`aud` may be a string or an array).
506        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        // Scopes are a space-delimited string (RFC 7662 §2.2). Enforce any
520        // required scopes before minting an identity.
521        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        // Subject identifies the principal; fall back to `username`.
536        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        // Cache the validated result (bounded by the cache's own TTL).
561        self.auth_cache
562            .write()
563            .insert(token.to_string(), result.clone());
564
565        Ok(result)
566    }
567
568    /// Perform an RFC 7662 introspection POST and return the parsed JSON
569    /// response body. Client authentication is HTTP Basic with the
570    /// configured client id/secret.
571    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    /// Authenticate using LDAP
602    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        // Without the `ldap-auth` feature there is no LDAP client compiled in;
630        // deny rather than accept any password (never a fabricated success).
631        #[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    /// Authenticate a user against an LDAP directory using the standard
641    /// search-then-bind flow:
642    ///
643    /// 1. Bind as the configured service account (anonymous if `bind_dn` is
644    ///    empty) and search `user_search_base` with `user_filter` (with `{0}`
645    ///    replaced by the RFC 4515-escaped username) to resolve the user's DN.
646    /// 2. Open a fresh connection and bind as that DN with the supplied
647    ///    password — a successful bind is proof the credentials are valid.
648    ///
649    /// Returns the user's group memberships (values of `group_attribute`).
650    /// An unknown user, an empty password (which would be an unauthenticated
651    /// bind per RFC 4513), or a failed user bind all map to
652    /// [`AuthError::InvalidCredentials`].
653    #[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        // ldap3's rustls 0.23 backend builds its TLS connector eagerly (even
663        // for plain ldap://) and needs a process-default crypto provider, or
664        // the connection driver dies with "channel closed". Install the ring
665        // provider once; ignore the error if another component already set one.
666        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        // --- Phase 1: service bind + search for the user DN -----------------
676        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            // No such user — deny without leaking which half failed.
707            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        // Reject empty passwords up front: an empty password is an
724        // "unauthenticated bind" that some servers report as success.
725        if password.is_empty() {
726            return Err(AuthError::InvalidCredentials);
727        }
728
729        // --- Phase 2: bind AS the user to verify the password ---------------
730        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    /// Authenticate using API key
745    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        // Check cache first
760        if let Some(cached) = self.auth_cache.read().get(key) {
761            return Ok(cached.clone());
762        }
763
764        // Validate API key
765        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        // Check expiration
772        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    /// Authenticate using HTTP Basic auth
787    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        // No user store is wired, so HTTP Basic cannot verify a password.
809        // Deny rather than accept any non-empty password.
810        let _ = (username, password);
811        Err(AuthError::InvalidCredentials)
812    }
813
814    /// Trust-based authentication (e.g., for internal services)
815    fn authenticate_trust(&self, request: &AuthRequest) -> Result<AuthResult, AuthError> {
816        // Trust authentication based on username or other context
817        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    /// Check rate limit
839    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        // Cleanup old entries periodically
848        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        // Check IP rate limit
862        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        // Check user rate limit
874        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    /// Verify an API key against its stored hash using a constant-time
892    /// comparison (so verification time doesn't leak how much of the hash
893    /// matched).
894    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    /// Hash an API key with SHA-256 (hex). Keys are high-entropy secrets, so a
909    /// fast cryptographic digest is appropriate (unlike user passwords, which
910    /// would warrant a slow KDF).
911    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    /// Register an API key
923    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    /// Revoke an API key
946    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    /// Refresh JWKS if needed
956    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    /// Clear authentication cache
966    pub fn clear_cache(&self) {
967        self.auth_cache.write().entries.clear();
968    }
969
970    /// Get cache statistics
971    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    /// Check if authentication is enabled
981    pub fn is_enabled(&self) -> bool {
982        self.config.enabled
983    }
984}
985
986/// Cache statistics
987#[derive(Debug, Clone)]
988pub struct CacheStats {
989    /// Number of cached entries
990    pub entries: usize,
991
992    /// Maximum cache size
993    pub max_size: usize,
994
995    /// TTL in seconds
996    pub ttl_seconds: u64,
997}
998
999/// Builder for AuthenticationHandler
1000pub struct AuthenticationHandlerBuilder {
1001    config: AuthConfig,
1002}
1003
1004impl AuthenticationHandlerBuilder {
1005    /// Create a new builder
1006    pub fn new() -> Self {
1007        Self {
1008            config: AuthConfig::default(),
1009        }
1010    }
1011
1012    /// Enable authentication
1013    pub fn enabled(mut self, enabled: bool) -> Self {
1014        self.config.enabled = enabled;
1015        self
1016    }
1017
1018    /// Configure JWT authentication
1019    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    /// Configure OAuth authentication
1026    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    /// Configure LDAP authentication
1033    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    /// Configure API key authentication
1040    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    /// Enable basic authentication
1047    pub fn with_basic_auth(mut self) -> Self {
1048        self.config.auth_methods.push(AuthMethod::Basic);
1049        self
1050    }
1051
1052    /// Enable trust authentication
1053    pub fn with_trust_auth(mut self) -> Self {
1054        self.config.auth_methods.push(AuthMethod::Trust);
1055        self
1056    }
1057
1058    /// Set default role
1059    pub fn default_role(mut self, role: impl Into<String>) -> Self {
1060        self.config.default_role = Some(role.into());
1061        self
1062    }
1063
1064    /// Build the handler
1065    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
1076/// Base64 decode helper
1077fn 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        // Register an API key
1159        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        // Authenticate with the key
1180        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        // Hardening: HTTP Basic used to accept ANY non-empty password. With no
1209        // user store wired it must now deny.
1210        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    // --- OAuth RFC 7662 introspection -------------------------------------
1226
1227    /// Minimal HTTP/1.1 server that answers every request with a fixed JSON
1228    /// body — stands in for an OAuth introspection endpoint. Returns its URL.
1229    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                // Drain (best-effort) the request, then respond.
1237                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    // --- LDAP search + bind (live, against a real directory) --------------
1318
1319    /// Live LDAP search-and-bind test against a real directory server. Gated
1320    /// on `HELIOS_LDAP_URL` (e.g. `ldap://127.0.0.1:1389`); skips when unset so
1321    /// CI without a directory stays green. `scripts/regress/ldap-test.sh`
1322    /// stands up an OpenLDAP container, seeds a user, and exports the env.
1323    ///
1324    /// Asserts: the right user+password authenticates and yields an `ldap`
1325    /// identity; a wrong password is denied; an unknown user is denied.
1326    #[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        // 1. Correct credentials authenticate.
1359        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        // 2. Wrong password is denied.
1370        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        // 3. Unknown user is denied.
1382        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}