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