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
16fn truncate_uuid(uuid: &Uuid) -> String {
18 let s = uuid.to_string();
19 s.chars().take(8).collect()
20}
21
22#[must_use]
27pub struct AuthDispatcher {
28 key_providers: Vec<Arc<dyn KeyProvider>>,
30
31 introspection_providers: Vec<Arc<dyn IntrospectionProvider>>,
33
34 plugin: Arc<dyn ClaimsPlugin>,
36
37 validation_config: ValidationConfig,
39}
40
41impl AuthDispatcher {
42 pub fn new(
47 validation_config: ValidationConfig,
48 config: &AuthConfig,
49 registry: &PluginRegistry,
50 ) -> Result<Self, ConfigError> {
51 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 pub fn with_key_provider(mut self, provider: Arc<dyn KeyProvider>) -> Self {
64 self.key_providers.push(provider);
65 self
66 }
67
68 pub fn with_introspection_provider(mut self, provider: Arc<dyn IntrospectionProvider>) -> Self {
70 self.introspection_providers.push(provider);
71 self
72 }
73
74 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 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 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 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 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 pub async fn validate_jwt(&self, token: &str) -> Result<Claims, ClaimsError> {
181 let (header, raw_claims) = self.try_validate_with_providers(token).await?;
183
184 let issuer = Self::extract_issuer_from_claims(&raw_claims)?;
186
187 let normalized = self.normalize_claims_with_logging(&raw_claims, issuer)?;
189
190 Self::validate_claims_with_logging(&normalized, &self.validation_config)?;
192
193 Self::log_jwt_success(&normalized, self.plugin.name(), header.kid.as_ref());
195
196 Ok(normalized)
197 }
198
199 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 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 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 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 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 pub async fn validate_opaque(&self, token: &str) -> Result<Claims, ClaimsError> {
301 let introspection_result = self.try_introspect_with_providers(token).await?;
303
304 Self::verify_token_active(&introspection_result)?;
306
307 let issuer = Self::extract_issuer_from_claims(&introspection_result)?;
309
310 tracing::debug!(
312 plugin = self.plugin.name(),
313 issuer = issuer,
314 "Using configured plugin for introspection"
315 );
316
317 let normalized = self.normalize_with_logging(&introspection_result, issuer)?;
319
320 Self::validate_and_log(&normalized, &self.validation_config)?;
322
323 Self::log_validation_success(&normalized, self.plugin.name());
325
326 Ok(normalized)
327 }
328
329 #[must_use]
331 pub fn validation_config(&self) -> &ValidationConfig {
332 &self.validation_config
333 }
334
335 #[must_use]
337 pub fn plugin(&self) -> &Arc<dyn ClaimsPlugin> {
338 &self.plugin
339 }
340
341 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#[async_trait]
369impl TokenValidator for AuthDispatcher {
370 async fn validate_and_parse(&self, token: &str) -> Result<Claims, AuthError> {
371 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 let _ = &config;
409 }
410
411 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 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 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 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 #[tokio::test]
558 async fn test_validate_jwt_success() {
559 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 let result = dispatcher.validate_jwt("test-token").await;
589
590 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 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 let result = dispatcher.validate_jwt("test-token").await;
611
612 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 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 let result = dispatcher.validate_jwt("test-token").await;
654
655 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 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 let result = dispatcher.validate_jwt("test-token").await;
683
684 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 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 let result = dispatcher.validate_jwt("test-token").await;
712
713 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 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 let result = dispatcher.validate_jwt("test-token").await;
752
753 assert!(result.is_err());
755 assert!(matches!(
756 result.unwrap_err(),
757 ClaimsError::InvalidIssuer { .. }
758 ));
759 }
760
761 #[tokio::test]
764 async fn test_validate_opaque_success() {
765 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 let result = dispatcher.validate_opaque("test-token").await;
793
794 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 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 let result = dispatcher.validate_opaque("test-token").await;
828
829 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 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 let result = dispatcher.validate_opaque("test-token").await;
859
860 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 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 let result = dispatcher.validate_opaque("test-token").await;
884
885 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 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 let result = dispatcher.validate_opaque("test-token").await;
905
906 assert!(result.is_err());
908 }
909
910 #[tokio::test]
911 async fn test_validate_opaque_normalization_failure() {
912 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 let result = dispatcher.validate_opaque("test-token").await;
933
934 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 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 let result = dispatcher.validate_opaque("test-token").await;
971
972 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 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 let result = dispatcher.validate_opaque("test-token").await;
1014
1015 assert!(result.is_ok());
1017 let normalized = result.unwrap();
1018 assert_eq!(normalized.issuer, claims.issuer);
1019 }
1020}