Skip to main content

clawft_kernel/
auth_service.rs

1//! Centralized credential management service -- Plan 9 Factotum pattern (K5-G2).
2//!
3//! The [`AuthService`] manages external credentials centrally so that agents
4//! never hold raw secrets. Instead, agents request scoped, time-limited tokens
5//! via IPC. All credential access is audited.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::Duration;
9
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use dashmap::DashMap;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use tracing::{info, warn};
16
17use crate::error::KernelError;
18use crate::health::HealthStatus;
19use crate::process::Pid;
20use crate::service::{ServiceType, SystemService};
21
22// ---------------------------------------------------------------------------
23// CredentialType
24// ---------------------------------------------------------------------------
25
26/// Classification of a stored credential.
27#[non_exhaustive]
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub enum CredentialType {
30    /// API key (e.g., OpenAI, GitHub).
31    ApiKey,
32    /// Bearer/OAuth token.
33    BearerToken,
34    /// TLS client certificate.
35    Certificate,
36    /// User-defined credential type.
37    Custom(String),
38}
39
40impl std::fmt::Display for CredentialType {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::ApiKey => write!(f, "api_key"),
44            Self::BearerToken => write!(f, "bearer_token"),
45            Self::Certificate => write!(f, "certificate"),
46            Self::Custom(s) => write!(f, "custom({s})"),
47        }
48    }
49}
50
51// ---------------------------------------------------------------------------
52// StoredCredential
53// ---------------------------------------------------------------------------
54
55/// An encrypted credential stored by the AuthService.
56#[derive(Debug, Clone)]
57pub struct StoredCredential {
58    /// Human-readable credential name.
59    pub name: String,
60    /// Credential classification.
61    pub credential_type: CredentialType,
62    /// Encrypted credential value (never exposed directly).
63    encrypted_value: Vec<u8>,
64    /// Agent IDs allowed to request tokens for this credential.
65    pub allowed_agents: Vec<String>,
66    /// When the credential was registered.
67    pub created_at: DateTime<Utc>,
68}
69
70// ---------------------------------------------------------------------------
71// IssuedToken
72// ---------------------------------------------------------------------------
73
74/// A scoped, time-limited token issued to an agent.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct IssuedToken {
77    /// Unique token identifier.
78    pub token_id: String,
79    /// Name of the credential this token grants access to.
80    pub credential_name: String,
81    /// PID of the agent the token was issued to.
82    pub issued_to: Pid,
83    /// When the token was issued.
84    pub issued_at: DateTime<Utc>,
85    /// When the token expires.
86    pub expires_at: DateTime<Utc>,
87    /// Scoped operations this token permits.
88    pub scope: Vec<String>,
89}
90
91impl IssuedToken {
92    /// Check whether the token has expired.
93    pub fn is_expired(&self) -> bool {
94        Utc::now() > self.expires_at
95    }
96}
97
98// ---------------------------------------------------------------------------
99// CredentialRequest / CredentialGrant
100// ---------------------------------------------------------------------------
101
102/// Request from an agent to obtain a credential token.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CredentialRequest {
105    /// Name of the credential to access.
106    pub credential_name: String,
107    /// PID of the requesting agent.
108    pub requester_pid: Pid,
109    /// Agent ID for authorization check.
110    pub agent_id: String,
111    /// Requested operations scope.
112    pub scope: Vec<String>,
113    /// Requested time-to-live.
114    pub ttl_secs: u64,
115}
116
117/// Response granting (or denying) a credential request.
118#[non_exhaustive]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum CredentialGrant {
121    /// Token granted.
122    Granted(IssuedToken),
123    /// Request denied.
124    Denied { reason: String },
125}
126
127// ---------------------------------------------------------------------------
128// AuthService
129// ---------------------------------------------------------------------------
130
131// ---------------------------------------------------------------------------
132// HashedCredential — SHA-256 hashed agent credentials
133// ---------------------------------------------------------------------------
134
135/// A hashed credential for agent authentication.
136///
137/// Raw credentials are **never** stored. Only the SHA-256 hash is kept.
138#[derive(Debug, Clone)]
139pub struct HashedCredential {
140    /// Agent identity this credential belongs to.
141    pub agent_id: String,
142    /// SHA-256 hash of the raw credential.
143    pub hash: Vec<u8>,
144    /// When the credential was created.
145    pub created_at: DateTime<Utc>,
146    /// Scopes this credential grants.
147    pub scopes: Vec<String>,
148}
149
150/// A scoped authentication token issued after successful authentication.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AuthToken {
153    /// Unique token identifier.
154    pub token_id: String,
155    /// Agent identity this token was issued to.
156    pub agent_id: String,
157    /// Scopes granted by this token.
158    pub scopes: Vec<String>,
159    /// When the token expires.
160    pub expires_at: DateTime<Utc>,
161    /// When the token was issued.
162    pub created_at: DateTime<Utc>,
163}
164
165impl AuthToken {
166    /// Check whether the token has expired.
167    pub fn is_expired(&self) -> bool {
168        Utc::now() > self.expires_at
169    }
170}
171
172// ---------------------------------------------------------------------------
173// AuthService
174// ---------------------------------------------------------------------------
175
176/// Centralized credential management service (Factotum pattern).
177///
178/// - Credentials are registered once, encrypted at rest.
179/// - Agents request scoped tokens; raw credentials are never exposed.
180/// - Token issuance and access are audited.
181/// - SHA-256 hashed credentials support agent authentication.
182pub struct AuthService {
183    /// Registered credentials (encrypted, name-based).
184    credentials: DashMap<String, StoredCredential>,
185    /// SHA-256 hashed credentials (agent-id-based).
186    hashed_credentials: DashMap<String, HashedCredential>,
187    /// Active tokens (token-based credential access).
188    active_tokens: DashMap<String, IssuedToken>,
189    /// Active auth tokens (from `authenticate`).
190    auth_tokens: DashMap<String, AuthToken>,
191    /// Audit log.
192    audit_log: std::sync::RwLock<Vec<AuditEntry>>,
193    /// Encryption key for credentials.
194    encryption_key: [u8; 32],
195    /// Monotonic token counter.
196    token_counter: AtomicU64,
197}
198
199/// An audit log entry.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AuditEntry {
202    /// What happened.
203    pub action: String,
204    /// Who did it.
205    pub agent_id: String,
206    /// Credential involved.
207    pub credential_name: String,
208    /// When it happened.
209    pub timestamp: DateTime<Utc>,
210    /// Whether it was allowed.
211    pub allowed: bool,
212}
213
214impl AuthService {
215    /// Create a new AuthService with the given encryption key.
216    pub fn new(encryption_key: [u8; 32]) -> Self {
217        Self {
218            credentials: DashMap::new(),
219            hashed_credentials: DashMap::new(),
220            active_tokens: DashMap::new(),
221            auth_tokens: DashMap::new(),
222            audit_log: std::sync::RwLock::new(Vec::new()),
223            encryption_key,
224            token_counter: AtomicU64::new(0),
225        }
226    }
227
228    /// Create with a default (zero) encryption key (testing only).
229    pub fn new_default() -> Self {
230        Self::new([0u8; 32])
231    }
232
233    // ── Credential registration ───────────────────────────────────
234
235    /// Register a new credential.
236    pub fn register_credential(
237        &self,
238        name: &str,
239        credential_type: CredentialType,
240        value: &[u8],
241        allowed_agents: Vec<String>,
242    ) -> Result<(), KernelError> {
243        if self.credentials.contains_key(name) {
244            return Err(KernelError::Service(format!(
245                "credential already registered: {name}"
246            )));
247        }
248
249        let encrypted = self.xor_encrypt(value);
250        self.credentials.insert(
251            name.to_string(),
252            StoredCredential {
253                name: name.to_string(),
254                credential_type,
255                encrypted_value: encrypted,
256                allowed_agents,
257                created_at: Utc::now(),
258            },
259        );
260
261        info!(name, "credential registered");
262        Ok(())
263    }
264
265    /// Update an existing credential's value (rotation).
266    pub fn rotate_credential(
267        &self,
268        name: &str,
269        new_value: &[u8],
270    ) -> Result<(), KernelError> {
271        let mut cred = self
272            .credentials
273            .get_mut(name)
274            .ok_or_else(|| KernelError::Service(format!("credential not found: {name}")))?;
275        cred.encrypted_value = self.xor_encrypt(new_value);
276        info!(name, "credential rotated");
277        Ok(())
278    }
279
280    // ── Token issuance ────────────────────────────────────────────
281
282    /// Request a scoped, time-limited token.
283    pub fn request_token(
284        &self,
285        request: &CredentialRequest,
286    ) -> Result<IssuedToken, KernelError> {
287        let cred = self
288            .credentials
289            .get(&request.credential_name)
290            .ok_or_else(|| {
291                KernelError::Service(format!(
292                    "credential not found: {}",
293                    request.credential_name
294                ))
295            })?;
296
297        // Authorization check.
298        if !cred.allowed_agents.is_empty()
299            && !cred.allowed_agents.contains(&request.agent_id)
300        {
301            self.audit("token.denied", &request.agent_id, &request.credential_name, false);
302            warn!(
303                agent_id = %request.agent_id,
304                credential = %request.credential_name,
305                "token request denied"
306            );
307            return Err(KernelError::CapabilityDenied {
308                pid: request.requester_pid,
309                action: "request_token".into(),
310                reason: format!(
311                    "agent '{}' not authorized for credential '{}'",
312                    request.agent_id, request.credential_name
313                ),
314            });
315        }
316
317        let ttl = Duration::from_secs(request.ttl_secs.max(1));
318        let token = IssuedToken {
319            token_id: uuid::Uuid::new_v4().to_string(),
320            credential_name: request.credential_name.clone(),
321            issued_to: request.requester_pid,
322            issued_at: Utc::now(),
323            expires_at: Utc::now() + chrono::Duration::from_std(ttl).unwrap_or(chrono::Duration::hours(1)),
324            scope: request.scope.clone(),
325        };
326
327        self.active_tokens
328            .insert(token.token_id.clone(), token.clone());
329        self.audit("token.issued", &request.agent_id, &request.credential_name, true);
330
331        info!(
332            token_id = %token.token_id,
333            credential = %request.credential_name,
334            agent = %request.agent_id,
335            ttl_secs = request.ttl_secs,
336            "token issued"
337        );
338        Ok(token)
339    }
340
341    /// Validate an issued token. Returns `Err` if expired or not found.
342    pub fn validate_token(&self, token_id: &str) -> Result<IssuedToken, KernelError> {
343        let token = self
344            .active_tokens
345            .get(token_id)
346            .ok_or_else(|| KernelError::Service("token not found".into()))?;
347
348        if token.is_expired() {
349            return Err(KernelError::Service("token expired".into()));
350        }
351
352        Ok(token.clone())
353    }
354
355    /// Revoke an active token.
356    pub fn revoke_token(&self, token_id: &str) -> bool {
357        self.active_tokens.remove(token_id).is_some()
358    }
359
360    /// List all active (non-expired) tokens.
361    pub fn active_token_count(&self) -> usize {
362        self.active_tokens
363            .iter()
364            .filter(|t| !t.value().is_expired())
365            .count()
366    }
367
368    // ── SHA-256 hashed credential operations ─────────────────────
369
370    /// Compute the SHA-256 hash of a raw credential.
371    fn sha256_hash(data: &[u8]) -> Vec<u8> {
372        let mut hasher = Sha256::new();
373        hasher.update(data);
374        hasher.finalize().to_vec()
375    }
376
377    /// Register a hashed credential for an agent.
378    ///
379    /// The raw credential is **never stored**; only its SHA-256 hash is kept.
380    pub fn register_hashed_credential(
381        &self,
382        agent_id: &str,
383        raw_credential: &[u8],
384        scopes: Vec<String>,
385    ) -> Result<(), KernelError> {
386        if self.hashed_credentials.contains_key(agent_id) {
387            return Err(KernelError::Service(format!(
388                "hashed credential already registered for agent: {agent_id}"
389            )));
390        }
391
392        let hash = Self::sha256_hash(raw_credential);
393        self.hashed_credentials.insert(
394            agent_id.to_string(),
395            HashedCredential {
396                agent_id: agent_id.to_string(),
397                hash,
398                created_at: Utc::now(),
399                scopes,
400            },
401        );
402
403        info!(agent_id, "hashed credential registered");
404        Ok(())
405    }
406
407    /// Authenticate an agent by verifying its raw credential against the stored hash.
408    ///
409    /// On success, issues a scoped [`AuthToken`] valid for one hour.
410    pub fn authenticate(
411        &self,
412        agent_id: &str,
413        raw_credential: &[u8],
414    ) -> Result<AuthToken, KernelError> {
415        let cred = self
416            .hashed_credentials
417            .get(agent_id)
418            .ok_or_else(|| KernelError::Service(format!("no credential for agent: {agent_id}")))?;
419
420        let provided_hash = Self::sha256_hash(raw_credential);
421        if cred.hash != provided_hash {
422            self.audit("authenticate.failed", agent_id, agent_id, false);
423            warn!(agent_id, "authentication failed — hash mismatch");
424            return Err(KernelError::Service("authentication failed".into()));
425        }
426
427        let seq = self.token_counter.fetch_add(1, Ordering::Relaxed);
428        let token = AuthToken {
429            token_id: format!("auth-{}-{seq}", uuid::Uuid::new_v4()),
430            agent_id: agent_id.to_string(),
431            scopes: cred.scopes.clone(),
432            expires_at: Utc::now() + chrono::Duration::hours(1),
433            created_at: Utc::now(),
434        };
435
436        self.auth_tokens
437            .insert(token.token_id.clone(), token.clone());
438        self.audit("authenticate.success", agent_id, agent_id, true);
439
440        info!(agent_id, token_id = %token.token_id, "agent authenticated");
441        Ok(token)
442    }
443
444    /// Validate an auth token. Returns `Err` if expired or not found.
445    pub fn validate_auth_token(&self, token_id: &str) -> Result<AuthToken, KernelError> {
446        let token = self
447            .auth_tokens
448            .get(token_id)
449            .ok_or_else(|| KernelError::Service("auth token not found".into()))?;
450
451        if token.is_expired() {
452            return Err(KernelError::Service("auth token expired".into()));
453        }
454
455        Ok(token.clone())
456    }
457
458    /// Revoke an auth token. Returns `true` if it existed.
459    pub fn revoke_auth_token(&self, token_id: &str) -> bool {
460        self.auth_tokens.remove(token_id).is_some()
461    }
462
463    /// Check whether an auth token has a specific scope.
464    pub fn check_scope(&self, token_id: &str, required_scope: &str) -> bool {
465        self.auth_tokens
466            .get(token_id)
467            .map(|t| !t.is_expired() && t.scopes.contains(&required_scope.to_string()))
468            .unwrap_or(false)
469    }
470
471    // ── Audit ─────────────────────────────────────────────────────
472
473    fn audit(&self, action: &str, agent_id: &str, credential_name: &str, allowed: bool) {
474        if let Ok(mut log) = self.audit_log.write() {
475            log.push(AuditEntry {
476                action: action.to_string(),
477                agent_id: agent_id.to_string(),
478                credential_name: credential_name.to_string(),
479                timestamp: Utc::now(),
480                allowed,
481            });
482        }
483    }
484
485    /// Get the audit log.
486    pub fn audit_log(&self) -> Vec<AuditEntry> {
487        self.audit_log.read().map(|l| l.clone()).unwrap_or_default()
488    }
489
490    // ── Encryption ────────────────────────────────────────────────
491
492    fn xor_encrypt(&self, data: &[u8]) -> Vec<u8> {
493        data.iter()
494            .enumerate()
495            .map(|(i, b)| b ^ self.encryption_key[i % 32])
496            .collect()
497    }
498}
499
500#[async_trait]
501impl SystemService for AuthService {
502    fn name(&self) -> &str {
503        "auth-service"
504    }
505
506    fn service_type(&self) -> ServiceType {
507        ServiceType::Core
508    }
509
510    async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
511        info!("auth service started");
512        Ok(())
513    }
514
515    async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
516        info!(
517            credentials = self.credentials.len(),
518            active_tokens = self.active_token_count(),
519            "auth service stopped"
520        );
521        Ok(())
522    }
523
524    async fn health_check(&self) -> HealthStatus {
525        HealthStatus::Healthy
526    }
527}
528
529// ── Tests ─────────────────────────────────────────────────────────────────
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    fn pid(n: u64) -> Pid {
536        n
537    }
538
539    fn make_request(cred: &str, agent: &str, pid_val: u64) -> CredentialRequest {
540        CredentialRequest {
541            credential_name: cred.to_string(),
542            requester_pid: pid(pid_val),
543            agent_id: agent.to_string(),
544            scope: vec!["read".to_string()],
545            ttl_secs: 3600,
546        }
547    }
548
549    #[test]
550    fn register_and_request_token() {
551        let svc = AuthService::new_default();
552        svc.register_credential(
553            "github-key",
554            CredentialType::ApiKey,
555            b"ghp_secret_value",
556            vec!["deploy-agent".to_string()],
557        )
558        .unwrap();
559
560        let req = make_request("github-key", "deploy-agent", 1);
561        let token = svc.request_token(&req).unwrap();
562        assert!(!token.token_id.is_empty());
563        assert_eq!(token.credential_name, "github-key");
564    }
565
566    #[test]
567    fn unauthorized_agent_denied() {
568        let svc = AuthService::new_default();
569        svc.register_credential(
570            "secret",
571            CredentialType::BearerToken,
572            b"token",
573            vec!["allowed-agent".to_string()],
574        )
575        .unwrap();
576
577        let req = make_request("secret", "other-agent", 2);
578        let result = svc.request_token(&req);
579        assert!(result.is_err());
580        let err = result.unwrap_err().to_string();
581        assert!(err.contains("denied") || err.contains("authorized"), "got: {err}");
582    }
583
584    #[test]
585    fn empty_allowed_agents_permits_all() {
586        let svc = AuthService::new_default();
587        svc.register_credential(
588            "open-cred",
589            CredentialType::ApiKey,
590            b"value",
591            vec![], // any agent
592        )
593        .unwrap();
594
595        let req = make_request("open-cred", "random-agent", 5);
596        let token = svc.request_token(&req).unwrap();
597        assert!(!token.is_expired());
598    }
599
600    #[test]
601    fn validate_token() {
602        let svc = AuthService::new_default();
603        svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
604            .unwrap();
605        let req = make_request("cred", "agent", 1);
606        let token = svc.request_token(&req).unwrap();
607        let validated = svc.validate_token(&token.token_id).unwrap();
608        assert_eq!(validated.token_id, token.token_id);
609    }
610
611    #[test]
612    fn validate_nonexistent_token_fails() {
613        let svc = AuthService::new_default();
614        assert!(svc.validate_token("no-such-token").is_err());
615    }
616
617    #[test]
618    fn revoke_token() {
619        let svc = AuthService::new_default();
620        svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
621            .unwrap();
622        let req = make_request("cred", "agent", 1);
623        let token = svc.request_token(&req).unwrap();
624        assert!(svc.revoke_token(&token.token_id));
625        assert!(svc.validate_token(&token.token_id).is_err());
626    }
627
628    #[test]
629    fn credential_rotation_preserves_tokens() {
630        let svc = AuthService::new_default();
631        svc.register_credential("rotate-cred", CredentialType::ApiKey, b"old_val", vec![])
632            .unwrap();
633        let req = make_request("rotate-cred", "agent", 1);
634        let token = svc.request_token(&req).unwrap();
635
636        // Rotate credential.
637        svc.rotate_credential("rotate-cred", b"new_val").unwrap();
638
639        // Existing token still valid.
640        let validated = svc.validate_token(&token.token_id).unwrap();
641        assert_eq!(validated.credential_name, "rotate-cred");
642    }
643
644    #[test]
645    fn raw_credential_never_exposed() {
646        let key = [0xAB; 32];
647        let svc = AuthService::new(key);
648        svc.register_credential("secret", CredentialType::ApiKey, b"raw_secret", vec![])
649            .unwrap();
650
651        // The stored value should be encrypted, not raw.
652        let cred = svc.credentials.get("secret").unwrap();
653        assert_ne!(cred.encrypted_value, b"raw_secret");
654    }
655
656    #[test]
657    fn audit_log_records_events() {
658        let svc = AuthService::new_default();
659        svc.register_credential(
660            "audited",
661            CredentialType::ApiKey,
662            b"val",
663            vec!["agent-a".to_string()],
664        )
665        .unwrap();
666
667        // Successful request.
668        let req = make_request("audited", "agent-a", 1);
669        svc.request_token(&req).unwrap();
670
671        // Failed request.
672        let bad_req = make_request("audited", "agent-b", 2);
673        let _ = svc.request_token(&bad_req);
674
675        let log = svc.audit_log();
676        assert_eq!(log.len(), 2);
677        assert!(log[0].allowed);
678        assert!(!log[1].allowed);
679    }
680
681    #[test]
682    fn duplicate_credential_registration_fails() {
683        let svc = AuthService::new_default();
684        svc.register_credential("dup", CredentialType::ApiKey, b"val", vec![])
685            .unwrap();
686        let result = svc.register_credential("dup", CredentialType::ApiKey, b"val2", vec![]);
687        assert!(result.is_err());
688    }
689
690    #[test]
691    fn request_nonexistent_credential_fails() {
692        let svc = AuthService::new_default();
693        let req = make_request("missing", "agent", 1);
694        assert!(svc.request_token(&req).is_err());
695    }
696
697    #[tokio::test]
698    async fn system_service_impl() {
699        let svc = AuthService::new_default();
700        assert_eq!(svc.name(), "auth-service");
701        assert_eq!(svc.service_type(), ServiceType::Core);
702        svc.start().await.unwrap();
703        assert_eq!(svc.health_check().await, HealthStatus::Healthy);
704        svc.stop().await.unwrap();
705    }
706
707    // ── SHA-256 hashed credential tests ──────────────────────────
708
709    #[test]
710    fn register_and_authenticate_success() {
711        let svc = AuthService::new_default();
712        svc.register_hashed_credential("agent-1", b"my_password", vec!["read".into()])
713            .unwrap();
714        let token = svc.authenticate("agent-1", b"my_password").unwrap();
715        assert_eq!(token.agent_id, "agent-1");
716        assert!(!token.token_id.is_empty());
717        assert_eq!(token.scopes, vec!["read".to_string()]);
718    }
719
720    #[test]
721    fn authenticate_wrong_credential_fails() {
722        let svc = AuthService::new_default();
723        svc.register_hashed_credential("agent-2", b"correct", vec![]).unwrap();
724        let result = svc.authenticate("agent-2", b"wrong");
725        assert!(result.is_err());
726        let err = result.unwrap_err().to_string();
727        assert!(err.contains("failed"), "got: {err}");
728    }
729
730    #[test]
731    fn authenticate_issues_token_with_scopes() {
732        let svc = AuthService::new_default();
733        svc.register_hashed_credential(
734            "scoped-agent",
735            b"secret",
736            vec!["read".into(), "write".into()],
737        )
738        .unwrap();
739        let token = svc.authenticate("scoped-agent", b"secret").unwrap();
740        assert_eq!(token.scopes, vec!["read".to_string(), "write".to_string()]);
741    }
742
743    #[test]
744    fn validate_auth_token_succeeds() {
745        let svc = AuthService::new_default();
746        svc.register_hashed_credential("v-agent", b"pass", vec![]).unwrap();
747        let token = svc.authenticate("v-agent", b"pass").unwrap();
748        let validated = svc.validate_auth_token(&token.token_id).unwrap();
749        assert_eq!(validated.agent_id, "v-agent");
750    }
751
752    #[test]
753    fn validate_expired_auth_token_fails() {
754        let svc = AuthService::new_default();
755        svc.register_hashed_credential("exp-agent", b"pass", vec![]).unwrap();
756        let token = svc.authenticate("exp-agent", b"pass").unwrap();
757
758        // Manually insert an expired token.
759        let expired = AuthToken {
760            token_id: "expired-tok".to_string(),
761            agent_id: "exp-agent".to_string(),
762            scopes: vec![],
763            expires_at: Utc::now() - chrono::Duration::hours(1),
764            created_at: Utc::now() - chrono::Duration::hours(2),
765        };
766        svc.auth_tokens.insert("expired-tok".into(), expired);
767
768        // The real token should be valid.
769        assert!(svc.validate_auth_token(&token.token_id).is_ok());
770        // The expired token should fail.
771        let result = svc.validate_auth_token("expired-tok");
772        assert!(result.is_err());
773        assert!(result.unwrap_err().to_string().contains("expired"));
774    }
775
776    #[test]
777    fn revoke_auth_token_works() {
778        let svc = AuthService::new_default();
779        svc.register_hashed_credential("rev-agent", b"pass", vec![]).unwrap();
780        let token = svc.authenticate("rev-agent", b"pass").unwrap();
781        assert!(svc.revoke_auth_token(&token.token_id));
782        assert!(svc.validate_auth_token(&token.token_id).is_err());
783        // Second revoke returns false.
784        assert!(!svc.revoke_auth_token(&token.token_id));
785    }
786
787    #[test]
788    fn check_scope_with_matching_scope() {
789        let svc = AuthService::new_default();
790        svc.register_hashed_credential("sc-agent", b"cred", vec!["admin".into(), "read".into()])
791            .unwrap();
792        let token = svc.authenticate("sc-agent", b"cred").unwrap();
793        assert!(svc.check_scope(&token.token_id, "admin"));
794        assert!(svc.check_scope(&token.token_id, "read"));
795    }
796
797    #[test]
798    fn check_scope_with_missing_scope_fails() {
799        let svc = AuthService::new_default();
800        svc.register_hashed_credential("sc-agent2", b"cred", vec!["read".into()])
801            .unwrap();
802        let token = svc.authenticate("sc-agent2", b"cred").unwrap();
803        assert!(!svc.check_scope(&token.token_id, "write"));
804        assert!(!svc.check_scope(&token.token_id, "admin"));
805    }
806
807    #[test]
808    fn raw_credentials_never_stored_in_hash() {
809        let svc = AuthService::new_default();
810        let raw = b"super_secret_password";
811        svc.register_hashed_credential("hash-check", raw, vec![]).unwrap();
812
813        let cred = svc.hashed_credentials.get("hash-check").unwrap();
814        // The hash must not equal the raw input.
815        assert_ne!(cred.hash.as_slice(), raw.as_slice());
816        // The hash should be 32 bytes (SHA-256).
817        assert_eq!(cred.hash.len(), 32);
818    }
819
820    #[test]
821    fn check_scope_on_nonexistent_token_returns_false() {
822        let svc = AuthService::new_default();
823        assert!(!svc.check_scope("no-such-token", "read"));
824    }
825}