Generic JWT validator with JWKS caching and kid-miss refresh. No
protocol-specific dependencies — use it from any async Rust project that
needs to verify bearer tokens against a JWKS endpoint.
- Signature verification via [
jsonwebtoken], with a selectable crypto backend — mirrorsjsonwebtoken's ownaws_lc_rs/rust_cryptoCargo features so this crate doesn't force a backend choice (and its lockfile-unifyingCargo.lockfeatures) onto every workspace member, including ones that don't even depend on it.aws_lc_rs(default) — the AWS-LC backend.rust_crypto— the pure-Rust (RustCrypto) backend instead. Select it withdefault-features = false, features = ["rust_crypto"]. Both backends build and pass the full test suite (cargo test --no-default-features --features rust_crypto).- Default allowlist:
RS256,ES256. Both exercised by the integration suite under both backends. - Opt-in via
.with_algorithms(...):RS384,RS512,ES384. Code paths exist and the JWKS parser accepts them, but integration coverage is RS256 + ES256 only. Adopters that enable the extras should extend the test matrix in their own project.
- JWKS fetched over HTTPS with an in-memory cache, a configurable
refresh interval, and automatic refetch on
kidcache miss. - Audience and issuer claim enforcement.
- Expiration (
exp) validation. - Extra-claims extraction via
serde_json::Value. - Cross-check: the token's
algheader must match the JWKS-advertisedalgfor the matchingkid. This blocks algorithm-confusion attacks where a caller submits an HS256 token using a public key as the HMAC secret.
These are off by default — existing behavior is unchanged unless you call the corresponding builder method:
.with_stale_window(Duration)— if a JWKS refresh fails, keep serving the existing cached keys for up to this long (measured from the last successful fetch) instead of propagating the error. Defaults toDuration::ZERO(never stale-serve)..with_retry(attempts, base_delay)— retry a failed JWKS fetch up toattemptstimes, with exponential backoff starting atbase_delay. The whole retry loop is bounded by an overall timeout ceiling derived fromattempts/base_delay, so a hung endpoint can't block indefinitely. Defaults to a single attempt with no backoff or ceiling.JwksFetchErrorKind—JwtValidationError::JwksFetchErrornow carries a#[non_exhaustive]kind(Timeout/Transport/HttpStatus(u16)/InvalidJson/NoSigningKeys) alongside themessage, so callers can build log/alert filters without string-matching..with_max_age(Duration)— revocation safety-net. Even on akid-hit, if the cached key is older than this, treat it like a cache miss and attempt a refresh before serving it (subject to the existingrefresh_intervalrate limit — a refresh skipped by that cooldown still serves the cached key rather than erroring). Defaults to unset: a cached key with a matchingkidis trusted indefinitely, matching pre-existing behavior.
use std::time::Duration;
use turul_jwt_validator::JwtValidator;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let validator = JwtValidator::new(
"https://auth.example.com/.well-known/jwks.json",
"my-audience",
)
.with_issuer("https://auth.example.com")
.with_refresh_interval(Duration::from_secs(60));
let claims = validator.validate("eyJhbGc...").await?;
println!("subject: {}", claims.sub);
Ok(())
}A runnable version lives at
examples/validate-token.rs and reads
the JWKS URL, audience, and token from environment variables:
TURUL_JWKS=https://auth.example.com/.well-known/jwks.json \
TURUL_AUDIENCE=my-audience \
TURUL_TOKEN='eyJhbGc...' \
cargo run --example validate-tokenThis crate enables jsonwebtoken's aws_lc_rs backend, which links
aws-lc-sys (C code built via cc-rs). cargo lambda build delegates
to cargo-zigbuild, whose ar shim currently requires Zig 0.15.x —
Zig 0.16 broke it, and every cc-rs-built crate (aws-lc-sys, ring,
…) fails to archive. This is a Zig-version issue, not a platform issue:
macOS and Linux are both affected if Zig 0.16+ is first on PATH.
Until cargo-zigbuild ships Zig 0.16 support, put Zig 0.15 ahead of any
newer Zig on your build host's PATH:
# macOS (Homebrew):
brew install zig@0.15
export PATH="/opt/homebrew/opt/zig@0.15/bin:$PATH"
# Linux: install Zig 0.15.x from a distro package, an upstream
# tarball, or asdf, and ensure it resolves first on PATH.
cargo lambda build --release -p your-lambda-crateIf Zig 0.15.x is the only Zig on your host, no action needed. Remove
the pin once cargo-zigbuild announces 0.16 compatibility.
- MSRV: Rust 1.85 (
rust-versioninCargo.toml). - Edition: 2024.
- Semver policy: this is a
0.xcrate. Minor bumps (0.1.x→0.2.0) may introduce breaking changes; patch bumps are additive or bug-fix only.
turul-a2a-auth— A2A bearer/JWT middleware built on this validator.turul-a2a— A2A (Agent-to-Agent) Protocol framework, first adopter of this crate.
Dual-licensed under MIT OR Apache 2.0 at your option.