scim_server/auth/
mod.rs

1//! Compile-time authentication system with type-level proofs.
2//!
3//! This module provides a zero-cost abstraction for authentication that leverages
4//! Rust's type system to ensure only authenticated clients can access resources.
5//! Authentication state is tracked at compile time, preventing runtime security bugs.
6//!
7//! # Type-Level Authentication Design
8//!
9//! The system uses phantom types and witness types to encode authentication state:
10//!
11//! * **Unauthenticated State**: Raw credentials that haven't been validated
12//! * **Authenticated State**: Credentials that have passed validation with type-level proof
13//! * **Authorized Context**: Request contexts that can only be created with valid authentication
14//! * **Linear Credentials**: Authentication tokens that can only be consumed once
15//!
16//! # Key Principles
17//!
18//! 1. **Impossible States**: Unauthenticated access is unrepresentable
19//! 2. **Zero Runtime Cost**: All validation happens at compile time where possible
20//! 3. **Linear Resources**: Credentials are consumed during authentication
21//! 4. **Proof Carrying**: Authenticated contexts carry evidence of validation
22//! 5. **Type Safety**: Operations require specific authentication levels
23//!
24//! # Example Usage
25//!
26//! ```rust
27//! use scim_server::auth::{
28//!     AuthenticationValidator, AuthenticatedRequestContext,
29//!     LinearCredential, Credential, Unauthenticated
30//! };
31//!
32//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33//! // Raw credential (unauthenticated)
34//! let raw_cred = LinearCredential::new("api-key-123");
35//!
36//! // Validation consumes the raw credential
37//! let validator = AuthenticationValidator::new();
38//! let witness = validator.authenticate(raw_cred).await?;
39//!
40//! // Only validated credentials can create authenticated contexts
41//! let auth_context = AuthenticatedRequestContext::from_witness(witness);
42//!
43//! // Only authenticated contexts can access resources
44//! // provider.list_resources(&auth_context).await;
45//! # Ok(())
46//! # }
47//! ```
48
49use crate::resource::{RequestContext, TenantContext};
50use sha2::{Digest, Sha256};
51use std::collections::HashMap;
52use std::marker::PhantomData;
53use std::sync::Arc;
54use tokio::sync::RwLock;
55
56/// Type-level authentication states using phantom types
57pub trait AuthState: Send + Sync + 'static {}
58
59/// Phantom type for unauthenticated state
60#[derive(Debug, Clone, Copy)]
61pub struct Unauthenticated;
62impl AuthState for Unauthenticated {}
63
64/// Phantom type for authenticated state
65#[derive(Debug, Clone, Copy)]
66pub struct Authenticated;
67impl AuthState for Authenticated {}
68
69/// Credential with compile-time authentication state
70#[derive(Debug, Clone)]
71pub struct Credential<S: AuthState> {
72    pub(crate) value: String,
73    pub(crate) _phantom: PhantomData<S>,
74}
75
76impl Credential<Unauthenticated> {
77    /// Create a new unauthenticated credential
78    pub fn new(value: impl Into<String>) -> Self {
79        Self {
80            value: value.into(),
81            _phantom: PhantomData,
82        }
83    }
84
85    /// Get the raw credential value (only available for unauthenticated)
86    pub fn raw_value(&self) -> &str {
87        &self.value
88    }
89}
90
91impl Credential<Authenticated> {
92    /// Create an authenticated credential (internal use only)
93    #[allow(dead_code)]
94    pub(crate) fn authenticated(value: String) -> Self {
95        Self {
96            value,
97            _phantom: PhantomData,
98        }
99    }
100
101    /// Get the credential value (only available after authentication)
102    pub fn authenticated_value(&self) -> &str {
103        &self.value
104    }
105}
106
107/// Witness type proving successful authentication
108///
109/// This type can only be constructed by the authentication system
110/// and serves as compile-time proof that validation occurred.
111#[derive(Debug, Clone)]
112pub struct AuthenticationWitness {
113    pub(crate) tenant_context: TenantContext,
114    pub(crate) credential_hash: String,
115    pub(crate) validated_at: chrono::DateTime<chrono::Utc>,
116}
117
118impl AuthenticationWitness {
119    /// Create a new authentication witness (internal use only)
120    pub(crate) fn new(tenant_context: TenantContext, credential_hash: String) -> Self {
121        Self {
122            tenant_context,
123            credential_hash,
124            validated_at: chrono::Utc::now(),
125        }
126    }
127
128    /// Get the tenant context (only available with witness)
129    pub fn tenant_context(&self) -> &TenantContext {
130        &self.tenant_context
131    }
132
133    /// Get validation timestamp
134    pub fn validated_at(&self) -> chrono::DateTime<chrono::Utc> {
135        self.validated_at
136    }
137
138    /// Get credential hash for audit purposes
139    pub fn credential_hash(&self) -> &str {
140        &self.credential_hash
141    }
142}
143
144/// Witness type proving tenant-level authority
145///
146/// This type can only be created from an AuthenticationWitness
147/// and proves the holder has authority within a specific tenant.
148#[derive(Debug, Clone)]
149pub struct TenantAuthority {
150    witness: AuthenticationWitness,
151}
152
153impl TenantAuthority {
154    /// Create tenant authority from authentication witness
155    pub fn from_witness(witness: AuthenticationWitness) -> Self {
156        Self { witness }
157    }
158
159    /// Get the underlying authentication witness
160    pub fn witness(&self) -> &AuthenticationWitness {
161        &self.witness
162    }
163
164    /// Get tenant ID (compile-time guaranteed to exist)
165    pub fn tenant_id(&self) -> &str {
166        &self.witness.tenant_context.tenant_id
167    }
168
169    /// Get client ID (compile-time guaranteed to exist)
170    pub fn client_id(&self) -> &str {
171        &self.witness.tenant_context.client_id
172    }
173}
174
175/// Linear credential that can only be consumed once
176///
177/// This prevents credential reuse and ensures authentication
178/// happens exactly once per credential.
179#[derive(Debug)]
180pub struct LinearCredential {
181    inner: Option<Credential<Unauthenticated>>,
182}
183
184impl LinearCredential {
185    /// Create a new linear credential
186    pub fn new(value: impl Into<String>) -> Self {
187        Self {
188            inner: Some(Credential::new(value)),
189        }
190    }
191
192    /// Consume this credential for authentication (can only be called once)
193    pub fn consume(mut self) -> Credential<Unauthenticated> {
194        self.inner.take().expect("Credential already consumed")
195    }
196
197    /// Check if credential has been consumed
198    pub fn is_consumed(&self) -> bool {
199        self.inner.is_none()
200    }
201}
202
203/// Marker type proving a credential was consumed
204#[derive(Debug)]
205pub struct ConsumedCredential {
206    _private: (),
207}
208
209impl ConsumedCredential {
210    /// Create proof of consumption (internal use only)
211    pub(crate) fn new() -> Self {
212        Self { _private: () }
213    }
214}
215
216/// Result of authentication that either succeeds with proof or fails
217#[derive(Debug)]
218pub enum AuthenticationResult {
219    /// Authentication succeeded with witness
220    Success {
221        witness: AuthenticationWitness,
222        consumed: ConsumedCredential,
223    },
224    /// Authentication failed
225    Failed { consumed: ConsumedCredential },
226}
227
228/// Request context that can only be created with authentication proof
229#[derive(Debug, Clone)]
230pub struct AuthenticatedRequestContext {
231    inner: RequestContext,
232    authority: TenantAuthority,
233}
234
235impl AuthenticatedRequestContext {
236    /// Create authenticated context from witness (consuming it)
237    pub fn from_witness(witness: AuthenticationWitness) -> Self {
238        let tenant_context = witness.tenant_context().clone();
239        let authority = TenantAuthority::from_witness(witness);
240        let inner = RequestContext::with_tenant_generated_id(tenant_context);
241
242        Self { inner, authority }
243    }
244
245    /// Create authenticated context with specific request ID
246    pub fn with_request_id(witness: AuthenticationWitness, request_id: String) -> Self {
247        let tenant_context = witness.tenant_context().clone();
248        let authority = TenantAuthority::from_witness(witness);
249        let inner = RequestContext::with_tenant(request_id, tenant_context);
250
251        Self { inner, authority }
252    }
253
254    /// Get the underlying request context
255    pub fn request_context(&self) -> &RequestContext {
256        &self.inner
257    }
258
259    /// Get tenant authority proof
260    pub fn authority(&self) -> &TenantAuthority {
261        &self.authority
262    }
263
264    /// Get tenant ID (compile-time guaranteed)
265    pub fn tenant_id(&self) -> &str {
266        self.authority.tenant_id()
267    }
268
269    /// Get client ID (compile-time guaranteed)
270    pub fn client_id(&self) -> &str {
271        self.authority.client_id()
272    }
273
274    /// Get request ID
275    pub fn request_id(&self) -> &str {
276        &self.inner.request_id
277    }
278}
279
280/// Simplified authenticated context for common operations
281#[derive(Debug, Clone)]
282pub struct AuthenticatedContext {
283    authority: TenantAuthority,
284}
285
286impl AuthenticatedContext {
287    /// Create from authentication witness
288    pub fn from_witness(witness: AuthenticationWitness) -> Self {
289        Self {
290            authority: TenantAuthority::from_witness(witness),
291        }
292    }
293
294    /// Convert to full request context
295    pub fn to_request_context(&self) -> AuthenticatedRequestContext {
296        let witness = self.authority.witness().clone();
297        AuthenticatedRequestContext::from_witness(witness)
298    }
299
300    /// Get tenant authority
301    pub fn authority(&self) -> &TenantAuthority {
302        &self.authority
303    }
304}
305
306/// Compile-time authentication validator
307#[derive(Debug, Clone)]
308pub struct AuthenticationValidator {
309    // Runtime credential store for validation
310    credentials: Arc<RwLock<HashMap<String, TenantContext>>>,
311}
312
313impl AuthenticationValidator {
314    /// Create a new authentication validator
315    pub fn new() -> Self {
316        Self {
317            credentials: Arc::new(RwLock::new(HashMap::new())),
318        }
319    }
320
321    /// Register a credential (for testing/setup)
322    pub async fn register_credential(&self, credential: &str, tenant_context: TenantContext) {
323        let mut creds = self.credentials.write().await;
324        creds.insert(credential.to_string(), tenant_context);
325    }
326
327    /// Authenticate a credential with compile-time proof
328    ///
329    /// This method consumes the credential and either returns an authentication
330    /// witness (proving successful validation) or an error with proof of consumption.
331    pub async fn authenticate(
332        &self,
333        credential: LinearCredential,
334    ) -> Result<AuthenticationWitness, AuthenticationError> {
335        // Consume the credential (can only happen once)
336        let raw_cred = credential.consume();
337        let _consumed_proof = ConsumedCredential::new();
338
339        // Runtime validation
340        let creds = self.credentials.read().await;
341        if let Some(tenant_context) = creds.get(raw_cred.raw_value()) {
342            // Create secure hash for witness using SHA-256
343            let mut hasher = Sha256::new();
344            hasher.update(raw_cred.raw_value().as_bytes());
345            let credential_hash = format!("{:x}", hasher.finalize());
346
347            Ok(AuthenticationWitness::new(
348                tenant_context.clone(),
349                credential_hash,
350            ))
351        } else {
352            Err(AuthenticationError::InvalidCredential)
353        }
354    }
355}
356
357impl Default for AuthenticationValidator {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363/// Authentication errors
364#[derive(Debug, thiserror::Error)]
365pub enum AuthenticationError {
366    #[error("Invalid credential provided")]
367    InvalidCredential,
368    #[error("Credential has been revoked")]
369    CredentialRevoked,
370    #[error("Authentication system unavailable")]
371    SystemUnavailable,
372}
373
374/// Type-safe authentication traits for providers
375pub trait AuthenticatedProvider {
376    type Error: std::error::Error + Send + Sync + 'static;
377
378    /// List resources with authenticated context (compile-time guaranteed)
379    fn list_resources_authenticated(
380        &self,
381        resource_type: &str,
382        context: &AuthenticatedRequestContext,
383    ) -> impl std::future::Future<Output = Result<Vec<crate::resource::Resource>, Self::Error>> + Send;
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::resource::TenantContext;
390
391    #[tokio::test]
392    async fn test_linear_credential_consumption() {
393        let cred = LinearCredential::new("test-key");
394        assert!(!cred.is_consumed());
395
396        let _raw = cred.consume();
397        // Credential is now consumed - cannot use again
398    }
399
400    #[tokio::test]
401    async fn test_authentication_flow() {
402        let validator = AuthenticationValidator::new();
403        let tenant_ctx = TenantContext::new("test-tenant".to_string(), "test-client".to_string());
404
405        validator.register_credential("valid-key", tenant_ctx).await;
406
407        // Create linear credential
408        let cred = LinearCredential::new("valid-key");
409
410        // Authenticate (consumes credential)
411        let witness = validator.authenticate(cred).await.unwrap();
412
413        // Create authenticated context
414        let auth_context = AuthenticatedRequestContext::from_witness(witness);
415
416        assert_eq!(auth_context.tenant_id(), "test-tenant");
417        assert_eq!(auth_context.client_id(), "test-client");
418    }
419
420    #[tokio::test]
421    async fn test_invalid_authentication() {
422        let validator = AuthenticationValidator::new();
423        let cred = LinearCredential::new("invalid-key");
424
425        let result = validator.authenticate(cred).await;
426        assert!(result.is_err());
427    }
428
429    #[test]
430    fn test_type_level_authentication_states() {
431        // Can create unauthenticated credential
432        let unauth = Credential::<Unauthenticated>::new("test");
433        assert_eq!(unauth.raw_value(), "test");
434
435        // Cannot create authenticated credential directly
436        // This would not compile:
437        // let auth = Credential::<Authenticated>::new("test");
438    }
439
440    #[test]
441    fn test_witness_types() {
442        let tenant_ctx = TenantContext::new("test".to_string(), "client".to_string());
443        let witness = AuthenticationWitness::new(tenant_ctx, "hash".to_string());
444
445        let authority = TenantAuthority::from_witness(witness);
446        assert_eq!(authority.tenant_id(), "test");
447        assert_eq!(authority.client_id(), "client");
448    }
449}