jwt-verify 0.1.0

JWT verification library for AWS Cognito tokens and any OIDC-compatible IDP
Documentation
use std::collections::HashSet;
use std::time::Duration;

use crate::claims::ClaimValidator;
use crate::common::error::JwtError;

/// Token use types for Cognito JWT tokens.
///
/// This enum represents the different types of tokens that can be issued by AWS Cognito:
/// - ID tokens (`id`): Used for authentication and contain user identity information
/// - Access tokens (`access`): Used for authorization and contain scopes/permissions
///
/// # Examples
///
/// ```
/// use jwt_verify::cognito::TokenUse;
///
/// // Create from string
/// let token_use = TokenUse::from_str("id").unwrap();
/// assert_eq!(token_use, TokenUse::Id);
///
/// // Convert to string
/// assert_eq!(TokenUse::Access.as_str(), "access");
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenUse {
    /// ID token - used for authentication and contains user identity information
    Id,
    /// Access token - used for authorization and contains scopes/permissions
    Access,
}

impl TokenUse {
    /// Get the string representation of the token use
    pub fn as_str(&self) -> &'static str {
        match self {
            TokenUse::Id => "id",
            TokenUse::Access => "access",
        }
    }

    /// Create a TokenUse from a string
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "id" => Some(TokenUse::Id),
            "access" => Some(TokenUse::Access),
            _ => None,
        }
    }
}

/// Configuration for the Cognito JWT verification process.
///
/// This struct holds all configuration options for verifying Cognito JWT tokens,
/// including AWS region, user pool ID, allowed client IDs, token types, clock skew,
/// cache duration, required claims, custom validators, and error verbosity.
///
/// The configuration is immutable after creation, but can be modified using the builder
/// pattern methods like `with_clock_skew`, `with_cache_duration`, etc.
///
/// # Examples
///
/// ```
/// use jwt_verify::{VerifierConfig, StringValueValidator};
/// use std::time::Duration;
///
/// // Create a basic configuration
/// let config = VerifierConfig::new(
///     "us-east-1",
///     "us-east-1_example",
///     &["client1".to_string()],
///     None,
/// ).unwrap();
///
/// // Add a custom validator
/// let config = config.with_custom_validator(
///     Box::new(StringValueValidator::new("app_id", "my-app"))
/// );
/// ```
pub struct VerifierConfig {
    /// AWS region where the Cognito user pool is located
    pub region: String,
    /// Cognito user pool ID in the format "region_poolid"
    pub user_pool_id: String,
    /// List of allowed client IDs for this user pool
    pub client_ids: Vec<String>,
    /// List of allowed token types (ID tokens, Access tokens)
    pub allowed_token_uses: Vec<TokenUse>,
    /// Clock skew tolerance for token expiration and issuance time validation
    pub clock_skew: Duration,
    /// Duration for which JWKs are cached before refreshing
    pub jwk_cache_duration: Duration,
    /// Set of claims that must be present and valid in the token
    pub required_claims: HashSet<String>,
    /// List of custom validators for additional claim validation
    #[allow(clippy::type_complexity)]
    pub custom_validators: Vec<Box<dyn ClaimValidator + Send + Sync>>,
    /// Level of detail in error messages
    pub error_verbosity: crate::common::error::ErrorVerbosity,
}

// Manual implementation of Debug for VerifierConfig since custom_validators doesn't implement Debug
impl std::fmt::Debug for VerifierConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("VerifierConfig")
            .field("region", &self.region)
            .field("user_pool_id", &self.user_pool_id)
            .field("client_ids", &self.client_ids)
            .field("clock_skew", &self.clock_skew)
            .field("jwk_cache_duration", &self.jwk_cache_duration)
            .field("required_claims", &self.required_claims)
            .field(
                "custom_validators",
                &format!("[{} validators]", self.custom_validators.len()),
            )
            .field("error_verbosity", &self.error_verbosity)
            .finish()
    }
}

// Manual implementation of Clone for VerifierConfig since custom_validators doesn't implement Clone
impl Clone for VerifierConfig {
    fn clone(&self) -> Self {
        Self {
            region: self.region.clone(),
            user_pool_id: self.user_pool_id.clone(),
            client_ids: self.client_ids.clone(),
            allowed_token_uses: self.allowed_token_uses.clone(),
            clock_skew: self.clock_skew,
            jwk_cache_duration: self.jwk_cache_duration,
            required_claims: self.required_claims.clone(),
            custom_validators: Vec::new(), // Custom validators can't be cloned, so we create an empty vector
            error_verbosity: self.error_verbosity,
        }
    }
}

impl VerifierConfig {
    /// Create a new verifier configuration with validation for required parameters.
    ///
    /// This method creates a new `VerifierConfig` with the specified region, user pool ID,
    /// and client IDs. It validates that the region and user pool ID are not empty and that
    /// the user pool ID has the correct format.
    ///
    /// # Parameters
    ///
    /// * `region` - AWS region where the Cognito user pool is located (e.g., "us-east-1")
    /// * `user_pool_id` - Cognito user pool ID in the format "region_poolid"
    /// * `client_ids` - List of allowed client IDs for this user pool
    /// * `token_uses` - Optional list of allowed token types (defaults to both ID and Access tokens)
    ///
    /// # Returns
    ///
    /// Returns a `Result` containing the new `VerifierConfig` if successful, or a `JwtError`
    /// if validation fails.
    ///
    /// # Errors
    ///
    /// Returns a `JwtError::ConfigurationError` if:
    /// - The region is empty
    /// - The user pool ID is empty
    /// - The user pool ID does not have the correct format (should contain an underscore)
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::VerifierConfig;
    ///
    /// // Create a basic configuration
    /// let config = VerifierConfig::new(
    ///     "us-east-1",
    ///     "us-east-1_example",
    ///     &["client1".to_string()],
    ///     None,
    /// ).unwrap();
    /// ```
    pub fn new(region: &str, user_pool_id: &str, client_ids: &[String], token_uses: Option<Vec<TokenUse>>,) -> Result<Self, JwtError> {
        // Validate region
        if region.is_empty() {
            return Err(JwtError::ConfigurationError {
                parameter: Some("region".to_string()),
                error: "Region cannot be empty".to_string(),
            });
        }

        // Validate user pool ID
        if user_pool_id.is_empty() {
            return Err(JwtError::ConfigurationError {
                parameter: Some("user_pool_id".to_string()),
                error: "User pool ID cannot be empty".to_string(),
            });
        }

        // Validate user pool ID format
        if !user_pool_id.contains('_') {
            return Err(JwtError::ConfigurationError {
                parameter: Some("user_pool_id".to_string()),
                error: "Invalid user pool ID format. Expected format: region_poolid".to_string(),
            });
        }


        let token_uses = match token_uses {
            None => vec![TokenUse::Id, TokenUse::Access],
            Some(tu) => tu,
        };

        Ok(Self {
            region: region.to_string(),
            user_pool_id: user_pool_id.to_string(),
            client_ids: client_ids.to_vec(),
            allowed_token_uses: token_uses, // Default: allow both ID and Access tokens
            clock_skew: Duration::from_secs(60),                      // Default: 1 minute
            jwk_cache_duration: Duration::from_secs(3600 * 24),       // Default: 24 hours
            required_claims: HashSet::from([
                "sub".to_string(),
                "iss".to_string(),
                "client_id".to_string(),
                "exp".to_string(),
                "iat".to_string(),
                // Removed "token_use" as it's validated separately
            ]),
            custom_validators: Vec::new(),
            error_verbosity: crate::common::error::ErrorVerbosity::Standard,
        })
    }

    /// Set clock skew tolerance for token validation.
    ///
    /// Clock skew is used to account for time differences between the token issuer
    /// and the token verifier. This is important for validating token expiration
    /// and issuance times.
    ///
    /// # Parameters
    ///
    /// * `skew` - The clock skew duration to allow (default: 60 seconds)
    ///
    /// # Returns
    ///
    /// Returns a new `VerifierConfig` with the updated clock skew.
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::VerifierConfig;
    /// use std::time::Duration;
    ///
    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
    ///     .unwrap()
    ///     .with_clock_skew(Duration::from_secs(120)); // 2 minutes
    /// ```
    pub fn with_clock_skew(mut self, skew: Duration) -> Self {
        self.clock_skew = skew;
        self
    }

    /// Set JWK cache duration for key management.
    ///
    /// This determines how long JWKs (JSON Web Keys) are cached before being refreshed
    /// from the Cognito endpoint. Longer durations reduce network requests but may
    /// delay key rotation recognition.
    ///
    /// # Parameters
    ///
    /// * `duration` - The cache duration (default: 24 hours)
    ///
    /// # Returns
    ///
    /// Returns a new `VerifierConfig` with the updated cache duration.
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::VerifierConfig;
    /// use std::time::Duration;
    ///
    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
    ///     .unwrap()
    ///     .with_cache_duration(Duration::from_secs(3600 * 12)); // 12 hours
    /// ```
    pub fn with_cache_duration(mut self, duration: Duration) -> Self {
        self.jwk_cache_duration = duration;
        self
    }

    /// Add a required claim to the validation process.
    ///
    /// Required claims must be present in the token and will be validated.
    /// By default, the following claims are required: "sub", "iss", "client_id", "exp", "iat".
    ///
    /// # Parameters
    ///
    /// * `claim` - The name of the claim to require
    ///
    /// # Returns
    ///
    /// Returns a new `VerifierConfig` with the added required claim.
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::VerifierConfig;
    ///
    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
    ///     .unwrap()
    ///     .with_required_claim("custom_claim");
    /// ```
    pub fn with_required_claim(mut self, claim: &str) -> Self {
        self.required_claims.insert(claim.to_string());
        self
    }

    /// Add a custom validator for additional claim validation.
    ///
    /// Custom validators allow for application-specific validation logic beyond
    /// the standard JWT claim validation. They can validate specific claim values,
    /// formats, or relationships between claims.
    ///
    /// # Parameters
    ///
    /// * `validator` - A boxed implementation of the `ClaimValidator` trait
    ///
    /// # Returns
    ///
    /// Returns a new `VerifierConfig` with the added custom validator.
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::{VerifierConfig, StringValueValidator};
    ///
    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
    ///     .unwrap()
    ///     .with_custom_validator(Box::new(StringValueValidator::new(
    ///         "app_id", "my-application"
    ///     )));
    /// ```
    pub fn with_custom_validator(
        mut self,
        validator: Box<dyn ClaimValidator + Send + Sync>,
    ) -> Self {
        self.custom_validators.push(validator);
        self
    }

    /// Set the error verbosity level for error reporting.
    ///
    /// This controls how much detail is included in error messages and logs.
    /// Higher verbosity levels include more information but may expose sensitive data.
    ///
    /// # Parameters
    ///
    /// * `verbosity` - The error verbosity level (default: Standard)
    ///
    /// # Returns
    ///
    /// Returns a new `VerifierConfig` with the updated error verbosity.
    ///
    /// # Examples
    ///
    /// ```
    /// use jwt_verify::{VerifierConfig, config::ErrorVerbosity};
    ///
    /// let config = VerifierConfig::new("us-east-1", "us-east-1_example", &[], None)
    ///     .unwrap()
    ///     .with_error_verbosity(ErrorVerbosity::Detailed);
    /// ```
    pub fn with_error_verbosity(mut self, verbosity: crate::common::error::ErrorVerbosity) -> Self {
        self.error_verbosity = verbosity;
        self
    }

    /// Get issuer URL
    pub fn get_issuer_url(&self) -> String {
        format!(
            "https://cognito-idp.{}.amazonaws.com/{}",
            self.region, self.user_pool_id
        )
    }

    /// Get JWK URL
    pub fn get_jwk_url(&self) -> String {
        format!("{}{}", self.get_issuer_url(), "/.well-known/jwks.json")
    }
}