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