Skip to main content

modkit_auth/
dispatcher.rs

1use crate::{
2    auth_mode::PluginRegistry,
3    claims::Claims,
4    claims_error::ClaimsError,
5    config::AuthConfig,
6    config_error::ConfigError,
7    errors::AuthError,
8    plugin_traits::{ClaimsPlugin, IntrospectionProvider, KeyProvider},
9    traits::TokenValidator,
10    validation::{ValidationConfig, validate_claims},
11};
12use async_trait::async_trait;
13use std::sync::Arc;
14use uuid::Uuid;
15
16/// Truncate UUID to first 8 characters for safe logging
17fn truncate_uuid(uuid: &Uuid) -> String {
18    let s = uuid.to_string();
19    s.chars().take(8).collect()
20}
21
22/// Central dispatcher for JWT and opaque token validation
23///
24/// Orchestrates key providers and claims plugins to validate tokens
25/// using a single configured plugin.
26#[must_use]
27pub struct AuthDispatcher {
28    /// Registered key providers (JWKS, etc.)
29    key_providers: Vec<Arc<dyn KeyProvider>>,
30
31    /// Registered introspection providers (for opaque tokens)
32    introspection_providers: Vec<Arc<dyn IntrospectionProvider>>,
33
34    /// The authentication plugin to use for claims normalization
35    plugin: Arc<dyn ClaimsPlugin>,
36
37    /// Common validation configuration
38    validation_config: ValidationConfig,
39}
40
41impl AuthDispatcher {
42    /// Create a new dispatcher with validation config and plugin.
43    ///
44    /// # Errors
45    /// Returns `ConfigError::UnknownPlugin` if the configured provider is not in the registry.
46    pub fn new(
47        validation_config: ValidationConfig,
48        config: &AuthConfig,
49        registry: &PluginRegistry,
50    ) -> Result<Self, ConfigError> {
51        // Get the configured plugin
52        let plugin = registry.get(&config.mode.provider)?.clone();
53
54        Ok(Self {
55            key_providers: Vec::new(),
56            introspection_providers: Vec::new(),
57            plugin,
58            validation_config,
59        })
60    }
61
62    /// Add a key provider
63    pub fn with_key_provider(mut self, provider: Arc<dyn KeyProvider>) -> Self {
64        self.key_providers.push(provider);
65        self
66    }
67
68    /// Add an introspection provider
69    pub fn with_introspection_provider(mut self, provider: Arc<dyn IntrospectionProvider>) -> Self {
70        self.introspection_providers.push(provider);
71        self
72    }
73
74    /// Try to validate and decode JWT with key providers
75    async fn try_validate_with_providers(
76        &self,
77        token: &str,
78    ) -> Result<(jsonwebtoken::Header, serde_json::Value), ClaimsError> {
79        let mut last_error = None;
80        let mut result = None;
81
82        for provider in &self.key_providers {
83            match provider.validate_and_decode(token).await {
84                Ok(r) => {
85                    tracing::debug!(
86                        provider = provider.name(),
87                        kid = ?r.0.kid,
88                        "Successfully validated token signature"
89                    );
90                    result = Some(r);
91                    break;
92                }
93                Err(e) => {
94                    tracing::debug!(
95                        provider = provider.name(),
96                        error = %e,
97                        "Provider failed to validate token"
98                    );
99                    last_error = Some(e);
100                }
101            }
102        }
103
104        result.ok_or_else(|| last_error.unwrap_or(ClaimsError::NoMatchingProvider))
105    }
106
107    /// Extract issuer from raw claims or introspection result
108    fn extract_issuer_from_claims(raw_claims: &serde_json::Value) -> Result<&str, ClaimsError> {
109        raw_claims
110            .get("iss")
111            .and_then(|v| v.as_str())
112            .ok_or_else(|| ClaimsError::Malformed("missing iss claim".into()))
113    }
114
115    /// Normalize claims using the configured plugin
116    fn normalize_claims_with_logging(
117        &self,
118        raw_claims: &serde_json::Value,
119        issuer: &str,
120    ) -> Result<Claims, ClaimsError> {
121        let plugin = &self.plugin;
122
123        tracing::debug!(
124            plugin = plugin.name(),
125            issuer = issuer,
126            "Using configured plugin"
127        );
128
129        plugin.normalize(raw_claims).map_err(|e| {
130            tracing::error!(
131                plugin = plugin.name(),
132                error = %e,
133                issuer = issuer,
134                "Failed to normalize claims"
135            );
136            e
137        })
138    }
139
140    /// Validate claims and log errors
141    fn validate_claims_with_logging(
142        claims: &Claims,
143        validation_config: &ValidationConfig,
144    ) -> Result<(), ClaimsError> {
145        validate_claims(claims, validation_config).map_err(|e| {
146            tracing::warn!(
147                error = %e,
148                sub_prefix = %truncate_uuid(&claims.subject),
149                issuer = %claims.issuer,
150                "Common validation failed"
151            );
152            e
153        })
154    }
155
156    /// Log successful JWT validation
157    fn log_jwt_success(claims: &Claims, plugin_name: &str, kid: Option<&String>) {
158        tracing::debug!(
159            sub_prefix = %truncate_uuid(&claims.subject),
160            issuer = %claims.issuer,
161            plugin = plugin_name,
162            kid = ?kid,
163            num_permissions = claims.permissions.len(),
164            tenant_id = %claims.tenant_id,
165            "Token validation successful"
166        );
167    }
168
169    /// Validate a JWT token.
170    ///
171    /// Workflow:
172    /// 1. Try each `KeyProvider` until one successfully validates the signature
173    /// 2. Extract issuer from token
174    /// 3. Use the configured plugin to normalize claims
175    /// 4. Run common validation (issuer, audience, exp, nbf, UUIDs)
176    /// 5. Return normalized claims
177    ///
178    /// # Errors
179    /// Returns `ClaimsError` if signature validation, claim normalization, or validation fails.
180    pub async fn validate_jwt(&self, token: &str) -> Result<Claims, ClaimsError> {
181        // Step 1: Try to validate signature with each key provider
182        let (header, raw_claims) = self.try_validate_with_providers(token).await?;
183
184        // Step 2: Extract issuer for logging
185        let issuer = Self::extract_issuer_from_claims(&raw_claims)?;
186
187        // Step 3: Normalize claims using the configured plugin
188        let normalized = self.normalize_claims_with_logging(&raw_claims, issuer)?;
189
190        // Step 4: Run common validation
191        Self::validate_claims_with_logging(&normalized, &self.validation_config)?;
192
193        // Step 5: Log success and return
194        Self::log_jwt_success(&normalized, self.plugin.name(), header.kid.as_ref());
195
196        Ok(normalized)
197    }
198
199    /// Try to introspect a token with each provider until one succeeds
200    async fn try_introspect_with_providers(
201        &self,
202        token: &str,
203    ) -> Result<serde_json::Value, ClaimsError> {
204        let mut last_error = None;
205        let mut result = None;
206
207        for provider in &self.introspection_providers {
208            match provider.introspect(token).await {
209                Ok(r) => {
210                    tracing::debug!(
211                        provider = provider.name(),
212                        "Successfully introspected token"
213                    );
214                    result = Some(r);
215                    break;
216                }
217                Err(e) => {
218                    tracing::debug!(
219                        provider = provider.name(),
220                        error = %e,
221                        "Provider failed to introspect token"
222                    );
223                    last_error = Some(e);
224                }
225            }
226        }
227
228        result.ok_or_else(|| {
229            last_error.unwrap_or_else(|| {
230                ClaimsError::Provider("No introspection provider available".into())
231            })
232        })
233    }
234
235    /// Verify that an introspection response indicates an active token
236    fn verify_token_active(introspection_result: &serde_json::Value) -> Result<(), ClaimsError> {
237        if let Some(active) = introspection_result
238            .get("active")
239            .and_then(serde_json::Value::as_bool)
240            && !active
241        {
242            return Err(ClaimsError::IntrospectionDenied);
243        }
244        Ok(())
245    }
246
247    /// Normalize claims with error logging (for opaque tokens)
248    fn normalize_with_logging(
249        &self,
250        introspection_result: &serde_json::Value,
251        issuer: &str,
252    ) -> Result<Claims, ClaimsError> {
253        self.plugin.normalize(introspection_result).map_err(|e| {
254            tracing::error!(
255                plugin = self.plugin.name(),
256                error = %e,
257                issuer = issuer,
258                "Failed to normalize introspection response"
259            );
260            e
261        })
262    }
263
264    /// Validate claims with error logging
265    fn validate_and_log(claims: &Claims, config: &ValidationConfig) -> Result<(), ClaimsError> {
266        validate_claims(claims, config).map_err(|e| {
267            tracing::warn!(
268                error = %e,
269                sub_prefix = %truncate_uuid(&claims.subject),
270                issuer = %claims.issuer,
271                "Common validation failed"
272            );
273            e
274        })
275    }
276
277    /// Log successful validation
278    fn log_validation_success(claims: &Claims, plugin_name: &str) {
279        tracing::debug!(
280            sub_prefix = %truncate_uuid(&claims.subject),
281            issuer = %claims.issuer,
282            plugin = plugin_name,
283            num_permissions = claims.permissions.len(),
284            tenant_id = %claims.tenant_id,
285            "Opaque token validation successful"
286        );
287    }
288
289    /// Validate an opaque token via introspection
290    ///
291    /// Workflow:
292    /// 1. Try each `IntrospectionProvider` until one succeeds
293    /// 2. Extract issuer from introspection response
294    /// 3. Use the configured plugin to normalize claims
295    /// 4. Run common validation
296    /// 5. Return normalized claims
297    ///
298    /// # Errors
299    /// Returns `ClaimsError` if introspection, claim normalization, or validation fails.
300    pub async fn validate_opaque(&self, token: &str) -> Result<Claims, ClaimsError> {
301        // Step 1: Try to introspect with each provider
302        let introspection_result = self.try_introspect_with_providers(token).await?;
303
304        // Step 2: Check if token is active
305        Self::verify_token_active(&introspection_result)?;
306
307        // Step 3: Extract issuer for logging
308        let issuer = Self::extract_issuer_from_claims(&introspection_result)?;
309
310        // Step 4: Log plugin usage
311        tracing::debug!(
312            plugin = self.plugin.name(),
313            issuer = issuer,
314            "Using configured plugin for introspection"
315        );
316
317        // Step 5: Normalize claims
318        let normalized = self.normalize_with_logging(&introspection_result, issuer)?;
319
320        // Step 6: Run common validation
321        Self::validate_and_log(&normalized, &self.validation_config)?;
322
323        // Step 7: Log success
324        Self::log_validation_success(&normalized, self.plugin.name());
325
326        Ok(normalized)
327    }
328
329    /// Get validation config (for inspection/testing)
330    #[must_use]
331    pub fn validation_config(&self) -> &ValidationConfig {
332        &self.validation_config
333    }
334
335    /// Get the configured authentication plugin (for inspection/testing)
336    #[must_use]
337    pub fn plugin(&self) -> &Arc<dyn ClaimsPlugin> {
338        &self.plugin
339    }
340
341    /// Trigger key refresh for all key providers.
342    ///
343    /// # Errors
344    /// Returns a vector of `ClaimsError` if any provider fails to refresh keys.
345    pub async fn refresh_keys(&self) -> Result<(), Vec<ClaimsError>> {
346        let mut errors = Vec::new();
347
348        for provider in &self.key_providers {
349            if let Err(e) = provider.refresh_keys().await {
350                tracing::warn!(
351                    provider = provider.name(),
352                    error = %e,
353                    "Key refresh failed"
354                );
355                errors.push(e);
356            }
357        }
358
359        if errors.is_empty() {
360            Ok(())
361        } else {
362            Err(errors)
363        }
364    }
365}
366
367/// Implement `TokenValidator` trait for `AuthDispatcher`
368#[async_trait]
369impl TokenValidator for AuthDispatcher {
370    async fn validate_and_parse(&self, token: &str) -> Result<Claims, AuthError> {
371        // All JWT validation errors should result in 401 Unauthenticated
372        self.validate_jwt(token)
373            .await
374            .map_err(|_| AuthError::Unauthenticated)
375    }
376}
377
378#[cfg(test)]
379#[cfg_attr(coverage_nightly, coverage(off))]
380mod tests {
381    use super::*;
382    use crate::auth_mode::AuthModeConfig;
383    use crate::config::PluginConfig;
384    use serde_json::json;
385    use std::collections::HashMap;
386
387    #[test]
388    fn test_dispatcher_creation() {
389        let mut plugins = HashMap::new();
390        plugins.insert(
391            "oidc".to_owned(),
392            PluginConfig::Oidc {
393                tenant_claim: "tenants".to_owned(),
394                roles_claim: "roles".to_owned(),
395            },
396        );
397
398        let config = AuthConfig {
399            mode: AuthModeConfig {
400                provider: "oidc".to_owned(),
401            },
402            plugins,
403            ..Default::default()
404        };
405
406        // This would need a real plugin in registry to work
407        // Just testing that the structure compiles
408        let _ = &config;
409    }
410
411    // ===== Test Mocks =====
412
413    /// Mock `KeyProvider` for testing
414    struct MockKeyProvider {
415        name: String,
416        response: Option<(jsonwebtoken::Header, serde_json::Value)>,
417        error_msg: Option<String>,
418    }
419
420    impl MockKeyProvider {
421        fn success(header: jsonwebtoken::Header, claims: serde_json::Value) -> Self {
422            Self {
423                name: "mock-key-provider".to_owned(),
424                response: Some((header, claims)),
425                error_msg: None,
426            }
427        }
428
429        fn failure(error_msg: String) -> Self {
430            Self {
431                name: "mock-key-provider".to_owned(),
432                response: None,
433                error_msg: Some(error_msg),
434            }
435        }
436    }
437
438    #[async_trait::async_trait]
439    impl KeyProvider for MockKeyProvider {
440        fn name(&self) -> &str {
441            &self.name
442        }
443
444        async fn validate_and_decode(
445            &self,
446            _token: &str,
447        ) -> Result<(jsonwebtoken::Header, serde_json::Value), ClaimsError> {
448            if let Some(msg) = &self.error_msg {
449                Err(ClaimsError::Provider(msg.clone()))
450            } else {
451                Ok(self.response.clone().unwrap())
452            }
453        }
454
455        async fn refresh_keys(&self) -> Result<(), ClaimsError> {
456            Ok(())
457        }
458    }
459
460    /// Mock `IntrospectionProvider` for testing
461    struct MockIntrospectionProvider {
462        response: Option<serde_json::Value>,
463        error_msg: Option<String>,
464        name: String,
465    }
466
467    impl MockIntrospectionProvider {
468        fn success(response: serde_json::Value) -> Self {
469            Self {
470                response: Some(response),
471                error_msg: None,
472                name: "mock-introspection".to_owned(),
473            }
474        }
475
476        fn failure(error_msg: String) -> Self {
477            Self {
478                response: None,
479                error_msg: Some(error_msg),
480                name: "mock-introspection".to_owned(),
481            }
482        }
483    }
484
485    #[async_trait::async_trait]
486    impl IntrospectionProvider for MockIntrospectionProvider {
487        fn name(&self) -> &str {
488            &self.name
489        }
490
491        async fn introspect(&self, _token: &str) -> Result<serde_json::Value, ClaimsError> {
492            if let Some(msg) = &self.error_msg {
493                Err(ClaimsError::Provider(msg.clone()))
494            } else {
495                Ok(self.response.clone().unwrap())
496            }
497        }
498    }
499
500    /// Mock `ClaimsPlugin` for testing
501    struct MockClaimsPlugin {
502        name: String,
503        normalized: Option<Claims>,
504        error_msg: Option<String>,
505    }
506
507    impl MockClaimsPlugin {
508        fn success(normalized: Claims) -> Self {
509            Self {
510                name: "mock-plugin".to_owned(),
511                normalized: Some(normalized),
512                error_msg: None,
513            }
514        }
515
516        fn failure(error_msg: String) -> Self {
517            Self {
518                name: "mock-plugin".to_owned(),
519                normalized: None,
520                error_msg: Some(error_msg),
521            }
522        }
523    }
524
525    impl ClaimsPlugin for MockClaimsPlugin {
526        fn name(&self) -> &str {
527            &self.name
528        }
529
530        fn normalize(&self, _raw: &serde_json::Value) -> Result<Claims, ClaimsError> {
531            if let Some(msg) = &self.error_msg {
532                Err(ClaimsError::Malformed(msg.clone()))
533            } else {
534                Ok(self.normalized.clone().unwrap())
535            }
536        }
537    }
538
539    /// Helper to create test claims
540    fn test_claims() -> Claims {
541        Claims {
542            issuer: "https://test.example.com".to_owned(),
543            subject: Uuid::new_v4(),
544            audiences: vec!["test-api".to_owned()],
545            expires_at: Some(time::OffsetDateTime::now_utc() + time::Duration::hours(1)),
546            not_before: None,
547            issued_at: None,
548            jwt_id: None,
549            tenant_id: Uuid::new_v4(),
550            permissions: vec![],
551            extras: serde_json::Map::new(),
552        }
553    }
554
555    // ===== Tests for validate_jwt =====
556
557    #[tokio::test]
558    async fn test_validate_jwt_success() {
559        // Given: A dispatcher with mock key provider and plugin
560        let claims = test_claims();
561        let raw_claims = json!({
562            "iss": claims.issuer.clone(),
563            "sub": claims.subject.to_string(),
564            "aud": claims.audiences.clone(),
565            "exp": claims.expires_at.unwrap().unix_timestamp()
566        });
567
568        let header = jsonwebtoken::Header::default();
569        let key_provider = Arc::new(MockKeyProvider::success(header.clone(), raw_claims));
570        let plugin = Arc::new(MockClaimsPlugin::success(claims.clone()));
571
572        let validation_config = ValidationConfig {
573            allowed_issuers: vec!["https://test.example.com".to_owned()],
574            allowed_audiences: vec!["test-api".to_owned()],
575            leeway_seconds: 60,
576            require_uuid_subject: true,
577            require_uuid_tenants: true,
578        };
579
580        let dispatcher = AuthDispatcher {
581            key_providers: vec![key_provider],
582            introspection_providers: Vec::new(),
583            plugin,
584            validation_config,
585        };
586
587        // When: We validate a JWT token
588        let result = dispatcher.validate_jwt("test-token").await;
589
590        // Then: Validation succeeds
591        assert!(result.is_ok());
592        let normalized = result.unwrap();
593        assert_eq!(normalized.issuer, claims.issuer);
594    }
595
596    #[tokio::test]
597    async fn test_validate_jwt_no_matching_provider() {
598        // Given: A dispatcher with no key providers
599        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
600        let validation_config = ValidationConfig::default();
601
602        let dispatcher = AuthDispatcher {
603            key_providers: Vec::new(),
604            introspection_providers: Vec::new(),
605            plugin,
606            validation_config,
607        };
608
609        // When: We validate a JWT token
610        let result = dispatcher.validate_jwt("test-token").await;
611
612        // Then: Validation fails with NoMatchingProvider
613        assert!(result.is_err());
614        assert!(matches!(
615            result.unwrap_err(),
616            ClaimsError::NoMatchingProvider
617        ));
618    }
619
620    #[tokio::test]
621    async fn test_validate_jwt_provider_failure_fallback() {
622        // Given: Two providers, first fails, second succeeds
623        let claims = test_claims();
624        let raw_claims = json!({
625            "iss": claims.issuer.clone(),
626            "sub": claims.subject.to_string(),
627            "aud": claims.audiences.clone(),
628            "exp": claims.expires_at.unwrap().unix_timestamp()
629        });
630
631        let failing_provider =
632            Arc::new(MockKeyProvider::failure("First provider failed".to_owned()));
633        let header = jsonwebtoken::Header::default();
634        let success_provider = Arc::new(MockKeyProvider::success(header, raw_claims));
635        let plugin = Arc::new(MockClaimsPlugin::success(claims.clone()));
636
637        let validation_config = ValidationConfig {
638            allowed_issuers: vec!["https://test.example.com".to_owned()],
639            allowed_audiences: vec!["test-api".to_owned()],
640            leeway_seconds: 60,
641            require_uuid_subject: true,
642            require_uuid_tenants: true,
643        };
644
645        let dispatcher = AuthDispatcher {
646            key_providers: vec![failing_provider, success_provider],
647            introspection_providers: Vec::new(),
648            plugin,
649            validation_config,
650        };
651
652        // When: We validate the token
653        let result = dispatcher.validate_jwt("test-token").await;
654
655        // Then: Validation succeeds with second provider
656        assert!(result.is_ok());
657        let normalized = result.unwrap();
658        assert_eq!(normalized.issuer, claims.issuer);
659    }
660
661    #[tokio::test]
662    async fn test_validate_jwt_missing_issuer() {
663        // Given: Raw claims without issuer
664        let raw_claims = json!({
665            "sub": "user-123"
666        });
667
668        let header = jsonwebtoken::Header::default();
669        let key_provider = Arc::new(MockKeyProvider::success(header, raw_claims));
670        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
671
672        let validation_config = ValidationConfig::default();
673
674        let dispatcher = AuthDispatcher {
675            key_providers: vec![key_provider],
676            introspection_providers: Vec::new(),
677            plugin,
678            validation_config,
679        };
680
681        // When: We validate the token
682        let result = dispatcher.validate_jwt("test-token").await;
683
684        // Then: Validation fails with Malformed
685        assert!(result.is_err());
686        assert!(matches!(result.unwrap_err(), ClaimsError::Malformed(_)));
687    }
688
689    #[tokio::test]
690    async fn test_validate_jwt_normalization_failure() {
691        // Given: A plugin that fails normalization
692        let raw_claims = json!({
693            "iss": "https://test.example.com",
694            "sub": "user-123"
695        });
696
697        let header = jsonwebtoken::Header::default();
698        let key_provider = Arc::new(MockKeyProvider::success(header, raw_claims));
699        let plugin = Arc::new(MockClaimsPlugin::failure("Normalization failed".to_owned()));
700
701        let validation_config = ValidationConfig::default();
702
703        let dispatcher = AuthDispatcher {
704            key_providers: vec![key_provider],
705            introspection_providers: Vec::new(),
706            plugin,
707            validation_config,
708        };
709
710        // When: We validate the token
711        let result = dispatcher.validate_jwt("test-token").await;
712
713        // Then: Validation fails with normalization error
714        assert!(result.is_err());
715        assert!(matches!(result.unwrap_err(), ClaimsError::Malformed(_)));
716    }
717
718    #[tokio::test]
719    async fn test_validate_jwt_validation_failure() {
720        // Given: Claims that fail common validation (wrong issuer)
721        let mut claims = test_claims();
722        claims.issuer = "https://wrong.example.com".to_owned();
723
724        let raw_claims = json!({
725            "iss": "https://wrong.example.com",
726            "sub": claims.subject.to_string(),
727            "aud": claims.audiences.clone(),
728            "exp": claims.expires_at.unwrap().unix_timestamp()
729        });
730
731        let header = jsonwebtoken::Header::default();
732        let key_provider = Arc::new(MockKeyProvider::success(header, raw_claims));
733        let plugin = Arc::new(MockClaimsPlugin::success(claims));
734
735        let validation_config = ValidationConfig {
736            allowed_issuers: vec!["https://test.example.com".to_owned()],
737            allowed_audiences: vec!["test-api".to_owned()],
738            leeway_seconds: 60,
739            require_uuid_subject: true,
740            require_uuid_tenants: true,
741        };
742
743        let dispatcher = AuthDispatcher {
744            key_providers: vec![key_provider],
745            introspection_providers: Vec::new(),
746            plugin,
747            validation_config,
748        };
749
750        // When: We validate the token
751        let result = dispatcher.validate_jwt("test-token").await;
752
753        // Then: Validation fails with InvalidIssuer
754        assert!(result.is_err());
755        assert!(matches!(
756            result.unwrap_err(),
757            ClaimsError::InvalidIssuer { .. }
758        ));
759    }
760
761    // ===== Regression Tests for validate_opaque =====
762
763    #[tokio::test]
764    async fn test_validate_opaque_success() {
765        // Given: A dispatcher with mock provider and plugin
766        let introspection_response = json!({
767            "active": true,
768            "iss": "https://test.example.com",
769            "sub": "user-123"
770        });
771
772        let claims = test_claims();
773        let provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
774        let plugin = Arc::new(MockClaimsPlugin::success(claims.clone()));
775
776        let validation_config = ValidationConfig {
777            allowed_issuers: vec!["https://test.example.com".to_owned()],
778            allowed_audiences: vec!["test-api".to_owned()],
779            leeway_seconds: 60,
780            require_uuid_subject: true,
781            require_uuid_tenants: true,
782        };
783
784        let dispatcher = AuthDispatcher {
785            key_providers: Vec::new(),
786            introspection_providers: vec![provider],
787            plugin,
788            validation_config,
789        };
790
791        // When: We validate an opaque token
792        let result = dispatcher.validate_opaque("test-token").await;
793
794        // Then: Validation succeeds
795        assert!(result.is_ok());
796        let normalized = result.unwrap();
797        assert_eq!(normalized.issuer, claims.issuer);
798    }
799
800    #[tokio::test]
801    async fn test_validate_opaque_inactive_token() {
802        // Given: A response with active=false
803        let introspection_response = json!({
804            "active": false,
805            "iss": "https://test.example.com"
806        });
807
808        let provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
809        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
810
811        let validation_config = ValidationConfig {
812            allowed_issuers: vec!["https://test.example.com".to_owned()],
813            allowed_audiences: vec!["test-api".to_owned()],
814            leeway_seconds: 60,
815            require_uuid_subject: true,
816            require_uuid_tenants: true,
817        };
818
819        let dispatcher = AuthDispatcher {
820            key_providers: Vec::new(),
821            introspection_providers: vec![provider],
822            plugin,
823            validation_config,
824        };
825
826        // When: We validate the token
827        let result = dispatcher.validate_opaque("test-token").await;
828
829        // Then: Validation fails with IntrospectionDenied
830        assert!(result.is_err());
831        assert!(matches!(
832            result.unwrap_err(),
833            ClaimsError::IntrospectionDenied
834        ));
835    }
836
837    #[tokio::test]
838    async fn test_validate_opaque_missing_issuer() {
839        // Given: A response without issuer
840        let introspection_response = json!({
841            "active": true,
842            "sub": "user-123"
843        });
844
845        let provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
846        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
847
848        let validation_config = ValidationConfig::default();
849
850        let dispatcher = AuthDispatcher {
851            key_providers: Vec::new(),
852            introspection_providers: vec![provider],
853            plugin,
854            validation_config,
855        };
856
857        // When: We validate the token
858        let result = dispatcher.validate_opaque("test-token").await;
859
860        // Then: Validation fails with Malformed
861        assert!(result.is_err());
862        assert!(matches!(result.unwrap_err(), ClaimsError::Malformed(_)));
863    }
864
865    #[tokio::test]
866    async fn test_validate_opaque_provider_failure() {
867        // Given: A provider that fails
868        let provider = Arc::new(MockIntrospectionProvider::failure(
869            "Provider error".to_owned(),
870        ));
871        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
872
873        let validation_config = ValidationConfig::default();
874
875        let dispatcher = AuthDispatcher {
876            key_providers: Vec::new(),
877            introspection_providers: vec![provider],
878            plugin,
879            validation_config,
880        };
881
882        // When: We validate the token
883        let result = dispatcher.validate_opaque("test-token").await;
884
885        // Then: Validation fails with Provider error
886        assert!(result.is_err());
887        assert!(matches!(result.unwrap_err(), ClaimsError::Provider(_)));
888    }
889
890    #[tokio::test]
891    async fn test_validate_opaque_no_providers() {
892        // Given: A dispatcher with no providers
893        let plugin = Arc::new(MockClaimsPlugin::success(test_claims()));
894        let validation_config = ValidationConfig::default();
895
896        let dispatcher = AuthDispatcher {
897            key_providers: Vec::new(),
898            introspection_providers: Vec::new(),
899            plugin,
900            validation_config,
901        };
902
903        // When: We validate the token
904        let result = dispatcher.validate_opaque("test-token").await;
905
906        // Then: Validation fails
907        assert!(result.is_err());
908    }
909
910    #[tokio::test]
911    async fn test_validate_opaque_normalization_failure() {
912        // Given: A plugin that fails normalization
913        let introspection_response = json!({
914            "active": true,
915            "iss": "https://test.example.com",
916            "sub": "user-123"
917        });
918
919        let provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
920        let plugin = Arc::new(MockClaimsPlugin::failure("Normalization failed".to_owned()));
921
922        let validation_config = ValidationConfig::default();
923
924        let dispatcher = AuthDispatcher {
925            key_providers: Vec::new(),
926            introspection_providers: vec![provider],
927            plugin,
928            validation_config,
929        };
930
931        // When: We validate the token
932        let result = dispatcher.validate_opaque("test-token").await;
933
934        // Then: Validation fails with normalization error
935        assert!(result.is_err());
936        assert!(matches!(result.unwrap_err(), ClaimsError::Malformed(_)));
937    }
938
939    #[tokio::test]
940    async fn test_validate_opaque_validation_failure() {
941        // Given: Claims that fail common validation (wrong issuer)
942        let introspection_response = json!({
943            "active": true,
944            "iss": "https://wrong.example.com",
945            "sub": "user-123"
946        });
947
948        let mut claims = test_claims();
949        claims.issuer = "https://wrong.example.com".to_owned();
950
951        let provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
952        let plugin = Arc::new(MockClaimsPlugin::success(claims));
953
954        let validation_config = ValidationConfig {
955            allowed_issuers: vec!["https://test.example.com".to_owned()],
956            allowed_audiences: vec!["test-api".to_owned()],
957            leeway_seconds: 60,
958            require_uuid_subject: true,
959            require_uuid_tenants: true,
960        };
961
962        let dispatcher = AuthDispatcher {
963            key_providers: Vec::new(),
964            introspection_providers: vec![provider],
965            plugin,
966            validation_config,
967        };
968
969        // When: We validate the token
970        let result = dispatcher.validate_opaque("test-token").await;
971
972        // Then: Validation fails with InvalidIssuer
973        assert!(result.is_err());
974        assert!(matches!(
975            result.unwrap_err(),
976            ClaimsError::InvalidIssuer { .. }
977        ));
978    }
979
980    #[tokio::test]
981    async fn test_validate_opaque_provider_fallback() {
982        // Given: Two providers, first fails, second succeeds
983        let failing_provider = Arc::new(MockIntrospectionProvider::failure(
984            "First provider failed".to_owned(),
985        ));
986
987        let introspection_response = json!({
988            "active": true,
989            "iss": "https://test.example.com",
990            "sub": "user-123"
991        });
992
993        let success_provider = Arc::new(MockIntrospectionProvider::success(introspection_response));
994        let claims = test_claims();
995        let plugin = Arc::new(MockClaimsPlugin::success(claims.clone()));
996
997        let validation_config = ValidationConfig {
998            allowed_issuers: vec!["https://test.example.com".to_owned()],
999            allowed_audiences: vec!["test-api".to_owned()],
1000            leeway_seconds: 60,
1001            require_uuid_subject: true,
1002            require_uuid_tenants: true,
1003        };
1004
1005        let dispatcher = AuthDispatcher {
1006            key_providers: Vec::new(),
1007            introspection_providers: vec![failing_provider, success_provider],
1008            plugin,
1009            validation_config,
1010        };
1011
1012        // When: We validate the token
1013        let result = dispatcher.validate_opaque("test-token").await;
1014
1015        // Then: Validation succeeds with second provider
1016        assert!(result.is_ok());
1017        let normalized = result.unwrap();
1018        assert_eq!(normalized.issuer, claims.issuer);
1019    }
1020}