jwt_verify/oidc/config.rs
1use std::collections::HashSet;
2use std::time::Duration;
3
4use crate::claims::ClaimValidator;
5use crate::common::error::{ErrorVerbosity, JwtError};
6use crate::oidc::discovery::{DiscoveryDocument, OidcDiscovery};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TokenUse {
10 /// ID token - used for authentication and contains user identity information
11 Id,
12 /// Access token - used for authorization and contains scopes/permissions
13 Access,
14}
15
16impl TokenUse {
17 /// Get the string representation of the token use
18 pub fn as_str(&self) -> &'static str {
19 match self {
20 TokenUse::Id => "id",
21 TokenUse::Access => "access",
22 }
23 }
24
25 /// Create a TokenUse from a string
26 pub fn from_str(s: &str) -> Option<Self> {
27 match s {
28 "id" => Some(TokenUse::Id),
29 "access" => Some(TokenUse::Access),
30 _ => None,
31 }
32 }
33}
34
35/// Configuration for OIDC providers.
36///
37/// This struct holds all configuration options for verifying JWTs from OIDC-compatible
38/// identity providers, including issuer URL, JWKS URL, allowed client IDs, token types,
39/// clock skew, cache duration, required claims, custom validators, and error verbosity.
40///
41/// The configuration is immutable after creation, but can be modified using the builder
42/// pattern methods like `with_clock_skew`, `with_cache_duration`, etc.
43///
44/// # Examples
45///
46/// ```
47/// use jwt_verify::oidc::OidcProviderConfig;
48/// use std::time::Duration;
49///
50/// // Create a basic configuration
51/// let config = OidcProviderConfig::new(
52/// "https://accounts.example.com",
53/// Some("https://accounts.example.com/.well-known/jwks.json"),
54/// &["client1".to_string()],
55/// ).unwrap();
56///
57/// // Create a configuration with custom settings
58/// let config = OidcProviderConfig::new(
59/// "https://accounts.example.com",
60/// None, // Will use discovery to find JWKS URL
61/// &["client1".to_string()],
62/// ).unwrap()
63/// .with_clock_skew(Duration::from_secs(120))
64/// .with_cache_duration(Duration::from_secs(3600 * 12));
65/// ```
66pub struct OidcProviderConfig {
67 /// OIDC issuer URL
68 pub issuer: String,
69 /// JWKS URL (optional, can be discovered from issuer)
70 pub jwks_url: Option<String>,
71 /// List of allowed client IDs for this provider
72 pub client_ids: Vec<String>,
73 /// List of allowed token types (ID tokens, Access tokens)
74 pub allowed_token_uses: Vec<TokenUse>,
75 /// Clock skew tolerance for token expiration and issuance time validation
76 pub clock_skew: Duration,
77 /// Duration for which JWKs are cached before refreshing
78 pub jwk_cache_duration: Duration,
79 /// Duration for which discovery documents are cached before refreshing
80 pub discovery_cache_duration: Duration,
81 /// Set of claims that must be present and valid in the token
82 pub required_claims: HashSet<String>,
83 /// List of custom validators for additional claim validation
84 #[allow(clippy::type_complexity)]
85 pub custom_validators: Vec<Box<dyn ClaimValidator + Send + Sync>>,
86 /// Level of detail in error messages
87 pub error_verbosity: ErrorVerbosity,
88 /// Whether to use auto-discovery for JWKS URL
89 pub use_discovery: bool,
90}
91
92// Manual implementation of Debug for OidcProviderConfig since custom_validators doesn't implement Debug
93impl std::fmt::Debug for OidcProviderConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("OidcProviderConfig")
96 .field("issuer", &self.issuer)
97 .field("jwks_url", &self.jwks_url)
98 .field("client_ids", &self.client_ids)
99 .field("clock_skew", &self.clock_skew)
100 .field("jwk_cache_duration", &self.jwk_cache_duration)
101 .field("discovery_cache_duration", &self.discovery_cache_duration)
102 .field("required_claims", &self.required_claims)
103 .field(
104 "custom_validators",
105 &format!("[{} validators]", self.custom_validators.len()),
106 )
107 .field("error_verbosity", &self.error_verbosity)
108 .field("use_discovery", &self.use_discovery)
109 .finish()
110 }
111}
112
113// Manual implementation of Clone for OidcProviderConfig since custom_validators doesn't implement Clone
114impl Clone for OidcProviderConfig {
115 fn clone(&self) -> Self {
116 Self {
117 issuer: self.issuer.clone(),
118 jwks_url: self.jwks_url.clone(),
119 client_ids: self.client_ids.clone(),
120 allowed_token_uses: self.allowed_token_uses.clone(),
121 clock_skew: self.clock_skew,
122 jwk_cache_duration: self.jwk_cache_duration,
123 discovery_cache_duration: self.discovery_cache_duration,
124 required_claims: self.required_claims.clone(),
125 custom_validators: Vec::new(), // Custom validators can't be cloned, so we create an empty vector
126 error_verbosity: self.error_verbosity,
127 use_discovery: self.use_discovery,
128 }
129 }
130}
131
132impl OidcProviderConfig {
133 /// Create a new OIDC provider configuration with validation for required parameters.
134 ///
135 /// This method creates a new `OidcProviderConfig` with the specified issuer URL,
136 /// optional JWKS URL, and client IDs. It validates that the issuer URL is not empty
137 /// and is a valid URL.
138 ///
139 /// # Parameters
140 ///
141 /// * `issuer` - OIDC issuer URL (e.g., "https://accounts.example.com")
142 /// * `jwks_url` - Optional JWKS URL (if None, will be discovered from issuer)
143 /// * `client_ids` - List of allowed client IDs for this provider
144 ///
145 /// # Returns
146 ///
147 /// Returns a `Result` containing the new `OidcProviderConfig` if successful, or a `JwtError`
148 /// if validation fails.
149 ///
150 /// # Errors
151 ///
152 /// Returns a `JwtError::ConfigurationError` if:
153 /// - The issuer URL is empty
154 /// - The issuer URL is not a valid URL
155 /// - The JWKS URL is provided but is not a valid URL
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use jwt_verify::oidc::OidcProviderConfig;
161 ///
162 /// // Create a basic configuration
163 /// let config = OidcProviderConfig::new(
164 /// "https://accounts.example.com",
165 /// Some("https://accounts.example.com/.well-known/jwks.json"),
166 /// &["client1".to_string()],
167 /// ).unwrap();
168 /// ```
169 pub fn new(
170 issuer: &str,
171 jwks_url: Option<&str>,
172 client_ids: &[String],
173 token_uses: Option<Vec<TokenUse>>,
174 ) -> Result<Self, JwtError> {
175 // Validate issuer
176 if issuer.is_empty() {
177 return Err(JwtError::ConfigurationError {
178 parameter: Some("issuer".to_string()),
179 error: "Issuer URL cannot be empty".to_string(),
180 });
181 }
182
183 // Validate issuer URL format
184 if !issuer.starts_with("http://") && !issuer.starts_with("https://") {
185 return Err(JwtError::ConfigurationError {
186 parameter: Some("issuer".to_string()),
187 error: "Issuer URL must start with http:// or https://".to_string(),
188 });
189 }
190
191 // Validate JWKS URL format if provided
192 if let Some(url) = jwks_url {
193 if !url.starts_with("http://") && !url.starts_with("https://") {
194 return Err(JwtError::ConfigurationError {
195 parameter: Some("jwks_url".to_string()),
196 error: "JWKS URL must start with http:// or https://".to_string(),
197 });
198 }
199 }
200
201 let token_uses = match token_uses {
202 None => vec![TokenUse::Id, TokenUse::Access],
203 Some(tu) => tu,
204 };
205
206 Ok(Self {
207 issuer: issuer.to_string(),
208 jwks_url: jwks_url.map(|s| s.to_string()),
209 client_ids: client_ids.to_vec(),
210 allowed_token_uses: token_uses,
211 clock_skew: Duration::from_secs(60), // Default: 1 minute
212 jwk_cache_duration: Duration::from_secs(3600 * 24), // Default: 24 hours
213 discovery_cache_duration: Duration::from_secs(3600 * 24), // Default: 24 hours
214 required_claims: HashSet::from([
215 "sub".to_string(),
216 "iss".to_string(),
217 "aud".to_string(),
218 "exp".to_string(),
219 "iat".to_string(),
220 ]),
221 custom_validators: Vec::new(),
222 error_verbosity: ErrorVerbosity::Standard,
223 use_discovery: jwks_url.is_none(), // Use discovery if no JWKS URL is provided
224 })
225 }
226
227 /// Create a new OIDC provider configuration with auto-discovery.
228 ///
229 /// This method creates a new `OidcProviderConfig` that will use auto-discovery
230 /// to find the JWKS URL and other OIDC provider configuration.
231 ///
232 /// # Parameters
233 ///
234 /// * `issuer` - OIDC issuer URL (e.g., "https://accounts.example.com")
235 /// * `client_ids` - List of allowed client IDs for this provider
236 ///
237 /// # Returns
238 ///
239 /// Returns a `Result` containing the new `OidcProviderConfig` if successful, or a `JwtError`
240 /// if validation fails.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use jwt_verify::oidc::OidcProviderConfig;
246 ///
247 /// // Create a configuration with auto-discovery
248 /// let config = OidcProviderConfig::with_discovery(
249 /// "https://accounts.example.com",
250 /// &["client1".to_string()],
251 /// ).unwrap();
252 /// ```
253 pub fn with_discovery(issuer: &str, client_ids: &[String]) -> Result<Self, JwtError> {
254 let mut config = Self::new(issuer, None, client_ids, None)?;
255 config.use_discovery = true;
256 Ok(config)
257 }
258
259 /// Enable or disable auto-discovery.
260 ///
261 /// When auto-discovery is enabled, the JWKS URL will be discovered from the
262 /// OIDC provider's well-known endpoint. When disabled, the JWKS URL must be
263 /// provided explicitly.
264 ///
265 /// # Parameters
266 ///
267 /// * `use_discovery` - Whether to use auto-discovery
268 ///
269 /// # Returns
270 ///
271 /// Returns a new `OidcProviderConfig` with the updated auto-discovery setting.
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use jwt_verify::oidc::OidcProviderConfig;
277 ///
278 /// let config = OidcProviderConfig::new(
279 /// "https://accounts.example.com",
280 /// Some("https://accounts.example.com/.well-known/jwks.json"),
281 /// &["client1".to_string()],
282 /// ).unwrap()
283 /// .set_discovery_enabled(true); // Enable auto-discovery even though JWKS URL is provided
284 /// ```
285 pub fn set_discovery_enabled(mut self, use_discovery: bool) -> Self {
286 self.use_discovery = use_discovery;
287 self
288 }
289
290 /// Set clock skew tolerance for token validation.
291 ///
292 /// Clock skew is used to account for time differences between the token issuer
293 /// and the token verifier. This is important for validating token expiration
294 /// and issuance times.
295 ///
296 /// # Parameters
297 ///
298 /// * `skew` - The clock skew duration to allow (default: 60 seconds)
299 ///
300 /// # Returns
301 ///
302 /// Returns a new `OidcProviderConfig` with the updated clock skew.
303 ///
304 /// # Examples
305 ///
306 /// ```
307 /// use jwt_verify::oidc::OidcProviderConfig;
308 /// use std::time::Duration;
309 ///
310 /// let config = OidcProviderConfig::new(
311 /// "https://accounts.example.com",
312 /// None,
313 /// &["client1".to_string()],
314 /// ).unwrap()
315 /// .with_clock_skew(Duration::from_secs(120)); // 2 minutes
316 /// ```
317 pub fn with_clock_skew(mut self, skew: Duration) -> Self {
318 self.clock_skew = skew;
319 self
320 }
321
322 /// Set JWK cache duration for key management.
323 ///
324 /// This determines how long JWKs (JSON Web Keys) are cached before being refreshed
325 /// from the OIDC provider endpoint. Longer durations reduce network requests but may
326 /// delay key rotation recognition.
327 ///
328 /// # Parameters
329 ///
330 /// * `duration` - The cache duration (default: 24 hours)
331 ///
332 /// # Returns
333 ///
334 /// Returns a new `OidcProviderConfig` with the updated cache duration.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use jwt_verify::oidc::OidcProviderConfig;
340 /// use std::time::Duration;
341 ///
342 /// let config = OidcProviderConfig::new(
343 /// "https://accounts.example.com",
344 /// None,
345 /// &["client1".to_string()],
346 /// ).unwrap()
347 /// .with_cache_duration(Duration::from_secs(3600 * 12)); // 12 hours
348 /// ```
349 pub fn with_cache_duration(mut self, duration: Duration) -> Self {
350 self.jwk_cache_duration = duration;
351 self
352 }
353
354 /// Set discovery cache duration.
355 ///
356 /// This determines how long OIDC discovery documents are cached before being refreshed
357 /// from the OIDC provider endpoint. Longer durations reduce network requests but may
358 /// delay configuration changes recognition.
359 ///
360 /// # Parameters
361 ///
362 /// * `duration` - The cache duration (default: 24 hours)
363 ///
364 /// # Returns
365 ///
366 /// Returns a new `OidcProviderConfig` with the updated discovery cache duration.
367 ///
368 /// # Examples
369 ///
370 /// ```
371 /// use jwt_verify::oidc::OidcProviderConfig;
372 /// use std::time::Duration;
373 ///
374 /// let config = OidcProviderConfig::new(
375 /// "https://accounts.example.com",
376 /// None,
377 /// &["client1".to_string()],
378 /// ).unwrap()
379 /// .with_discovery_cache_duration(Duration::from_secs(3600 * 12)); // 12 hours
380 /// ```
381 pub fn with_discovery_cache_duration(mut self, duration: Duration) -> Self {
382 self.discovery_cache_duration = duration;
383 self
384 }
385
386 /// Add a required claim to the validation process.
387 ///
388 /// Required claims must be present in the token and will be validated.
389 /// By default, the following claims are required: "sub", "iss", "aud", "exp", "iat".
390 ///
391 /// # Parameters
392 ///
393 /// * `claim` - The name of the claim to require
394 ///
395 /// # Returns
396 ///
397 /// Returns a new `OidcProviderConfig` with the added required claim.
398 ///
399 /// # Examples
400 ///
401 /// ```
402 /// use jwt_verify::oidc::OidcProviderConfig;
403 ///
404 /// let config = OidcProviderConfig::new(
405 /// "https://accounts.example.com",
406 /// None,
407 /// &["client1".to_string()],
408 /// ).unwrap()
409 /// .with_required_claim("nonce");
410 /// ```
411 pub fn with_required_claim(mut self, claim: &str) -> Self {
412 self.required_claims.insert(claim.to_string());
413 self
414 }
415
416 /// Add a custom validator for additional claim validation.
417 ///
418 /// Custom validators allow for application-specific validation logic beyond
419 /// the standard JWT claim validation. They can validate specific claim values,
420 /// formats, or relationships between claims.
421 ///
422 /// # Parameters
423 ///
424 /// * `validator` - A boxed implementation of the `ClaimValidator` trait
425 ///
426 /// # Returns
427 ///
428 /// Returns a new `OidcProviderConfig` with the added custom validator.
429 ///
430 /// # Examples
431 ///
432 /// ```
433 /// use jwt_verify::{oidc::OidcProviderConfig, StringValueValidator};
434 ///
435 /// let config = OidcProviderConfig::new(
436 /// "https://accounts.example.com",
437 /// None,
438 /// &["client1".to_string()],
439 /// ).unwrap()
440 /// .with_custom_validator(Box::new(StringValueValidator::new(
441 /// "app_id", "my-application"
442 /// )));
443 /// ```
444 pub fn with_custom_validator(
445 mut self,
446 validator: Box<dyn ClaimValidator + Send + Sync>,
447 ) -> Self {
448 self.custom_validators.push(validator);
449 self
450 }
451
452 /// Set the error verbosity level for error reporting.
453 ///
454 /// This controls how much detail is included in error messages and logs.
455 /// Higher verbosity levels include more information but may expose sensitive data.
456 ///
457 /// # Parameters
458 ///
459 /// * `verbosity` - The error verbosity level (default: Standard)
460 ///
461 /// # Returns
462 ///
463 /// Returns a new `OidcProviderConfig` with the updated error verbosity.
464 ///
465 /// # Examples
466 ///
467 /// ```
468 /// use jwt_verify::{oidc::OidcProviderConfig, ErrorVerbosity};
469 ///
470 /// let config = OidcProviderConfig::new(
471 /// "https://accounts.example.com",
472 /// None,
473 /// &["client1".to_string()],
474 /// ).unwrap()
475 /// .with_error_verbosity(ErrorVerbosity::Detailed);
476 /// ```
477 pub fn with_error_verbosity(mut self, verbosity: ErrorVerbosity) -> Self {
478 self.error_verbosity = verbosity;
479 self
480 }
481
482 /// Get the well-known configuration URL for the OIDC provider.
483 ///
484 /// This URL is used to discover the OIDC provider configuration, including
485 /// the JWKS URL, if not explicitly provided.
486 ///
487 /// # Returns
488 ///
489 /// Returns the well-known configuration URL for the OIDC provider.
490 pub fn get_well_known_url(&self) -> String {
491 format!(
492 "{}/.well-known/openid-configuration",
493 self.issuer.trim_end_matches('/')
494 )
495 }
496
497 /// Discover the JWKS URL from the OIDC provider's well-known endpoint.
498 ///
499 /// This method fetches the OIDC provider configuration from the well-known
500 /// endpoint and extracts the JWKS URL.
501 ///
502 /// # Parameters
503 ///
504 /// * `discovery` - The OIDC discovery service to use
505 ///
506 /// # Returns
507 ///
508 /// Returns a `Result` containing the JWKS URL if successful, or a `JwtError`
509 /// if discovery fails.
510 ///
511 /// # Examples
512 ///
513 /// ```
514 /// use jwt_verify::oidc::{OidcProviderConfig, OidcDiscovery};
515 /// use std::time::Duration;
516 ///
517 /// // Create a discovery service
518 /// let discovery = OidcDiscovery::new(Duration::from_secs(3600));
519 ///
520 /// // Create a configuration
521 /// let config = OidcProviderConfig::new(
522 /// "https://accounts.example.com",
523 /// None,
524 /// &["client1".to_string()],
525 /// ).unwrap();
526 ///
527 /// // Discover the JWKS URL
528 /// // let jwks_url = config.discover_jwks_url(&discovery).await.unwrap();
529 /// ```
530 pub async fn discover_jwks_url(&self, discovery: &OidcDiscovery) -> Result<String, JwtError> {
531 // If we have a JWKS URL and aren't using discovery, return it
532 if let Some(url) = &self.jwks_url {
533 if !self.use_discovery {
534 return Ok(url.clone());
535 }
536 }
537
538 // Otherwise, discover the JWKS URL
539 let document = discovery
540 .discover_with_fallback(&self.issuer, self.jwks_url.as_deref())
541 .await?;
542 Ok(document.jwks_uri.clone())
543 }
544
545 /// Discover the OIDC provider configuration.
546 ///
547 /// This method fetches the OIDC provider configuration from the well-known
548 /// endpoint or uses the provided JWKS URL to create a minimal configuration.
549 ///
550 /// # Parameters
551 ///
552 /// * `discovery` - The OIDC discovery service to use
553 ///
554 /// # Returns
555 ///
556 /// Returns a `Result` containing the discovery document if successful, or a `JwtError`
557 /// if discovery fails.
558 ///
559 /// # Examples
560 ///
561 /// ```
562 /// use jwt_verify::oidc::{OidcProviderConfig, OidcDiscovery};
563 /// use std::time::Duration;
564 ///
565 /// // Create a discovery service
566 /// let discovery = OidcDiscovery::new(Duration::from_secs(3600));
567 ///
568 /// // Create a configuration
569 /// let config = OidcProviderConfig::new(
570 /// "https://accounts.example.com",
571 /// None,
572 /// &["client1".to_string()],
573 /// ).unwrap();
574 ///
575 /// // Discover the OIDC provider configuration
576 /// // let document = config.discover(&discovery).await.unwrap();
577 /// ```
578 pub async fn discover(&self, discovery: &OidcDiscovery) -> Result<DiscoveryDocument, JwtError> {
579 if self.use_discovery {
580 // Use discovery to get the full configuration
581 discovery
582 .discover_with_fallback(&self.issuer, self.jwks_url.as_deref())
583 .await
584 } else if let Some(jwks_url) = &self.jwks_url {
585 // Create a minimal configuration with the provided JWKS URL
586 Ok(DiscoveryDocument::new(&self.issuer, jwks_url))
587 } else {
588 // No JWKS URL and not using discovery
589 Err(JwtError::ConfigurationError {
590 parameter: Some("jwks_url".to_string()),
591 error: "JWKS URL is required when auto-discovery is disabled".to_string(),
592 })
593 }
594 }
595
596 /// Create a new OIDC discovery service based on the configuration.
597 ///
598 /// This method creates a new `OidcDiscovery` instance with the cache duration
599 /// specified in the configuration.
600 ///
601 /// # Returns
602 ///
603 /// Returns a new `OidcDiscovery` instance.
604 ///
605 /// # Examples
606 ///
607 /// ```
608 /// use jwt_verify::oidc::OidcProviderConfig;
609 ///
610 /// // Create a configuration
611 /// let config = OidcProviderConfig::new(
612 /// "https://accounts.example.com",
613 /// None,
614 /// &["client1".to_string()],
615 /// ).unwrap();
616 ///
617 /// // Create a discovery service
618 /// let discovery = config.create_discovery();
619 /// ```
620 pub fn create_discovery(&self) -> OidcDiscovery {
621 OidcDiscovery::new(self.discovery_cache_duration)
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_new_valid_config() {
631 let config = OidcProviderConfig::new(
632 "https://accounts.example.com",
633 Some("https://accounts.example.com/.well-known/jwks.json"),
634 &["client1".to_string()],
635 None,
636 );
637 assert!(config.is_ok());
638 let config = config.unwrap();
639 assert_eq!(config.issuer, "https://accounts.example.com");
640 assert_eq!(
641 config.jwks_url,
642 Some("https://accounts.example.com/.well-known/jwks.json".to_string())
643 );
644 assert_eq!(config.client_ids, vec!["client1".to_string()]);
645 assert!(!config.use_discovery); // Should not use discovery when JWKS URL is provided
646 }
647
648 #[test]
649 fn test_new_with_discovery() {
650 let config = OidcProviderConfig::new(
651 "https://accounts.example.com",
652 None,
653 &["client1".to_string()],
654 None,
655 );
656 assert!(config.is_ok());
657 let config = config.unwrap();
658 assert_eq!(config.issuer, "https://accounts.example.com");
659 assert_eq!(config.jwks_url, None);
660 assert!(config.use_discovery); // Should use discovery when no JWKS URL is provided
661 }
662
663 #[test]
664 fn test_with_discovery_explicit() {
665 let config = OidcProviderConfig::with_discovery(
666 "https://accounts.example.com",
667 &["client1".to_string()],
668 );
669 assert!(config.is_ok());
670 let config = config.unwrap();
671 assert_eq!(config.issuer, "https://accounts.example.com");
672 assert_eq!(config.jwks_url, None);
673 assert!(config.use_discovery); // Should use discovery when explicitly requested
674 }
675
676 #[test]
677 fn test_new_empty_issuer() {
678 let config = OidcProviderConfig::new(
679 "",
680 Some("https://accounts.example.com/.well-known/jwks.json"),
681 &["client1".to_string()],
682 None,
683 );
684 assert!(config.is_err());
685 match config.unwrap_err() {
686 JwtError::ConfigurationError { parameter, .. } => {
687 assert_eq!(parameter, Some("issuer".to_string()));
688 }
689 _ => panic!("Expected ConfigurationError"),
690 }
691 }
692
693 #[test]
694 fn test_new_invalid_issuer_url() {
695 let config = OidcProviderConfig::new(
696 "invalid-url",
697 Some("https://accounts.example.com/.well-known/jwks.json"),
698 &["client1".to_string()],
699 None,
700 );
701 assert!(config.is_err());
702 match config.unwrap_err() {
703 JwtError::ConfigurationError { parameter, .. } => {
704 assert_eq!(parameter, Some("issuer".to_string()));
705 }
706 _ => panic!("Expected ConfigurationError"),
707 }
708 }
709
710 #[test]
711 fn test_new_invalid_jwks_url() {
712 let config = OidcProviderConfig::new(
713 "https://accounts.example.com",
714 Some("invalid-url"),
715 &["client1".to_string()],
716 None,
717 );
718 assert!(config.is_err());
719 match config.unwrap_err() {
720 JwtError::ConfigurationError { parameter, .. } => {
721 assert_eq!(parameter, Some("jwks_url".to_string()));
722 }
723 _ => panic!("Expected ConfigurationError"),
724 }
725 }
726
727 #[test]
728 fn test_with_clock_skew() {
729 let config = OidcProviderConfig::new(
730 "https://accounts.example.com",
731 None,
732 &["client1".to_string()],
733 None,
734 )
735 .unwrap()
736 .with_clock_skew(Duration::from_secs(120));
737 assert_eq!(config.clock_skew, Duration::from_secs(120));
738 }
739
740 #[test]
741 fn test_with_cache_duration() {
742 let config = OidcProviderConfig::new(
743 "https://accounts.example.com",
744 None,
745 &["client1".to_string()],
746 None,
747 )
748 .unwrap()
749 .with_cache_duration(Duration::from_secs(3600 * 12));
750 assert_eq!(config.jwk_cache_duration, Duration::from_secs(3600 * 12));
751 }
752
753 #[test]
754 fn test_with_discovery_cache_duration() {
755 let config = OidcProviderConfig::new(
756 "https://accounts.example.com",
757 None,
758 &["client1".to_string()],
759 None,
760 )
761 .unwrap()
762 .with_discovery_cache_duration(Duration::from_secs(3600 * 6));
763 assert_eq!(
764 config.discovery_cache_duration,
765 Duration::from_secs(3600 * 6)
766 );
767 }
768
769 #[test]
770 fn test_with_required_claim() {
771 let config = OidcProviderConfig::new(
772 "https://accounts.example.com",
773 None,
774 &["client1".to_string()],
775 None,
776 )
777 .unwrap()
778 .with_required_claim("nonce");
779 assert!(config.required_claims.contains("nonce"));
780 }
781
782 #[test]
783 fn test_with_discovery_flag() {
784 let config = OidcProviderConfig::new(
785 "https://accounts.example.com",
786 Some("https://accounts.example.com/.well-known/jwks.json"),
787 &["client1".to_string()],
788 None,
789 )
790 .unwrap()
791 .set_discovery_enabled(true);
792 assert!(config.use_discovery);
793
794 let config = config.set_discovery_enabled(false);
795 assert!(!config.use_discovery);
796 }
797
798 #[test]
799 fn test_get_well_known_url() {
800 let config = OidcProviderConfig::new(
801 "https://accounts.example.com",
802 None,
803 &["client1".to_string()],
804 None,
805 )
806 .unwrap();
807 assert_eq!(
808 config.get_well_known_url(),
809 "https://accounts.example.com/.well-known/openid-configuration"
810 );
811
812 // Test with trailing slash
813 let config = OidcProviderConfig::new(
814 "https://accounts.example.com/",
815 None,
816 &["client1".to_string()],
817 None,
818 )
819 .unwrap();
820 assert_eq!(
821 config.get_well_known_url(),
822 "https://accounts.example.com/.well-known/openid-configuration"
823 );
824 }
825
826 #[test]
827 fn test_create_discovery() {
828 let config = OidcProviderConfig::new(
829 "https://accounts.example.com",
830 None,
831 &["client1".to_string()],
832 None,
833 )
834 .unwrap()
835 .with_discovery_cache_duration(Duration::from_secs(7200));
836
837 let discovery = config.create_discovery();
838 assert_eq!(discovery.get_cache_duration(), Duration::from_secs(7200));
839 }
840}