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}