| Crates.io | jwt-verify |
| lib.rs | jwt-verify |
| version | 0.1.3 |
| created_at | 2025-11-25 07:04:25.186707+00 |
| updated_at | 2025-11-27 04:42:53.601215+00 |
| description | JWT verification library for AWS Cognito tokens and any OIDC-compatible IDP |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1949269 |
| size | 486,035 |
Rust library for verifying JWTs signed by Amazon Cognito, and any OIDC-compatible IDP.
Inspired by awslabs/aws-jwt-verify.
Add this to your Cargo.toml:
[dependencies]
jwt-verify = "0.1.2"
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?;
// Access standard fields via trait methods
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?;
// Access standard fields via trait methods
println!("Scopes: {:?}", access_claims.get_scopes());
println!("Has 'read' scope: {}", access_claims.has_scope("read"));
Ok(())
}
Need provider-specific fields? The examples above use trait methods that work across all providers. To access Cognito-specific fields like cognito_groups, cognito_username, or custom claims, see the Downcasting section.
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?;
// Access standard fields via trait methods
println!("Subject: {}", claims.get_sub());
println!("Email: {}", claims.get_email().unwrap_or("N/A"));
Ok(())
}
Need provider-specific fields? The example above uses trait methods that work across all providers. To access OIDC-specific fields like preferred_username, picture, locale, or custom claims, see the Downcasting section.
A common use case is having one user pool with multiple client IDs (e.g., web app, mobile app):
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(())
}
The verifier automatically selects the correct user pool based on the token's issuer claim:
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(())
}
Prefetch JWKs to avoid cold start latency:
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(())
}
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(())
}
The library includes comprehensive examples demonstrating various use cases:
cognito_basic.rs: AWS Cognito JWT verification including:
oidc_basic.rs: OIDC JWT verification including:
cognito_axum.rs: Full-featured Axum web server with Cognito JWT authentication:
oidc_axum.rs: Same features as cognito_axum.rs but for OIDC providersSet up configuration using a .env file:
cd examples
cp .env.example .env
# Edit .env with your actual configuration
Run the examples:
# Basic CLI examples
cargo run --example cognito_basic
cargo run --example oidc_basic
# Axum web server examples
cargo run --example cognito_axum
cargo run --example oidc_axum
Test the Axum server endpoints:
# Public endpoint (no auth)
curl http://localhost:3000/
# Protected endpoint with ID token
curl -H "Authorization: Bearer <ID_TOKEN>" \
http://localhost:3000/protected/id-token
# Protected endpoint with access token
curl -H "Authorization: Bearer <ACCESS_TOKEN>" \
http://localhost:3000/protected/access-token
# Admin endpoint (requires 'admin' scope)
curl -H "Authorization: Bearer <ACCESS_TOKEN>" \
http://localhost:3000/admin
The examples support various configurations through environment variables:
# 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.
// 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
}
The verify_id_token() and verify_access_token() methods return trait objects (Box<dyn IdTokenClaims> and Box<dyn AccessTokenClaims>). While trait methods provide access to common fields, you can downcast to concrete types to access provider-specific fields.
use jwt_verify::{CognitoJwtVerifier, CognitoIdTokenClaims, CognitoAccessTokenClaims, JwtVerifier};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let verifier = CognitoJwtVerifier::new_single_pool(
"us-east-1",
"us-east-1_example",
&["client1".to_string()],
)?;
// Verify and downcast ID token
let id_token = "your_id_token";
let claims = verifier.verify_id_token(id_token).await?;
if let Some(cognito_claims) = claims.downcast_ref::<CognitoIdTokenClaims>() {
// Access Cognito-specific fields
println!("Subject: {}", cognito_claims.base.sub);
println!("Email: {:?}", cognito_claims.email);
println!("Cognito username: {:?}", cognito_claims.cognito_username);
println!("Cognito groups: {:?}", cognito_claims.cognito_groups);
println!("Cognito roles: {:?}", cognito_claims.cognito_roles);
// Check group membership
if cognito_claims.cognito_groups.as_ref()
.map(|g| g.contains(&"admins".to_string()))
.unwrap_or(false) {
println!("User is an admin!");
}
}
// Verify and downcast access token
let access_token = "your_access_token";
let access_claims = verifier.verify_access_token(access_token).await?;
if let Some(cognito_claims) = access_claims.downcast_ref::<CognitoAccessTokenClaims>() {
// Access Cognito-specific fields
println!("Username: {:?}", cognito_claims.base.username);
println!("Token use: {}", cognito_claims.base.token_use);
println!("Version: {:?}", cognito_claims.version);
// Access custom claims
if let Some(dept) = cognito_claims.base.get_custom_claim_string("department") {
println!("Department: {}", dept);
}
}
Ok(())
}
use jwt_verify::{OidcJwtVerifier, OidcIdTokenClaims, OidcAccessTokenClaims, JwtVerifier, OidcProviderConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OidcProviderConfig::new(
"https://accounts.example.com",
Some("https://accounts.example.com/.well-known/jwks.json"),
&["client1".to_string()],
None,
)?;
let verifier = OidcJwtVerifier::new(vec![config])?;
// Verify and downcast ID token
let id_token = "your_id_token";
let claims = verifier.verify_id_token(id_token).await?;
if let Some(oidc_claims) = claims.downcast_ref::<OidcIdTokenClaims>() {
// Access OIDC-specific fields
println!("Subject: {}", oidc_claims.base.sub);
println!("Email: {:?}", oidc_claims.email);
println!("Preferred username: {:?}", oidc_claims.preferred_username);
println!("Picture: {:?}", oidc_claims.picture);
println!("Locale: {:?}", oidc_claims.locale);
println!("Given name: {:?}", oidc_claims.given_name);
println!("Family name: {:?}", oidc_claims.family_name);
}
// Verify and downcast access token
let access_token = "your_access_token";
let access_claims = verifier.verify_access_token(access_token).await?;
if let Some(oidc_claims) = access_claims.downcast_ref::<OidcAccessTokenClaims>() {
// Access OIDC-specific fields
println!("Audience: {}", oidc_claims.base.aud);
println!("Authorized party: {:?}", oidc_claims.base.azp);
println!("Client ID: {:?}", oidc_claims.client_id);
// Access custom claims
if let Some(username) = oidc_claims.base.get_custom_claim_string("username") {
println!("Username: {}", username);
}
}
Ok(())
}
Always downcast to the correct type that matches your token:
| Token Type | Verifier Method | Downcast To |
|---|---|---|
| Cognito ID Token | verify_id_token() |
CognitoIdTokenClaims |
| Cognito Access Token | verify_access_token() |
CognitoAccessTokenClaims |
| OIDC ID Token | verify_id_token() |
OidcIdTokenClaims |
| OIDC Access Token | verify_access_token() |
OidcAccessTokenClaims |
Common Mistake:
// ❌ WRONG: Trying to downcast ID token to AccessTokenClaims
let claims = verifier.verify_id_token(token).await?;
let wrong = claims.downcast_ref::<CognitoAccessTokenClaims>(); // Won't compile!
// ✅ CORRECT: Downcast ID token to IdTokenClaims
let claims = verifier.verify_id_token(token).await?;
let correct = claims.downcast_ref::<CognitoIdTokenClaims>(); // Works!
| Approach | Use When |
|---|---|
| Trait methods only | You only need standard fields (sub, email, scopes, exp, etc.) |
| Downcasting | You need provider-specific fields (groups, custom claims, etc.) |
| Concrete verifier type | You only use one provider and want to avoid trait objects entirely |
If you only use one provider and want to avoid trait objects, use the concrete verifier type directly:
use jwt_verify::{CognitoJwtVerifier, CognitoAccessTokenClaims};
let verifier = CognitoJwtVerifier::new_single_pool(
"us-east-1",
"us-east-1_example",
&["client1".to_string()],
)?;
// Use the generic verify method directly (no trait object)
let claims: CognitoAccessTokenClaims = verifier.verify(token).await?;
// Direct access to all fields, no downcasting needed
println!("Username: {:?}", claims.base.username);
The library provides detailed error information for debugging:
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
}
}
hydrate() to warm up the cache and avoid cold start latencyThis project is licensed under the MIT License.