Skip to main content

rivven_core/
service_auth.rs

1//! Service-to-Service Authentication for Rivven
2//!
3//! This module provides authentication mechanisms for service accounts,
4//! specifically designed for `rivven-connect` authenticating to `rivvend`.
5//!
6//! ## Supported Methods
7//!
8//! 1. **API Keys** - Simple, rotatable tokens for service authentication
9//! 2. **mTLS** - Mutual TLS with certificate-based identity
10//! 3. **OIDC Client Credentials** - OAuth2 client credentials flow
11//! 4. **SASL/SCRAM** - Password-based with strong hashing
12//!
13//! ## Security Design
14//!
15//! ```text
16//! ┌─────────────────────────────────────────────────────────────────────────┐
17//! │                    SERVICE AUTHENTICATION FLOW                          │
18//! ├─────────────────────────────────────────────────────────────────────────┤
19//! │                                                                         │
20//! │  ┌──────────────┐                      ┌──────────────────┐             │
21//! │  │rivven-connect│                      │     rivvend      │             │
22//! │  └──────┬───────┘                      └────────┬─────────┘             │
23//! │         │                                       │                       │
24//! │         │  1. TLS Handshake (optional mTLS)     │                       │
25//! │         │◄─────────────────────────────────────►│                       │
26//! │         │                                       │                       │
27//! │         │  2. ServiceAuth { method, credentials }                       │
28//! │         │──────────────────────────────────────►│                       │
29//! │         │                                       │                       │
30//! │         │  3. Validate credentials              │                       │
31//! │         │                                       │ ← Check API key/cert  │
32//! │         │                                       │   Extract identity    │
33//! │         │                                       │   Create session      │
34//! │         │                                       │                       │
35//! │         │  4. ServiceAuthResult { session_id, permissions }             │
36//! │         │◄──────────────────────────────────────│                       │
37//! │         │                                       │                       │
38//! │         │  5. Authenticated requests            │                       │
39//! │         │──────────────────────────────────────►│                       │
40//! │         │                                       │                       │
41//! └─────────────────────────────────────────────────────────────────────────┘
42//! ```
43//!
44//! ## API Key Format
45//!
46//! API keys use a structured format for security and usability:
47//!
48//! ```text
49//! rvn.<version>.<key_id>.<secret>
50//!
51//! Example: rvn.v1.a1b2c3d4e5f6.MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI
52//!          ^^^ ^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53//!           │   │       │            │
54//!           │   │       │            └─ 32-byte random secret (base64)
55//!           │   │       └────────────── Key identifier (hex, for rotation)
56//!           │   └────────────────────── Version (for format changes)
57//!           └────────────────────────── Prefix (identifies Rivven keys)
58//! ```
59
60use parking_lot::RwLock;
61use ring::rand::{SecureRandom, SystemRandom};
62use serde::{Deserialize, Serialize};
63use sha2::{Digest, Sha256};
64use std::collections::HashMap;
65use std::time::{Duration, Instant, SystemTime};
66use thiserror::Error;
67use tracing::{debug, info, warn};
68
69// ============================================================================
70// Error Types
71// ============================================================================
72
73#[derive(Error, Debug)]
74pub enum ServiceAuthError {
75    #[error("Invalid API key format")]
76    InvalidKeyFormat,
77
78    #[error("API key not found: {0}")]
79    KeyNotFound(String),
80
81    #[error("API key expired")]
82    KeyExpired,
83
84    #[error("API key revoked")]
85    KeyRevoked,
86
87    #[error("Invalid credentials")]
88    InvalidCredentials,
89
90    #[error("Certificate error: {0}")]
91    CertificateError(String),
92
93    #[error("Service account not found: {0}")]
94    ServiceAccountNotFound(String),
95
96    #[error("Service account disabled: {0}")]
97    ServiceAccountDisabled(String),
98
99    #[error("Permission denied: {0}")]
100    PermissionDenied(String),
101
102    #[error("Rate limited: too many authentication attempts")]
103    RateLimited,
104
105    #[error("Internal error: {0}")]
106    Internal(String),
107}
108
109pub type ServiceAuthResult<T> = Result<T, ServiceAuthError>;
110
111// ============================================================================
112// API Key
113// ============================================================================
114
115/// API Key for service authentication
116///
117/// # Security Note
118///
119/// This struct implements a custom Debug that redacts the secret_hash field
120/// to prevent accidental leakage to logs.
121#[derive(Clone, Serialize, Deserialize)]
122pub struct ApiKey {
123    /// Key identifier (public, used for lookup)
124    pub key_id: String,
125
126    /// Hashed secret (never store plaintext)
127    pub secret_hash: String,
128
129    /// Service account this key belongs to
130    pub service_account: String,
131
132    /// Human-readable description
133    pub description: Option<String>,
134
135    /// When the key was created
136    pub created_at: SystemTime,
137
138    /// When the key expires (None = never)
139    pub expires_at: Option<SystemTime>,
140
141    /// When the key was last used
142    pub last_used_at: Option<SystemTime>,
143
144    /// Whether the key is revoked
145    pub revoked: bool,
146
147    /// IP allowlist (empty = all IPs allowed)
148    pub allowed_ips: Vec<String>,
149
150    /// Permissions granted to this key
151    pub permissions: Vec<String>,
152}
153
154impl std::fmt::Debug for ApiKey {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct("ApiKey")
157            .field("key_id", &self.key_id)
158            .field("secret_hash", &"[REDACTED]")
159            .field("service_account", &self.service_account)
160            .field("description", &self.description)
161            .field("created_at", &self.created_at)
162            .field("expires_at", &self.expires_at)
163            .field("last_used_at", &self.last_used_at)
164            .field("revoked", &self.revoked)
165            .field("allowed_ips", &self.allowed_ips)
166            .field("permissions", &self.permissions)
167            .finish()
168    }
169}
170
171impl ApiKey {
172    /// Generate a new API key
173    pub fn generate(
174        service_account: &str,
175        description: Option<&str>,
176        expires_in: Option<Duration>,
177        permissions: Vec<String>,
178    ) -> ServiceAuthResult<(Self, String)> {
179        let rng = SystemRandom::new();
180
181        // Generate key ID (8 bytes = 16 hex chars)
182        let mut key_id_bytes = [0u8; 8];
183        rng.fill(&mut key_id_bytes)
184            .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
185        let key_id = hex::encode(key_id_bytes);
186
187        // Generate secret (32 bytes = high entropy)
188        let mut secret_bytes = [0u8; 32];
189        rng.fill(&mut secret_bytes)
190            .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
191        let secret = base64::Engine::encode(
192            &base64::engine::general_purpose::STANDARD_NO_PAD,
193            secret_bytes,
194        );
195
196        // Hash the secret for storage
197        let secret_hash = Self::hash_secret(&secret);
198
199        // Build the full key string (using . as separator to avoid conflicts with base64)
200        let full_key = format!("rvn.v1.{}.{}", key_id, secret);
201
202        let now = SystemTime::now();
203        let expires_at = expires_in.map(|d| now + d);
204
205        let api_key = Self {
206            key_id,
207            secret_hash,
208            service_account: service_account.to_string(),
209            description: description.map(|s| s.to_string()),
210            created_at: now,
211            expires_at,
212            last_used_at: None,
213            revoked: false,
214            allowed_ips: vec![],
215            permissions,
216        };
217
218        Ok((api_key, full_key))
219    }
220
221    /// Hash a secret for storage
222    fn hash_secret(secret: &str) -> String {
223        let mut hasher = Sha256::new();
224        hasher.update(secret.as_bytes());
225        hex::encode(hasher.finalize())
226    }
227
228    /// Parse an API key string and extract components
229    pub fn parse_key(key: &str) -> ServiceAuthResult<(String, String)> {
230        let parts: Vec<&str> = key.split('.').collect();
231        if parts.len() != 4 || parts[0] != "rvn" || parts[1] != "v1" {
232            return Err(ServiceAuthError::InvalidKeyFormat);
233        }
234
235        let key_id = parts[2].to_string();
236        let secret = parts[3].to_string();
237
238        Ok((key_id, secret))
239    }
240
241    /// Verify a secret against this key
242    pub fn verify_secret(&self, secret: &str) -> bool {
243        let provided_hash = Self::hash_secret(secret);
244        // Constant-time comparison using subtle crate pattern
245        // Compare byte-by-byte with constant time to prevent timing attacks
246        if provided_hash.len() != self.secret_hash.len() {
247            return false;
248        }
249
250        let mut result = 0u8;
251        for (a, b) in provided_hash
252            .as_bytes()
253            .iter()
254            .zip(self.secret_hash.as_bytes())
255        {
256            result |= a ^ b;
257        }
258        result == 0
259    }
260
261    /// Check if key is valid (not expired, not revoked)
262    pub fn is_valid(&self) -> bool {
263        if self.revoked {
264            return false;
265        }
266
267        if let Some(expires_at) = self.expires_at {
268            if SystemTime::now() > expires_at {
269                return false;
270            }
271        }
272
273        true
274    }
275
276    /// Check if IP is allowed
277    pub fn is_ip_allowed(&self, ip: &str) -> bool {
278        if self.allowed_ips.is_empty() {
279            return true;
280        }
281
282        self.allowed_ips.iter().any(|allowed| {
283            // Support CIDR notation
284            if allowed.contains('/') {
285                Self::ip_in_cidr(ip, allowed)
286            } else {
287                allowed == ip
288            }
289        })
290    }
291
292    fn ip_in_cidr(ip: &str, cidr: &str) -> bool {
293        // Simple CIDR check - in production, use a proper IP library
294        let parts: Vec<&str> = cidr.split('/').collect();
295        if parts.len() != 2 {
296            return false;
297        }
298
299        let network = parts[0];
300        let prefix_len: u32 = parts[1].parse().unwrap_or(32);
301
302        // Parse IPs
303        let ip_parts: Vec<u8> = ip.split('.').filter_map(|p| p.parse().ok()).collect();
304        let net_parts: Vec<u8> = network.split('.').filter_map(|p| p.parse().ok()).collect();
305
306        if ip_parts.len() != 4 || net_parts.len() != 4 {
307            return false;
308        }
309
310        let ip_num = u32::from_be_bytes([ip_parts[0], ip_parts[1], ip_parts[2], ip_parts[3]]);
311        let net_num = u32::from_be_bytes([net_parts[0], net_parts[1], net_parts[2], net_parts[3]]);
312
313        let mask = if prefix_len == 0 {
314            0
315        } else {
316            !0u32 << (32 - prefix_len)
317        };
318
319        (ip_num & mask) == (net_num & mask)
320    }
321}
322
323// ============================================================================
324// Service Account
325// ============================================================================
326
327/// A service account represents a non-human identity (like rivven-connect)
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ServiceAccount {
330    /// Unique identifier
331    pub name: String,
332
333    /// Human-readable description
334    pub description: Option<String>,
335
336    /// Whether the account is enabled
337    pub enabled: bool,
338
339    /// When the account was created
340    pub created_at: SystemTime,
341
342    /// Roles assigned to this account
343    pub roles: Vec<String>,
344
345    /// Additional metadata
346    pub metadata: HashMap<String, String>,
347
348    /// mTLS certificate subject (if using cert auth)
349    pub certificate_subject: Option<String>,
350
351    /// OIDC client ID (if using OIDC client credentials)
352    pub oidc_client_id: Option<String>,
353}
354
355impl ServiceAccount {
356    pub fn new(name: impl Into<String>) -> Self {
357        Self {
358            name: name.into(),
359            description: None,
360            enabled: true,
361            created_at: SystemTime::now(),
362            roles: vec![],
363            metadata: HashMap::new(),
364            certificate_subject: None,
365            oidc_client_id: None,
366        }
367    }
368
369    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
370        self.description = Some(desc.into());
371        self
372    }
373
374    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
375        self.roles = roles;
376        self
377    }
378
379    pub fn with_certificate_subject(mut self, subject: impl Into<String>) -> Self {
380        self.certificate_subject = Some(subject.into());
381        self
382    }
383
384    pub fn with_oidc_client_id(mut self, client_id: impl Into<String>) -> Self {
385        self.oidc_client_id = Some(client_id.into());
386        self
387    }
388}
389
390// ============================================================================
391// Service Session
392// ============================================================================
393
394/// A session for an authenticated service
395#[derive(Debug, Clone)]
396pub struct ServiceSession {
397    /// Unique session identifier
398    pub id: String,
399
400    /// Service account name
401    pub service_account: String,
402
403    /// Authentication method used
404    pub auth_method: AuthMethod,
405
406    /// When the session expires (monotonic time for expiration)
407    expires_at: Instant,
408
409    /// When the session was created (wall clock for logging)
410    pub created_timestamp: SystemTime,
411
412    /// Permissions granted in this session
413    pub permissions: Vec<String>,
414
415    /// Client IP that created this session
416    pub client_ip: String,
417
418    /// API key ID (if authenticated via API key)
419    pub api_key_id: Option<String>,
420}
421
422impl ServiceSession {
423    pub fn is_expired(&self) -> bool {
424        Instant::now() > self.expires_at
425    }
426
427    /// Time until expiration
428    pub fn time_until_expiration(&self) -> Duration {
429        self.expires_at.saturating_duration_since(Instant::now())
430    }
431
432    /// Seconds until expiration
433    pub fn expires_in_secs(&self) -> u64 {
434        self.time_until_expiration().as_secs()
435    }
436}
437
438/// Authentication method used
439#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440pub enum AuthMethod {
441    ApiKey,
442    MutualTls,
443    OidcClientCredentials,
444    SaslScram,
445}
446
447// ============================================================================
448// Rate Limiter for Auth Attempts
449// ============================================================================
450
451struct AuthRateLimiter {
452    attempts: HashMap<String, Vec<Instant>>,
453    max_attempts: usize,
454    window: Duration,
455}
456
457impl AuthRateLimiter {
458    fn new(max_attempts: usize, window: Duration) -> Self {
459        Self {
460            attempts: HashMap::new(),
461            max_attempts,
462            window,
463        }
464    }
465
466    fn check_and_record(&mut self, key: &str) -> bool {
467        let now = Instant::now();
468        let cutoff = now - self.window;
469
470        let attempts = self.attempts.entry(key.to_string()).or_default();
471
472        // Remove old attempts
473        attempts.retain(|&t| t > cutoff);
474
475        // Check limit
476        if attempts.len() >= self.max_attempts {
477            return false;
478        }
479
480        // Record this attempt
481        attempts.push(now);
482        true
483    }
484
485    fn clear(&mut self, key: &str) {
486        self.attempts.remove(key);
487    }
488}
489
490// ============================================================================
491// Service Auth Manager
492// ============================================================================
493
494/// Manages service authentication
495pub struct ServiceAuthManager {
496    /// API keys by key_id
497    api_keys: RwLock<HashMap<String, ApiKey>>,
498
499    /// Service accounts by name
500    service_accounts: RwLock<HashMap<String, ServiceAccount>>,
501
502    /// Active sessions
503    sessions: RwLock<HashMap<String, ServiceSession>>,
504
505    /// Rate limiter for auth attempts
506    rate_limiter: RwLock<AuthRateLimiter>,
507
508    /// Session duration
509    session_duration: Duration,
510}
511
512impl ServiceAuthManager {
513    pub fn new() -> Self {
514        Self {
515            api_keys: RwLock::new(HashMap::new()),
516            service_accounts: RwLock::new(HashMap::new()),
517            sessions: RwLock::new(HashMap::new()),
518            rate_limiter: RwLock::new(AuthRateLimiter::new(10, Duration::from_secs(60))),
519            session_duration: Duration::from_secs(3600), // 1 hour default
520        }
521    }
522
523    pub fn with_session_duration(mut self, duration: Duration) -> Self {
524        self.session_duration = duration;
525        self
526    }
527
528    // ========================================================================
529    // Service Account Management
530    // ========================================================================
531
532    /// Create a new service account
533    pub fn create_service_account(&self, account: ServiceAccount) -> ServiceAuthResult<()> {
534        let mut accounts = self.service_accounts.write();
535
536        if accounts.contains_key(&account.name) {
537            return Err(ServiceAuthError::Internal(format!(
538                "Service account '{}' already exists",
539                account.name
540            )));
541        }
542
543        info!("Created service account: {}", account.name);
544        accounts.insert(account.name.clone(), account);
545        Ok(())
546    }
547
548    /// Get a service account
549    pub fn get_service_account(&self, name: &str) -> Option<ServiceAccount> {
550        self.service_accounts.read().get(name).cloned()
551    }
552
553    /// Disable a service account (revokes all sessions)
554    pub fn disable_service_account(&self, name: &str) -> ServiceAuthResult<()> {
555        let mut accounts = self.service_accounts.write();
556
557        let account = accounts
558            .get_mut(name)
559            .ok_or_else(|| ServiceAuthError::ServiceAccountNotFound(name.to_string()))?;
560
561        account.enabled = false;
562
563        // Revoke all sessions for this account
564        let mut sessions = self.sessions.write();
565        sessions.retain(|_, s| s.service_account != name);
566
567        info!("Disabled service account: {}", name);
568        Ok(())
569    }
570
571    // ========================================================================
572    // API Key Management
573    // ========================================================================
574
575    /// Create a new API key for a service account
576    pub fn create_api_key(
577        &self,
578        service_account: &str,
579        description: Option<&str>,
580        expires_in: Option<Duration>,
581        permissions: Vec<String>,
582    ) -> ServiceAuthResult<String> {
583        // Verify service account exists
584        {
585            let accounts = self.service_accounts.read();
586            if !accounts.contains_key(service_account) {
587                return Err(ServiceAuthError::ServiceAccountNotFound(
588                    service_account.to_string(),
589                ));
590            }
591        }
592
593        let (api_key, full_key) =
594            ApiKey::generate(service_account, description, expires_in, permissions)?;
595
596        let key_id = api_key.key_id.clone();
597        self.api_keys.write().insert(key_id.clone(), api_key);
598
599        info!(
600            "Created API key '{}' for service account '{}'",
601            key_id, service_account
602        );
603
604        Ok(full_key)
605    }
606
607    /// Revoke an API key
608    pub fn revoke_api_key(&self, key_id: &str) -> ServiceAuthResult<()> {
609        let mut keys = self.api_keys.write();
610
611        let key = keys
612            .get_mut(key_id)
613            .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.to_string()))?;
614
615        key.revoked = true;
616
617        // Revoke all sessions using this key
618        let mut sessions = self.sessions.write();
619        sessions.retain(|_, s| s.api_key_id.as_deref() != Some(key_id));
620
621        info!("Revoked API key: {}", key_id);
622        Ok(())
623    }
624
625    /// List API keys for a service account (without secrets)
626    pub fn list_api_keys(&self, service_account: &str) -> Vec<ApiKey> {
627        self.api_keys
628            .read()
629            .values()
630            .filter(|k| k.service_account == service_account)
631            .cloned()
632            .collect()
633    }
634
635    // ========================================================================
636    // Authentication
637    // ========================================================================
638
639    /// Authenticate using an API key
640    pub fn authenticate_api_key(
641        &self,
642        key_string: &str,
643        client_ip: &str,
644    ) -> ServiceAuthResult<ServiceSession> {
645        // Rate limit check
646        {
647            let mut limiter = self.rate_limiter.write();
648            if !limiter.check_and_record(client_ip) {
649                warn!("Rate limited auth attempt from {}", client_ip);
650                return Err(ServiceAuthError::RateLimited);
651            }
652        }
653
654        // Parse the key
655        let (key_id, secret) = ApiKey::parse_key(key_string)?;
656
657        // Look up the key
658        let mut keys = self.api_keys.write();
659        let api_key = keys
660            .get_mut(&key_id)
661            .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.clone()))?;
662
663        // Verify the secret
664        if !api_key.verify_secret(&secret) {
665            warn!("Invalid API key secret for key_id={}", key_id);
666            return Err(ServiceAuthError::InvalidCredentials);
667        }
668
669        // Check validity
670        if !api_key.is_valid() {
671            if api_key.revoked {
672                return Err(ServiceAuthError::KeyRevoked);
673            } else {
674                return Err(ServiceAuthError::KeyExpired);
675            }
676        }
677
678        // Check IP allowlist
679        if !api_key.is_ip_allowed(client_ip) {
680            warn!("API key {} used from non-allowed IP {}", key_id, client_ip);
681            return Err(ServiceAuthError::PermissionDenied(
682                "IP not in allowlist".to_string(),
683            ));
684        }
685
686        // Check service account is enabled
687        {
688            let accounts = self.service_accounts.read();
689            let account = accounts.get(&api_key.service_account).ok_or_else(|| {
690                ServiceAuthError::ServiceAccountNotFound(api_key.service_account.clone())
691            })?;
692
693            if !account.enabled {
694                return Err(ServiceAuthError::ServiceAccountDisabled(
695                    api_key.service_account.clone(),
696                ));
697            }
698        }
699
700        // Update last used
701        api_key.last_used_at = Some(SystemTime::now());
702
703        // Create session
704        let session = self.create_session(
705            &api_key.service_account,
706            AuthMethod::ApiKey,
707            client_ip,
708            api_key.permissions.clone(),
709            Some(key_id.clone()),
710        );
711
712        // Clear rate limit on success
713        self.rate_limiter.write().clear(client_ip);
714
715        info!(
716            "Authenticated service '{}' via API key '{}' from {}",
717            api_key.service_account, key_id, client_ip
718        );
719
720        Ok(session)
721    }
722
723    /// Authenticate using mTLS certificate
724    pub fn authenticate_certificate(
725        &self,
726        cert_subject: &str,
727        client_ip: &str,
728    ) -> ServiceAuthResult<ServiceSession> {
729        // Find service account by certificate subject
730        let accounts = self.service_accounts.read();
731        let account = accounts
732            .values()
733            .find(|a| a.certificate_subject.as_deref() == Some(cert_subject))
734            .ok_or_else(|| {
735                ServiceAuthError::CertificateError(format!(
736                    "No service account for certificate: {}",
737                    cert_subject
738                ))
739            })?;
740
741        if !account.enabled {
742            return Err(ServiceAuthError::ServiceAccountDisabled(
743                account.name.clone(),
744            ));
745        }
746
747        // Create session with roles from account
748        let permissions = account.roles.clone();
749        let session = self.create_session(
750            &account.name,
751            AuthMethod::MutualTls,
752            client_ip,
753            permissions,
754            None,
755        );
756
757        info!(
758            "Authenticated service '{}' via mTLS certificate from {}",
759            account.name, client_ip
760        );
761
762        Ok(session)
763    }
764
765    /// Create a new session
766    fn create_session(
767        &self,
768        service_account: &str,
769        auth_method: AuthMethod,
770        client_ip: &str,
771        permissions: Vec<String>,
772        api_key_id: Option<String>,
773    ) -> ServiceSession {
774        let rng = SystemRandom::new();
775        let mut session_id_bytes = [0u8; 16];
776        rng.fill(&mut session_id_bytes).expect("RNG failed");
777        let session_id = hex::encode(session_id_bytes);
778
779        let now = Instant::now();
780        let session = ServiceSession {
781            id: session_id.clone(),
782            service_account: service_account.to_string(),
783            auth_method,
784            expires_at: now + self.session_duration,
785            created_timestamp: SystemTime::now(),
786            permissions,
787            client_ip: client_ip.to_string(),
788            api_key_id,
789        };
790
791        self.sessions.write().insert(session_id, session.clone());
792        session
793    }
794
795    /// Validate a session
796    pub fn validate_session(&self, session_id: &str) -> Option<ServiceSession> {
797        let sessions = self.sessions.read();
798        let session = sessions.get(session_id)?;
799
800        if session.is_expired() {
801            return None;
802        }
803
804        // Verify service account still enabled
805        let accounts = self.service_accounts.read();
806        let account = accounts.get(&session.service_account)?;
807        if !account.enabled {
808            return None;
809        }
810
811        Some(session.clone())
812    }
813
814    /// Invalidate a session
815    pub fn invalidate_session(&self, session_id: &str) {
816        self.sessions.write().remove(session_id);
817    }
818
819    /// Cleanup expired sessions
820    pub fn cleanup_expired_sessions(&self) {
821        let mut sessions = self.sessions.write();
822        let before = sessions.len();
823        sessions.retain(|_, s| !s.is_expired());
824        let removed = before - sessions.len();
825        if removed > 0 {
826            debug!("Cleaned up {} expired service sessions", removed);
827        }
828    }
829}
830
831impl Default for ServiceAuthManager {
832    fn default() -> Self {
833        Self::new()
834    }
835}
836
837// ============================================================================
838// Authentication Request/Response Protocol
839// ============================================================================
840
841/// Request to authenticate a service
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub enum ServiceAuthRequest {
844    /// Authenticate with API key
845    ApiKey { key: String },
846
847    /// Authenticate with mTLS (server extracts cert info)
848    MutualTls { certificate_subject: String },
849
850    /// Authenticate with OIDC client credentials
851    OidcClientCredentials {
852        client_id: String,
853        client_secret: String,
854    },
855}
856
857/// Response to authentication request
858#[derive(Debug, Clone, Serialize, Deserialize)]
859pub enum ServiceAuthResponse {
860    /// Authentication successful
861    Success {
862        session_id: String,
863        expires_in_secs: u64,
864        permissions: Vec<String>,
865    },
866
867    /// Authentication failed
868    Failure { error: String },
869}
870
871// ============================================================================
872// Configuration
873// ============================================================================
874
875/// Configuration for service authentication
876#[derive(Debug, Clone, Serialize, Deserialize)]
877pub struct ServiceAuthConfig {
878    /// Enable API key authentication
879    #[serde(default = "default_true")]
880    pub api_key_enabled: bool,
881
882    /// Enable mTLS authentication
883    #[serde(default)]
884    pub mtls_enabled: bool,
885
886    /// Enable OIDC client credentials
887    #[serde(default)]
888    pub oidc_enabled: bool,
889
890    /// Session duration in seconds
891    #[serde(default = "default_session_duration")]
892    pub session_duration_secs: u64,
893
894    /// Max auth attempts per IP per minute
895    #[serde(default = "default_max_attempts")]
896    pub max_auth_attempts: usize,
897
898    /// Pre-configured service accounts
899    #[serde(default)]
900    pub service_accounts: Vec<ServiceAccountConfig>,
901}
902
903fn default_true() -> bool {
904    true
905}
906
907fn default_session_duration() -> u64 {
908    3600
909}
910
911fn default_max_attempts() -> usize {
912    10
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
916pub struct ServiceAccountConfig {
917    pub name: String,
918    pub description: Option<String>,
919    pub roles: Vec<String>,
920    pub certificate_subject: Option<String>,
921    pub oidc_client_id: Option<String>,
922}
923
924impl Default for ServiceAuthConfig {
925    fn default() -> Self {
926        Self {
927            api_key_enabled: true,
928            mtls_enabled: false,
929            oidc_enabled: false,
930            session_duration_secs: 3600,
931            max_auth_attempts: 10,
932            service_accounts: vec![],
933        }
934    }
935}
936
937// ============================================================================
938// Tests
939// ============================================================================
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944
945    #[test]
946    fn test_api_key_generation() {
947        let (api_key, full_key) =
948            ApiKey::generate("test-service", Some("Test key"), None, vec![]).unwrap();
949
950        assert!(!api_key.revoked);
951        assert!(api_key.is_valid());
952        assert!(full_key.starts_with("rvn.v1."));
953
954        // Parse and verify
955        let (key_id, secret) = ApiKey::parse_key(&full_key).unwrap();
956        assert_eq!(key_id, api_key.key_id);
957        assert!(api_key.verify_secret(&secret));
958    }
959
960    #[test]
961    fn test_api_key_expiration() {
962        let (mut api_key, _) = ApiKey::generate(
963            "test-service",
964            None,
965            Some(Duration::from_secs(0)), // Expires immediately
966            vec![],
967        )
968        .unwrap();
969
970        // Wait a bit
971        std::thread::sleep(Duration::from_millis(10));
972        assert!(!api_key.is_valid());
973
974        // Test revocation
975        api_key.revoked = true;
976        assert!(!api_key.is_valid());
977    }
978
979    #[test]
980    fn test_ip_allowlist() {
981        let mut api_key = ApiKey::generate("test-service", None, None, vec![])
982            .unwrap()
983            .0;
984
985        // Empty allowlist = all allowed
986        assert!(api_key.is_ip_allowed("192.168.1.1"));
987
988        // With allowlist
989        api_key.allowed_ips = vec!["192.168.1.0/24".to_string()];
990        assert!(api_key.is_ip_allowed("192.168.1.100"));
991        assert!(!api_key.is_ip_allowed("10.0.0.1"));
992    }
993
994    #[test]
995    fn test_service_auth_manager() {
996        let manager = ServiceAuthManager::new();
997
998        // Create service account
999        let account = ServiceAccount::new("connector-postgres")
1000            .with_description("PostgreSQL CDC connector")
1001            .with_roles(vec!["connector".to_string()]);
1002
1003        manager.create_service_account(account).unwrap();
1004
1005        // Create API key
1006        let full_key = manager
1007            .create_api_key(
1008                "connector-postgres",
1009                Some("Production key"),
1010                None,
1011                vec!["topic:read".to_string(), "topic:write".to_string()],
1012            )
1013            .unwrap();
1014
1015        // Authenticate
1016        let session = manager
1017            .authenticate_api_key(&full_key, "127.0.0.1")
1018            .unwrap();
1019
1020        assert_eq!(session.service_account, "connector-postgres");
1021        assert_eq!(session.auth_method, AuthMethod::ApiKey);
1022        assert!(!session.is_expired());
1023
1024        // Validate session
1025        let validated = manager.validate_session(&session.id).unwrap();
1026        assert_eq!(validated.id, session.id);
1027    }
1028
1029    #[test]
1030    fn test_invalid_api_key() {
1031        let manager = ServiceAuthManager::new();
1032
1033        // Create service account
1034        manager
1035            .create_service_account(ServiceAccount::new("test"))
1036            .unwrap();
1037
1038        // Try to authenticate with invalid key (correctly formatted but non-existent)
1039        let result = manager.authenticate_api_key(
1040            "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1041            "127.0.0.1",
1042        );
1043        assert!(matches!(result, Err(ServiceAuthError::KeyNotFound(_))));
1044    }
1045
1046    #[test]
1047    fn test_rate_limiting() {
1048        let manager = ServiceAuthManager::new();
1049
1050        // Try many invalid auth attempts
1051        for _ in 0..15 {
1052            let _ = manager.authenticate_api_key(
1053                "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1054                "1.2.3.4",
1055            );
1056        }
1057
1058        // Should be rate limited
1059        let result = manager.authenticate_api_key(
1060            "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1061            "1.2.3.4",
1062        );
1063        assert!(matches!(result, Err(ServiceAuthError::RateLimited)));
1064    }
1065
1066    #[test]
1067    fn test_service_account_disable() {
1068        let manager = ServiceAuthManager::new();
1069
1070        // Create and authenticate
1071        manager
1072            .create_service_account(ServiceAccount::new("test-service"))
1073            .unwrap();
1074
1075        let key = manager
1076            .create_api_key("test-service", None, None, vec![])
1077            .unwrap();
1078
1079        let session = manager.authenticate_api_key(&key, "127.0.0.1").unwrap();
1080
1081        // Disable account
1082        manager.disable_service_account("test-service").unwrap();
1083
1084        // Session should be invalid
1085        assert!(manager.validate_session(&session.id).is_none());
1086
1087        // New auth should fail
1088        let result = manager.authenticate_api_key(&key, "127.0.0.1");
1089        assert!(matches!(
1090            result,
1091            Err(ServiceAuthError::ServiceAccountDisabled(_))
1092        ));
1093    }
1094
1095    #[test]
1096    fn test_certificate_auth() {
1097        let manager = ServiceAuthManager::new();
1098
1099        // Create service account with certificate
1100        let account = ServiceAccount::new("connector-orders")
1101            .with_certificate_subject("CN=connector-orders,O=Rivven")
1102            .with_roles(vec!["connector".to_string()]);
1103
1104        manager.create_service_account(account).unwrap();
1105
1106        // Authenticate with certificate
1107        let session = manager
1108            .authenticate_certificate("CN=connector-orders,O=Rivven", "127.0.0.1")
1109            .unwrap();
1110
1111        assert_eq!(session.service_account, "connector-orders");
1112        assert_eq!(session.auth_method, AuthMethod::MutualTls);
1113    }
1114
1115    #[test]
1116    fn test_api_key_debug_redacts_secret_hash() {
1117        let (api_key, _) = ApiKey::generate(
1118            "test-service",
1119            Some("Test key"),
1120            None,
1121            vec!["read".to_string()],
1122        )
1123        .unwrap();
1124
1125        let debug_output = format!("{:?}", api_key);
1126
1127        // Should contain REDACTED for secret_hash
1128        assert!(
1129            debug_output.contains("[REDACTED]"),
1130            "Debug output should contain [REDACTED]: {}",
1131            debug_output
1132        );
1133
1134        // Should NOT contain the actual hash (which is a base64-like string)
1135        assert!(
1136            !debug_output.contains(&api_key.secret_hash),
1137            "Debug output should not contain the secret hash"
1138        );
1139
1140        // Should still show non-sensitive fields
1141        assert!(
1142            debug_output.contains("key_id"),
1143            "Debug output should show key_id field"
1144        );
1145        assert!(
1146            debug_output.contains("test-service"),
1147            "Debug output should show service_account"
1148        );
1149    }
1150}