jwt-verify 0.1.0

JWT verification library for AWS Cognito tokens and any OIDC-compatible IDP
Documentation
# JWT Verify

**Rust** library for **verifying** JWTs signed by **Amazon Cognito**, and any **OIDC-compatible IDP**.

Inspired by [awslabs/aws-jwt-verify](https://github.com/awslabs/aws-jwt-verify).

## Features

- [x] Comprehensive validation of JWT tokens (ID tokens and Access tokens)
- [x] Support for both AWS Cognito and generic OIDC providers
- [x] Efficient JWK key management with automatic caching
- [x] Multiple user pools/providers with automatic issuer matching
- [x] Multiple client IDs per pool/provider
- [x] Configurable clock skew and cache duration
- [x] JWK prefetching (hydration) for cold start optimization
- [x] Detailed error handling and reporting
- [x] Thread-safe for use in async contexts
- [ ] Integration with popular Rust web frameworks (Axum)

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
jwt-verify = "0.1.0"
```

## Quick Start

### Verifying AWS Cognito Tokens

```rust
use jwt_verify::{CognitoJwtVerifier, JwtError, JwtVerifier};

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    // Create a verifier with a single user pool
    let verifier = CognitoJwtVerifier::new_single_pool(
        "us-east-1",              // AWS region
        "us-east-1_example",      // Cognito user pool ID
        &["client1".to_string()], // Allowed client IDs
    )?;

    // Verify an ID token
    let id_token = "your_jwt_id_token_here";
    let claims = verifier.verify_id_token(id_token).await?;

    println!("Subject: {}", claims.get_sub());
    println!("Email: {}", claims.get_email().unwrap_or("N/A"));

    // Verify an access token
    let access_token = "your_jwt_access_token_here";
    let access_claims = verifier.verify_access_token(access_token).await?;

    println!("Scopes: {:?}", access_claims.get_scopes());
    println!("Has 'read' scope: {}", access_claims.has_scope("read"));

    Ok(())
}
```

### Verifying OIDC Tokens

```rust
use jwt_verify::{JwtError, JwtVerifier, OidcJwtVerifier, OidcProviderConfig};

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    // Create a configuration for an OIDC provider
    let config = OidcProviderConfig::new(
        "https://accounts.example.com",                              // Issuer URL
        Some("https://accounts.example.com/.well-known/jwks.json"),  // JWKS URL
        &["client1".to_string()],                                    // Allowed client IDs
        None,                                                        // Optional additional config
    )?;

    // Create an OIDC verifier
    let verifier = OidcJwtVerifier::new(vec![config])?;

    // Verify an ID token
    let id_token = "your_jwt_id_token_here";
    let claims = verifier.verify_id_token(id_token).await?;

    println!("Subject: {}", claims.get_sub());

    Ok(())
}
```

## Advanced Usage

### Single Pool with Multiple Client IDs

A common use case is having one user pool with multiple client IDs (e.g., web app, mobile app):

```rust
use jwt_verify::{CognitoJwtVerifier, JwtError, VerifierConfig};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    // Single user pool with multiple client IDs
    let config = VerifierConfig::new(
        "us-east-1",
        "us-east-1_example",
        &["web-client-id".to_string(), "mobile-client-id".to_string()],
        None,
    )?
    .with_clock_skew(Duration::from_secs(120))           // 2 minutes clock skew
    .with_cache_duration(Duration::from_secs(3600 * 12)); // 12 hours cache

    let verifier = CognitoJwtVerifier::new(vec![config])?;

    // Tokens from either client ID will be accepted
    let token = "your_jwt_token_here";
    let claims = verifier.verify_id_token(token).await?;

    Ok(())
}
```

### Multiple User Pools

The verifier automatically selects the correct user pool based on the token's issuer claim:

```rust
use jwt_verify::{CognitoJwtVerifier, JwtError, VerifierConfig};

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    // Create configurations for multiple user pools
    let config1 = VerifierConfig::new(
        "us-east-1",
        "us-east-1_pool1",
        &["client1".to_string()],
        None,
    )?;

    let config2 = VerifierConfig::new(
        "us-west-2",
        "us-west-2_pool2",
        &["client2".to_string()],
        None,
    )?;

    // Create a verifier with multiple user pools
    let verifier = CognitoJwtVerifier::new(vec![config1, config2])?;

    // The verifier automatically matches the token to the correct pool
    let token = "your_jwt_token_here";
    let claims = verifier.verify_id_token(token).await?;

    Ok(())
}
```

### JWK Prefetching (Hydration)

Prefetch JWKs to avoid cold start latency:

```rust
use jwt_verify::{CognitoJwtVerifier, JwtError, JwtVerifier};

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    let verifier = CognitoJwtVerifier::new_single_pool(
        "us-east-1",
        "us-east-1_example",
        &["client1".to_string()],
    )?;

    // Prefetch JWKs to warm up the cache
    let hydration_results = verifier.hydrate().await;
    for (pool_id, result) in hydration_results {
        match result {
            Ok(_) => println!("✅ Prefetched JWKs for pool {}", pool_id),
            Err(e) => println!("❌ Failed to prefetch for pool {}: {}", pool_id, e),
        }
    }

    // Now token verification will be faster
    let token = "your_jwt_token_here";
    let claims = verifier.verify_id_token(token).await?;

    Ok(())
}
```

### Multiple OIDC Providers

```rust
use jwt_verify::{JwtError, OidcJwtVerifier, OidcProviderConfig};

#[tokio::main]
async fn main() -> Result<(), JwtError> {
    let provider1 = OidcProviderConfig::new(
        "https://accounts.example.com",
        Some("https://accounts.example.com/.well-known/jwks.json"),
        &["client1".to_string()],
        None,
    )?;

    let provider2 = OidcProviderConfig::new(
        "https://auth.example2.com",
        Some("https://auth.example2.com/.well-known/jwks.json"),
        &["client2".to_string()],
        None,
    )?;

    // Create a verifier with multiple providers
    let verifier = OidcJwtVerifier::new(vec![provider1, provider2])?;

    // The verifier automatically matches the token to the correct provider
    let token = "your_jwt_token_here";
    let claims = verifier.verify_id_token(token).await?;

    Ok(())
}
```

## Examples

The library includes comprehensive examples demonstrating various use cases:

- **`cognito_basic.rs`**: AWS Cognito JWT verification including:
  - Single user pool with single client ID
  - Multiple user pools with different client IDs
  - Single user pool with multiple client IDs (web/mobile apps)
  - Negative test cases (wrong token types, expired tokens, etc.)
- **`oidc_basic.rs`**: OIDC JWT verification including:
  - Single provider with single client ID
  - Multiple providers with different client IDs
  - Single provider with multiple client IDs
  - Negative test cases

### Running Examples

1. Set up configuration using a `.env` file:

   ```bash
   cd examples
   cp .env.example .env
   # Edit .env with your actual configuration
   ```

2. Run the examples:

   ```bash
   cargo run --example cognito_basic
   cargo run --example oidc_basic
   ```

### Example Configuration

The examples support various configurations through environment variables:

```bash
# Single user pool with multiple client IDs
AWS_REGION=us-east-1
COGNITO_USER_POOL_ID=us-east-1_example
COGNITO_CLIENT_ID=web-app-client-id
COGNITO_CLIENT_ID_2=mobile-app-client-id

# Your test tokens
COGNITO_ID_TOKEN=your-id-token
COGNITO_ACCESS_TOKEN=your-access-token
```

See `examples/README.md` for detailed configuration instructions and more examples.

## Common Use Cases

### ID Token vs Access Token

- **ID Tokens**: Used for authentication - contains user identity information (email, name, etc.)
- **Access Tokens**: Used for authorization - contains scopes and permissions

```rust
// Verify ID token for authentication
let id_claims = verifier.verify_id_token(id_token).await?;
println!("User: {}", id_claims.get_email().unwrap_or("N/A"));

// Verify access token for authorization
let access_claims = verifier.verify_access_token(access_token).await?;
if access_claims.has_scope("admin") {
    // Allow admin operations
}
```

### Error Handling

The library provides detailed error information for debugging:

```rust
match verifier.verify_id_token(token).await {
    Ok(claims) => {
        // Token is valid
        println!("User: {}", claims.get_sub());
    }
    Err(e) => {
        // Handle specific error cases
        eprintln!("Token verification failed: {}", e);
        // Don't expose detailed errors to clients in production
    }
}
```

## Best Practices

1. **Reuse verifier instances**: Create a single verifier instance and reuse it for all verifications (thread-safe)
2. **Set appropriate clock skew**: Use 1-2 minutes to account for time differences between systems
3. **Configure cache duration**: Match your IdP's key rotation policy (default: 12 hours)
4. **Prefetch JWKs**: Use `hydrate()` to warm up the cache and avoid cold start latency
5. **Use correct token types**: ID tokens for authentication, access tokens for authorization
6. **Validate scopes**: Always check scopes in access tokens for authorization decisions
7. **Handle errors gracefully**: Don't expose detailed error information to clients in production
8. **Multiple client IDs**: Use a single pool/provider config with multiple client IDs for different apps (web, mobile)

## License

This project is licensed under the MIT License.