jwt_verify/cognito/
config.rs

1use std::collections::HashSet;
2use std::time::Duration;
3
4use crate::claims::ClaimValidator;
5use crate::common::error::JwtError;
6
7/// Token use types for Cognito JWT tokens.
8///
9/// This enum represents the different types of tokens that can be issued by AWS Cognito:
10/// - ID tokens (`id`): Used for authentication and contain user identity information
11/// - Access tokens (`access`): Used for authorization and contain scopes/permissions
12///
13/// # Examples
14///
15/// ```
16/// use jwt_verify::cognito::TokenUse;
17///
18/// // Create from string
19/// let token_use = TokenUse::from_str("id").unwrap();
20/// assert_eq!(token_use, TokenUse::Id);
21///
22/// // Convert to string
23/// assert_eq!(TokenUse::Access.as_str(), "access");
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TokenUse {
27    /// ID token - used for authentication and contains user identity information
28    Id,
29    /// Access token - used for authorization and contains scopes/permissions
30    Access,
31}
32
33impl TokenUse {
34    /// Get the string representation of the token use
35    pub fn as_str(&self) -> &'static str {
36        match self {
37            TokenUse::Id => "id",
38            TokenUse::Access => "access",
39        }
40    }
41
42    /// Create a TokenUse from a string
43    pub fn from_str(s: &str) -> Option<Self> {
44        match s {
45            "id" => Some(TokenUse::Id),
46            "access" => Some(TokenUse::Access),
47            _ => None,
48        }
49    }
50}
51
52/// Configuration for the Cognito JWT verification process.
53///
54/// This struct holds all configuration options for verifying Cognito JWT tokens,
55/// including AWS region, user pool ID, allowed client IDs, token types, clock skew,
56/// cache duration, required claims, custom validators, and error verbosity.
57///
58/// The configuration is immutable after creation, but can be modified using the builder
59/// pattern methods like `with_clock_skew`, `with_cache_duration`, etc.
60///
61/// # Examples
62///
63/// ```
64/// use jwt_verify::{VerifierConfig, StringValueValidator};
65/// use std::time::Duration;
66///
67/// // Create a basic configuration
68/// let config = VerifierConfig::new(
69///     "us-east-1",
70///     "us-east-1_example",
71///     &["client1".to_string()],
72///     None,
73/// ).unwrap();
74///
75/// // Add a custom validator
76/// let config = config.with_custom_validator(
77///     Box::new(StringValueValidator::new("app_id", "my-app"))
78/// );
79/// ```
80pub struct VerifierConfig {
81    /// AWS region where the Cognito user pool is located
82    pub region: String,
83    /// Cognito user pool ID in the format "region_poolid"
84    pub user_pool_id: String,
85    /// List of allowed client IDs for this user pool
86    pub client_ids: Vec<String>,
87    /// List of allowed token types (ID tokens, Access tokens)
88    pub allowed_token_uses: Vec<TokenUse>,
89    /// Clock skew tolerance for token expiration and issuance time validation
90    pub clock_skew: Duration,
91    /// Duration for which JWKs are cached before refreshing
92    pub jwk_cache_duration: Duration,
93    /// Set of claims that must be present and valid in the token
94    pub required_claims: HashSet<String>,
95    /// List of custom validators for additional claim validation
96    #[allow(clippy::type_complexity)]
97    pub custom_validators: Vec<Box<dyn ClaimValidator + Send + Sync>>,
98    /// Level of detail in error messages
99    pub error_verbosity: crate::common::error::ErrorVerbosity,
100}
101
102// Manual implementation of Debug for VerifierConfig since custom_validators doesn't implement Debug
103impl std::fmt::Debug for VerifierConfig {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("VerifierConfig")
106            .field("region", &self.region)
107            .field("user_pool_id", &self.user_pool_id)
108            .field("client_ids", &self.client_ids)
109            .field("clock_skew", &self.clock_skew)
110            .field("jwk_cache_duration", &self.jwk_cache_duration)
111            .field("required_claims", &self.required_claims)
112            .field(
113                "custom_validators",
114                &format!("[{} validators]", self.custom_validators.len()),
115            )
116            .field("error_verbosity", &self.error_verbosity)
117            .finish()
118    }
119}
120
121// Manual implementation of Clone for VerifierConfig since custom_validators doesn't implement Clone
122impl Clone for VerifierConfig {
123    fn clone(&self) -> Self {
124        Self {
125            region: self.region.clone(),
126            user_pool_id: self.user_pool_id.clone(),
127            client_ids: self.client_ids.clone(),
128            allowed_token_uses: self.allowed_token_uses.clone(),
129            clock_skew: self.clock_skew,
130            jwk_cache_duration: self.jwk_cache_duration,
131            required_claims: self.required_claims.clone(),
132            custom_validators: Vec::new(), // Custom validators can't be cloned, so we create an empty vector
133            error_verbosity: self.error_verbosity,
134        }
135    }
136}
137
138impl VerifierConfig {
139    /// Create a new verifier configuration with validation for required parameters.
140    ///
141    /// This method creates a new `VerifierConfig` with the specified region, user pool ID,
142    /// and client IDs. It validates that the region and user pool ID are not empty and that
143    /// the user pool ID has the correct format.
144    ///
145    /// # Parameters
146    ///
147    /// * `region` - AWS region where the Cognito user pool is located (e.g., "us-east-1")
148    /// * `user_pool_id` - Cognito user pool ID in the format "region_poolid"
149    /// * `client_ids` - List of allowed client IDs for this user pool
150    /// * `token_uses` - Optional list of allowed token types (defaults to both ID and Access tokens)
151    ///
152    /// # Returns
153    ///
154    /// Returns a `Result` containing the new `VerifierConfig` if successful, or a `JwtError`
155    /// if validation fails.
156    ///
157    /// # Errors
158    ///
159    /// Returns a `JwtError::ConfigurationError` if:
160    /// - The region is empty
161    /// - The user pool ID is empty
162    /// - The user pool ID does not have the correct format (should contain an underscore)
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use jwt_verify::VerifierConfig;
168    ///
169    /// // Create a basic configuration
170    /// let config = VerifierConfig::new(
171    ///     "us-east-1",
172    ///     "us-east-1_example",
173    ///     &["client1".to_string()],
174    ///     None,
175    /// ).unwrap();
176    /// ```
177    pub fn new(region: &str, user_pool_id: &str, client_ids: &[String], token_uses: Option<Vec<TokenUse>>,) -> Result<Self, JwtError> {
178        // Validate region
179        if region.is_empty() {
180            return Err(JwtError::ConfigurationError {
181                parameter: Some("region".to_string()),
182                error: "Region cannot be empty".to_string(),
183            });
184        }
185
186        // Validate user pool ID
187        if user_pool_id.is_empty() {
188            return Err(JwtError::ConfigurationError {
189                parameter: Some("user_pool_id".to_string()),
190                error: "User pool ID cannot be empty".to_string(),
191            });
192        }
193
194        // Validate user pool ID format
195        if !user_pool_id.contains('_') {
196            return Err(JwtError::ConfigurationError {
197                parameter: Some("user_pool_id".to_string()),
198                error: "Invalid user pool ID format. Expected format: region_poolid".to_string(),
199            });
200        }
201
202
203        let token_uses = match token_uses {
204            None => vec![TokenUse::Id, TokenUse::Access],
205            Some(tu) => tu,
206        };
207
208        Ok(Self {
209            region: region.to_string(),
210            user_pool_id: user_pool_id.to_string(),
211            client_ids: client_ids.to_vec(),
212            allowed_token_uses: token_uses, // Default: allow both ID and Access tokens
213            clock_skew: Duration::from_secs(60),                      // Default: 1 minute
214            jwk_cache_duration: Duration::from_secs(3600 * 24),       // Default: 24 hours
215            required_claims: HashSet::from([
216                "sub".to_string(),
217                "iss".to_string(),
218                "client_id".to_string(),
219                "exp".to_string(),
220                "iat".to_string(),
221                // Removed "token_use" as it's validated separately
222            ]),
223            custom_validators: Vec::new(),
224            error_verbosity: crate::common::error::ErrorVerbosity::Standard,
225        })
226    }
227
228    /// Set clock skew tolerance for token validation.
229    ///
230    /// Clock skew is used to account for time differences between the token issuer
231    /// and the token verifier. This is important for validating token expiration
232    /// and issuance times.
233    ///
234    /// # Parameters
235    ///
236    /// * `skew` - The clock skew duration to allow (default: 60 seconds)
237    ///
238    /// # Returns
239    ///
240    /// Returns a new `VerifierConfig` with the updated clock skew.
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use jwt_verify::VerifierConfig;
246    /// use std::time::Duration;
247    ///
248    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
249    ///     .unwrap()
250    ///     .with_clock_skew(Duration::from_secs(120)); // 2 minutes
251    /// ```
252    pub fn with_clock_skew(mut self, skew: Duration) -> Self {
253        self.clock_skew = skew;
254        self
255    }
256
257    /// Set JWK cache duration for key management.
258    ///
259    /// This determines how long JWKs (JSON Web Keys) are cached before being refreshed
260    /// from the Cognito endpoint. Longer durations reduce network requests but may
261    /// delay key rotation recognition.
262    ///
263    /// # Parameters
264    ///
265    /// * `duration` - The cache duration (default: 24 hours)
266    ///
267    /// # Returns
268    ///
269    /// Returns a new `VerifierConfig` with the updated cache duration.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use jwt_verify::VerifierConfig;
275    /// use std::time::Duration;
276    ///
277    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
278    ///     .unwrap()
279    ///     .with_cache_duration(Duration::from_secs(3600 * 12)); // 12 hours
280    /// ```
281    pub fn with_cache_duration(mut self, duration: Duration) -> Self {
282        self.jwk_cache_duration = duration;
283        self
284    }
285
286    /// Add a required claim to the validation process.
287    ///
288    /// Required claims must be present in the token and will be validated.
289    /// By default, the following claims are required: "sub", "iss", "client_id", "exp", "iat".
290    ///
291    /// # Parameters
292    ///
293    /// * `claim` - The name of the claim to require
294    ///
295    /// # Returns
296    ///
297    /// Returns a new `VerifierConfig` with the added required claim.
298    ///
299    /// # Examples
300    ///
301    /// ```
302    /// use jwt_verify::VerifierConfig;
303    ///
304    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
305    ///     .unwrap()
306    ///     .with_required_claim("custom_claim");
307    /// ```
308    pub fn with_required_claim(mut self, claim: &str) -> Self {
309        self.required_claims.insert(claim.to_string());
310        self
311    }
312
313    /// Add a custom validator for additional claim validation.
314    ///
315    /// Custom validators allow for application-specific validation logic beyond
316    /// the standard JWT claim validation. They can validate specific claim values,
317    /// formats, or relationships between claims.
318    ///
319    /// # Parameters
320    ///
321    /// * `validator` - A boxed implementation of the `ClaimValidator` trait
322    ///
323    /// # Returns
324    ///
325    /// Returns a new `VerifierConfig` with the added custom validator.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use jwt_verify::{VerifierConfig, StringValueValidator};
331    ///
332    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
333    ///     .unwrap()
334    ///     .with_custom_validator(Box::new(StringValueValidator::new(
335    ///         "app_id", "my-application"
336    ///     )));
337    /// ```
338    pub fn with_custom_validator(
339        mut self,
340        validator: Box<dyn ClaimValidator + Send + Sync>,
341    ) -> Self {
342        self.custom_validators.push(validator);
343        self
344    }
345
346    /// Set the error verbosity level for error reporting.
347    ///
348    /// This controls how much detail is included in error messages and logs.
349    /// Higher verbosity levels include more information but may expose sensitive data.
350    ///
351    /// # Parameters
352    ///
353    /// * `verbosity` - The error verbosity level (default: Standard)
354    ///
355    /// # Returns
356    ///
357    /// Returns a new `VerifierConfig` with the updated error verbosity.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use jwt_verify::{VerifierConfig, config::ErrorVerbosity};
363    ///
364    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
365    ///     .unwrap()
366    ///     .with_error_verbosity(ErrorVerbosity::Detailed);
367    /// ```
368    pub fn with_error_verbosity(mut self, verbosity: crate::common::error::ErrorVerbosity) -> Self {
369        self.error_verbosity = verbosity;
370        self
371    }
372
373    /// Get issuer URL
374    pub fn get_issuer_url(&self) -> String {
375        format!(
376            "https://cognito-idp.{}.amazonaws.com/{}",
377            self.region, self.user_pool_id
378        )
379    }
380
381    /// Get JWK URL
382    pub fn get_jwk_url(&self) -> String {
383        format!("{}{}", self.get_issuer_url(), "/.well-known/jwks.json")
384    }
385}