jwt-verify 0.1.3

JWT verification library for AWS Cognito tokens and any OIDC-compatible IDP
Documentation
#[cfg(test)]
mod tests {
    use serde_json::Value;
    use std::collections::{HashMap, HashSet};
    use std::sync::Arc;
    use std::time::{Duration, SystemTime, UNIX_EPOCH};

    use crate::claims::validator::ClaimsValidator;
    use crate::claims::{ClaimValidator, CognitoJwtClaims};
    use crate::cognito::config::{TokenUse, VerifierConfig};
    use crate::common::error::JwtError;

    // Helper function to create a test claims object
    fn create_test_claims() -> CognitoJwtClaims {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        CognitoJwtClaims {
            sub: "user123".to_string(),
            iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example".to_string(),
            client_id: "client1".to_string(),
            origin_jti: Some("origin123".to_string()),
            event_id: Some("event123".to_string()),
            token_use: "id".to_string(),
            scope: Some("openid profile".to_string()),
            auth_time: now - 3600,
            exp: now + 3600,
            iat: now - 3600,
            jti: "jti123".to_string(),
            username: Some("testuser".to_string()),
            custom_claims: {
                let mut map = HashMap::new();
                map.insert("aud".to_string(), Value::String("client1".to_string()));
                map
            },
        }
    }

    // Helper function to create a test config
    fn create_test_config() -> Arc<VerifierConfig> {
        Arc::new(VerifierConfig {
            region: "us-east-1".to_string(),
            user_pool_id: "us-east-1_example".to_string(),
            client_ids: vec!["client1".to_string(), "client2".to_string()],
            allowed_token_uses: vec![TokenUse::Id, TokenUse::Access],
            clock_skew: Duration::from_secs(60),
            jwk_cache_duration: Duration::from_secs(3600),
            required_claims: {
                let mut set = HashSet::new();
                set.insert("sub".to_string());
                set.insert("iss".to_string());
                set.insert("client_id".to_string());
                set
            },
            custom_validators: Vec::new(),
            error_verbosity: crate::common::error::ErrorVerbosity::Standard,
        })
    }

    #[test]
    fn test_validate_claims_valid() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);
        let claims = create_test_claims();

        let result = validator.validate_claims(&claims);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_claims_missing_required_claim() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        // Test with empty subject
        let mut claims = create_test_claims();
        claims.sub = "".to_string();

        let result = validator.validate_claims(&claims);
        assert!(result.is_err());
        if let Err(JwtError::InvalidClaim {
            claim,
            reason: _,
            value: _,
        }) = result
        {
            assert_eq!(claim, "sub");
        } else {
            panic!("Expected InvalidClaim error for 'sub'");
        }

        // Test with empty issuer
        let mut claims = create_test_claims();
        claims.iss = "".to_string();

        let result = validator.validate_claims(&claims);
        assert!(result.is_err());
        if let Err(JwtError::InvalidClaim {
            claim,
            reason: _,
            value: _,
        }) = result
        {
            assert_eq!(claim, "iss");
        } else {
            panic!("Expected InvalidClaim error for 'iss'");
        }

        // Test with empty client_id
        let mut claims = create_test_claims();
        claims.client_id = "".to_string();

        let result = validator.validate_claims(&claims);
        assert!(result.is_err());
        if let Err(JwtError::InvalidClaim {
            claim,
            reason: _,
            value: _,
        }) = result
        {
            assert_eq!(claim, "client_id");
        } else {
            panic!("Expected InvalidClaim error for 'client_id'");
        }
    }

    #[test]
    fn test_validate_token_use() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        // Test with valid ID token use
        let result = validator.validate_token_use("id");
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), TokenUse::Id);

        // Test with valid access token use
        let result = validator.validate_token_use("access");
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), TokenUse::Access);

        // Test with empty token use
        let result = validator.validate_token_use("");
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), JwtError::InvalidClaim { .. }));

        // Test with invalid token use
        let result = validator.validate_token_use("refresh");
        assert!(result.is_err());
        if let Err(JwtError::InvalidTokenUse {
            expected: _,
            actual,
        }) = result
        {
            assert_eq!(actual, "refresh");
        } else {
            panic!("Expected InvalidTokenUse error");
        }
    }

    #[test]
    fn test_validate_expiration() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        // Test with valid expiration (future)
        let result = validator.validate_expiration(now + 3600);
        assert!(result.is_ok());

        // Test with expiration within clock skew (should be valid)
        let result = validator.validate_expiration(now - 30);
        assert!(result.is_ok());

        // Test with expired token beyond clock skew
        let result = validator.validate_expiration(now - 120);
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), JwtError::ExpiredToken { .. }));
    }

    #[test]
    fn test_validate_issued_at() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        // Test with valid issued at time (past)
        let result = validator.validate_issued_at(now - 3600);
        assert!(result.is_ok());

        // Test with issued at time in the future within clock skew (should be valid)
        let result = validator.validate_issued_at(now + 30);
        assert!(result.is_ok());

        // Test with issued at time too far in the future
        let result = validator.validate_issued_at(now + 120);
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), JwtError::InvalidClaim { .. }));
    }

    #[test]
    fn test_validate_not_before() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        // Test with valid not before time (past)
        let result = validator.validate_not_before(Some(now - 3600));
        assert!(result.is_ok());

        // Test with not before time in the future within clock skew (should be valid)
        let result = validator.validate_not_before(Some(now + 30));
        assert!(result.is_ok());

        // Test with not before time too far in the future
        let result = validator.validate_not_before(Some(now + 120));
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            JwtError::TokenNotYetValid { .. }
        ));

        // Test with None (should be valid)
        let result = validator.validate_not_before(None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_issuer() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        // Test with valid issuer
        let result = validator
            .validate_issuer("https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example");
        assert!(result.is_ok());

        // Test with invalid issuer
        let result = validator
            .validate_issuer("https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example");
        assert!(result.is_err());
        if let Err(JwtError::InvalidIssuer { expected, actual }) = result {
            assert_eq!(
                expected,
                "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example"
            );
            assert_eq!(
                actual,
                "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example"
            );
        } else {
            panic!("Expected InvalidIssuer error");
        }
    }

    #[test]
    fn test_validate_client_id() {
        let config = create_test_config();
        let validator = ClaimsValidator::new(config);

        // Test with valid client ID
        let result = validator.validate_client_id("client1");
        assert!(result.is_ok());

        // Test with another valid client ID
        let result = validator.validate_client_id("client2");
        assert!(result.is_ok());

        // Test with invalid client ID
        let result = validator.validate_client_id("client3");
        assert!(result.is_err());
        if let Err(JwtError::InvalidClientId { expected, actual }) = result {
            assert_eq!(expected, vec!["client1".to_string(), "client2".to_string()]);
            assert_eq!(actual, "client3");
        } else {
            panic!("Expected InvalidClientId error");
        }

        // Test with empty client IDs list (should skip validation)
        let config = Arc::new(VerifierConfig {
            client_ids: vec![],
            ..(*create_test_config()).clone()
        });
        let validator = ClaimsValidator::new(config);

        let result = validator.validate_client_id("any_client");
        assert!(result.is_ok());
    }

    #[test]
    fn test_custom_validators() {
        // Create a custom validator
        struct TestValidator;
        impl ClaimValidator for TestValidator {
            fn validate(&self, claims: &CognitoJwtClaims) -> Result<(), String> {
                // Check if username is "testuser"
                if claims.username.as_deref() != Some("testuser") {
                    return Err("Username must be 'testuser'".to_string());
                }
                Ok(())
            }
        }

        // Create config with custom validator
        let mut config = (*create_test_config()).clone();
        config.custom_validators.push(Box::new(TestValidator));
        let config = Arc::new(config);

        let validator = ClaimsValidator::new(config);

        // Test with valid claims
        let claims = create_test_claims();
        let result = validator.validate_claims(&claims);
        assert!(result.is_ok());

        // Test with invalid claims for custom validator
        let mut claims = create_test_claims();
        claims.username = Some("otheruser".to_string());

        let result = validator.validate_claims(&claims);
        assert!(result.is_err());
        if let Err(JwtError::InvalidClaim {
            claim,
            reason,
            value: _,
        }) = result
        {
            assert_eq!(claim, "custom");
            assert_eq!(reason, "Username must be 'testuser'");
        } else {
            panic!("Expected InvalidClaim error from custom validator");
        }
    }
}