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        AuthConfig {
1088            enabled: true,
1089            auth_methods: vec![AuthMethod::Trust],
1090            ..Default::default()
1091        }
1092    }
1093
1094    #[tokio::test]
1095    async fn test_authentication_disabled() {
1096        let config = AuthConfig {
1097            enabled: false,
1098            ..Default::default()
1099        };
1100        let handler = AuthenticationHandler::new(config);
1101
1102        let request = AuthRequest::new();
1103        let result = handler.authenticate(&request).await.unwrap();
1104
1105        assert_eq!(result.identity.auth_method, "anonymous");
1106    }
1107
1108    #[tokio::test]
1109    async fn test_trust_authentication() {
1110        let handler = AuthenticationHandler::new(test_config());
1111
1112        let request = AuthRequest::new().with_username("testuser");
1113        let result = handler.authenticate(&request).await.unwrap();
1114
1115        assert_eq!(result.identity.user_id, "testuser");
1116        assert_eq!(result.identity.auth_method, "trust");
1117    }
1118
1119    #[test]
1120    fn test_auth_request_builder() {
1121        let request = AuthRequest::new()
1122            .with_username("user")
1123            .with_password("pass")
1124            .with_database("mydb")
1125            .with_header("Authorization", "Bearer token123");
1126
1127        assert_eq!(request.username, Some("user".to_string()));
1128        assert_eq!(request.password, Some("pass".to_string()));
1129        assert_eq!(request.database, Some("mydb".to_string()));
1130        assert_eq!(request.bearer_token(), Some("token123"));
1131    }
1132
1133    #[test]
1134    fn test_bearer_token_extraction() {
1135        let request = AuthRequest::new().with_header("Authorization", "Bearer my-jwt-token");
1136
1137        assert_eq!(request.bearer_token(), Some("my-jwt-token"));
1138    }
1139
1140    #[test]
1141    fn test_api_key_extraction() {
1142        let request = AuthRequest::new().with_header("X-API-Key", "secret-key-123");
1143
1144        assert_eq!(request.api_key("X-API-Key"), Some("secret-key-123"));
1145    }
1146
1147    #[tokio::test]
1148    async fn test_api_key_registration_and_auth() {
1149        let config = AuthConfig {
1150            enabled: true,
1151            api_keys: Some(ApiKeyConfig {
1152                header_name: "X-API-Key".to_string(),
1153                query_param: None,
1154                prefix: None,
1155                hash_algorithm: "sha256".to_string(),
1156            }),
1157            auth_methods: vec![AuthMethod::ApiKey],
1158            ..Default::default()
1159        };
1160
1161        let handler = AuthenticationHandler::new(config);
1162
1163        // Register an API key
1164        let identity = Identity {
1165            user_id: "api_user".to_string(),
1166            name: Some("API User".to_string()),
1167            email: None,
1168            roles: vec!["api".to_string()],
1169            groups: Vec::new(),
1170            tenant_id: None,
1171            claims: HashMap::new(),
1172            auth_method: "api_key".to_string(),
1173            authenticated_at: chrono::Utc::now(),
1174        };
1175
1176        handler.register_api_key(
1177            "key1".to_string(),
1178            "secret123".to_string(),
1179            identity,
1180            None,
1181            vec!["read".to_string()],
1182        );
1183
1184        // Authenticate with the key
1185        let request = AuthRequest::new().with_header("X-API-Key", "secret123");
1186
1187        let result = handler.authenticate(&request).await.unwrap();
1188        assert_eq!(result.identity.user_id, "api_user");
1189    }
1190
1191    #[test]
1192    fn test_cache_stats() {
1193        let handler = AuthenticationHandler::new(test_config());
1194        let stats = handler.cache_stats();
1195
1196        assert_eq!(stats.entries, 0);
1197        assert_eq!(stats.max_size, 1000);
1198    }
1199
1200    #[test]
1201    fn test_handler_builder() {
1202        let handler = AuthenticationHandler::builder()
1203            .enabled(true)
1204            .with_trust_auth()
1205            .default_role("user")
1206            .build();
1207
1208        assert!(handler.is_enabled());
1209    }
1210
1211    #[tokio::test]
1212    async fn basic_auth_denies_without_user_store() {
1213        // Hardening: HTTP Basic used to accept ANY non-empty password. With no
1214        // user store wired it must now deny.
1215        let config = AuthConfig {
1216            enabled: true,
1217            auth_methods: vec![AuthMethod::Basic],
1218            ..Default::default()
1219        };
1220        let handler = AuthenticationHandler::new(config);
1221
1222        use base64::{engine::general_purpose::STANDARD, Engine};
1223        let creds = STANDARD.encode("alice:any-password");
1224        let request = AuthRequest::new().with_header("Authorization", format!("Basic {creds}"));
1225        let result = handler.authenticate(&request).await;
1226        assert!(
1227            result.is_err(),
1228            "basic auth must deny without a user store, got {result:?}"
1229        );
1230    }
1231
1232    // --- OAuth RFC 7662 introspection -------------------------------------
1233
1234    /// Minimal HTTP/1.1 server that answers every request with a fixed JSON
1235    /// body — stands in for an OAuth introspection endpoint. Returns its URL.
1236    async fn spawn_introspection_mock(body: &'static str) -> String {
1237        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1238        use tokio::net::TcpListener;
1239        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1240        let addr = listener.local_addr().unwrap();
1241        tokio::spawn(async move {
1242            while let Ok((mut sock, _)) = listener.accept().await {
1243                // Drain (best-effort) the request, then respond.
1244                let mut buf = [0u8; 4096];
1245                let _ = sock.read(&mut buf).await;
1246                let resp = format!(
1247                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
1248                     Content-Length: {}\r\nConnection: close\r\n\r\n{}",
1249                    body.len(),
1250                    body
1251                );
1252                let _ = sock.write_all(resp.as_bytes()).await;
1253                let _ = sock.shutdown().await;
1254            }
1255        });
1256        format!("http://{}/introspect", addr)
1257    }
1258
1259    fn oauth_cfg(introspection_url: String, required_scopes: Vec<String>) -> OAuthConfig {
1260        OAuthConfig {
1261            introspection_url,
1262            client_id: "proxy".into(),
1263            client_secret: "secret".into(),
1264            token_url: None,
1265            scopes: Vec::new(),
1266            cache_ttl: std::time::Duration::from_secs(60),
1267            required_scopes,
1268            issuer: String::new(),
1269            authorization_url: None,
1270            audience: None,
1271        }
1272    }
1273
1274    #[tokio::test]
1275    async fn oauth_introspection_accepts_active_token() {
1276        let url = spawn_introspection_mock(
1277            r#"{"active":true,"sub":"alice","username":"alice","scope":"read write"}"#,
1278        )
1279        .await;
1280        let handler = AuthenticationHandlerBuilder::new()
1281            .enabled(true)
1282            .with_oauth(oauth_cfg(url, Vec::new()))
1283            .build();
1284        let req = AuthRequest::new().with_header("Authorization", "Bearer tok-abc");
1285        let result = handler
1286            .authenticate_oauth(&req)
1287            .await
1288            .expect("active token must authenticate");
1289        assert_eq!(result.identity.user_id, "alice");
1290        assert!(result.identity.roles.contains(&"read".to_string()));
1291        assert!(result.identity.roles.contains(&"write".to_string()));
1292    }
1293
1294    #[tokio::test]
1295    async fn oauth_introspection_denies_inactive_token() {
1296        let url = spawn_introspection_mock(r#"{"active":false}"#).await;
1297        let handler = AuthenticationHandlerBuilder::new()
1298            .enabled(true)
1299            .with_oauth(oauth_cfg(url, Vec::new()))
1300            .build();
1301        let req = AuthRequest::new().with_header("Authorization", "Bearer dead");
1302        let err = handler.authenticate_oauth(&req).await.unwrap_err();
1303        assert!(
1304            matches!(err, AuthError::InvalidCredentials),
1305            "inactive token must be denied, got {err:?}"
1306        );
1307    }
1308
1309    #[tokio::test]
1310    async fn oauth_introspection_enforces_required_scopes() {
1311        let url = spawn_introspection_mock(r#"{"active":true,"sub":"bob","scope":"read"}"#).await;
1312        let handler = AuthenticationHandlerBuilder::new()
1313            .enabled(true)
1314            .with_oauth(oauth_cfg(url, vec!["admin".to_string()]))
1315            .build();
1316        let req = AuthRequest::new().with_header("Authorization", "Bearer tok");
1317        let err = handler.authenticate_oauth(&req).await.unwrap_err();
1318        assert!(
1319            matches!(err, AuthError::InsufficientPermissions(_)),
1320            "missing required scope must deny, got {err:?}"
1321        );
1322    }
1323
1324    // --- LDAP search + bind (live, against a real directory) --------------
1325
1326    /// Live LDAP search-and-bind test against a real directory server. Gated
1327    /// on `HELIOS_LDAP_URL` (e.g. `ldap://127.0.0.1:1389`); skips when unset so
1328    /// CI without a directory stays green. `scripts/regress/ldap-test.sh`
1329    /// stands up an OpenLDAP container, seeds a user, and exports the env.
1330    ///
1331    /// Asserts: the right user+password authenticates and yields an `ldap`
1332    /// identity; a wrong password is denied; an unknown user is denied.
1333    #[cfg(feature = "ldap-auth")]
1334    #[tokio::test]
1335    async fn ldap_live_search_and_bind() {
1336        use std::time::Duration;
1337
1338        let url = match std::env::var("HELIOS_LDAP_URL") {
1339            Ok(u) if !u.is_empty() => u,
1340            _ => {
1341                eprintln!("skipping ldap_live_search_and_bind: set HELIOS_LDAP_URL");
1342                return;
1343            }
1344        };
1345        let env = |k: &str, d: &str| std::env::var(k).unwrap_or_else(|_| d.to_string());
1346        let cfg = LdapConfig {
1347            server_url: url,
1348            bind_dn: env("HELIOS_LDAP_BIND_DN", ""),
1349            bind_password: env("HELIOS_LDAP_BIND_PW", ""),
1350            user_search_base: env("HELIOS_LDAP_BASE", "ou=users,dc=example,dc=org"),
1351            user_filter: env("HELIOS_LDAP_FILTER", "(uid={0})"),
1352            group_search_base: None,
1353            group_attribute: "memberOf".to_string(),
1354            timeout: Duration::from_secs(5),
1355            starttls: false,
1356        };
1357        let user = env("HELIOS_LDAP_USER", "alice");
1358        let pass = env("HELIOS_LDAP_PASS", "alicepw");
1359
1360        let handler = AuthenticationHandlerBuilder::new()
1361            .enabled(true)
1362            .with_ldap(cfg)
1363            .build();
1364
1365        // 1. Correct credentials authenticate.
1366        let ok_req = AuthRequest::new()
1367            .with_username(user.clone())
1368            .with_password(pass.clone());
1369        let result = handler
1370            .authenticate_ldap(&ok_req)
1371            .await
1372            .expect("valid LDAP credentials must authenticate");
1373        assert_eq!(result.identity.user_id, user);
1374        assert_eq!(result.identity.auth_method, "ldap");
1375
1376        // 2. Wrong password is denied.
1377        let bad_pw = AuthRequest::new()
1378            .with_username(user.clone())
1379            .with_password("definitely-wrong");
1380        assert!(
1381            matches!(
1382                handler.authenticate_ldap(&bad_pw).await,
1383                Err(AuthError::InvalidCredentials)
1384            ),
1385            "wrong password must be denied"
1386        );
1387
1388        // 3. Unknown user is denied.
1389        let unknown = AuthRequest::new()
1390            .with_username("nosuchuser")
1391            .with_password("whatever");
1392        assert!(
1393            matches!(
1394                handler.authenticate_ldap(&unknown).await,
1395                Err(AuthError::InvalidCredentials)
1396            ),
1397            "unknown user must be denied"
1398        );
1399    }
1400}