| Crates.io | jwks-cache |
| lib.rs | jwks-cache |
| version | 0.1.5 |
| created_at | 2025-11-01 16:08:00.503475+00 |
| updated_at | 2025-12-10 09:40:16.669985+00 |
| description | High-performance async JWKS cache with ETag revalidation, early refresh, and multi-tenant support — built for modern Rust identity systems. |
| homepage | https://hack.ink/jwks-cache |
| repository | https://github.com/hack-ink/jwks-cache |
| max_upload_size | |
| id | 1912134 |
| size | 225,545 |
High-performance async JWKS cache with ETag revalidation, early refresh, and multi-tenant support — built for modern Rust identity systems.
Cache-Control, Expires, ETag, and Last-Modified headers via http-cache-semantics, so refresh cadence tracks the upstream contract instead of guessing TTLs.Add the crate to your project and enable optional integrations as needed:
# Cargo.toml
[dependencies]
# Drop `redis` if persistence is unnecessary.
jwks-cache = { version = "0.1", features = ["redis"] }
jsonwebtoken = { version = "10.1" }
metrics = { version = "0.24" }
reqwest = { version = "0.12", features = ["http2", "json", "rustls-tls", "stream"] }
tracing = { version = "0.1" }
tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "sync", "time"] }
The crate is fully async and designed for the Tokio multi-threaded runtime.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
// Optional Prometheus exporter (metrics are always sent via the `metrics` facade).
jwks_cache::install_default_exporter()?;
let registry = jwks_cache::Registry::builder()
.require_https(true)
.add_allowed_domain("tenant-a.auth0.com")
.with_redis_client(redis::Client::open("redis://127.0.0.1/")?)
.build();
let mut registration = jwks_cache::IdentityProviderRegistration::new(
"tenant-a",
"auth0",
"https://tenant-a.auth0.com/.well-known/jwks.json",
)?;
registration.stale_while_error = std::time::Duration::from_secs(90);
registry.register(registration).await?;
let jwks = registry.resolve("tenant-a", "auth0", None).await?;
println!("Fetched {} keys.", jwks.keys.len());
// No-op unless the `redis` feature is enabled.
registry.persist_all().await?;
Ok(())
}
Use the registry to resolve a kid and build a DecodingKey for jsonwebtoken:
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use jwks_cache::Registry;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
exp: usize,
aud: Vec<String>,
}
async fn verify(registry: &Registry, token: &str) -> Result<Claims, Box<dyn std::error::Error>> {
let header = jsonwebtoken::decode_header(token)?;
let kid = header.kid.ok_or("token is missing a kid claim")?;
let jwks = registry.resolve("tenant-a", "auth0", Some(&kid)).await?;
let jwk = jwks.find(&kid).ok_or("no JWKS entry found for kid")?;
let decoding_key = DecodingKey::from_jwk(jwk)?;
let mut validation = Validation::new(header.alg);
validation.set_audience(&["api://default"]);
let token = jsonwebtoken::decode::<Claims>(token, &decoding_key, &validation)?;
Ok(token.claims)
}
The optional third argument to Registry::resolve lets you pass the kid up front, enabling cache hits even when providers rotate keys frequently.
Registry keeps tenant/provider state isolated while applying consistent guardrails. The most relevant knobs on IdentityProviderRegistration are:
| Field | Purpose | Default |
|---|---|---|
refresh_early |
Proactive refresh lead time before TTL expiry. | 30s (overridable globally via RegistryBuilder::default_refresh_early) |
stale_while_error |
Serve cached payloads while refreshes fail. | 60s (overridable via default_stale_while_error) |
min_ttl |
Floor applied to upstream cache directives. | 30s |
max_ttl |
Cap applied to upstream TTLs. | 24h |
max_response_bytes |
Maximum JWKS payload size accepted. | 1_048_576 bytes |
negative_cache_ttl |
Optional TTL for failed upstream fetches. | Disabled (0s) |
max_redirects |
Upper bound on HTTP redirects while fetching. | 3 (hard limit 10) |
prefetch_jitter |
Randomised offset applied to refresh scheduling. | 5s |
retry_policy |
Exponential backoff configuration for fetches. | Initial attempt + 2 retries, 250 ms → 2 s backoff, 3 s per attempt, 8 s deadline, full jitter |
pinned_spki |
SHA-256 SPKI fingerprints for TLS pinning. | Empty |
register / unregister keep provider state scoped to each tenant.resolve serves cached JWKS payloads with per-tenant metrics tagging.refresh triggers an immediate background refresh without waiting for TTL expiry.provider_status and all_statuses expose lifecycle state, expiry, error counters, hit rates, and the metrics that power jwks-cache.openapi.yaml.RegistryBuilder::require_https(true) (default) enforces HTTPS for every registration.add_allowed_domain) or per registration (allowed_domains).pinned_spki values (base64 SHA-256) to guard against certificate substitution.redis: enable Redis-backed snapshots for persist_all and restore_from_persistence. When disabled, these methods are cheap no-ops so lifecycle code can stay shared.metrics facade include jwks_cache_requests_total, jwks_cache_hits_total, jwks_cache_misses_total, jwks_cache_stale_total, jwks_cache_refresh_total, jwks_cache_refresh_errors_total, and the jwks_cache_refresh_duration_seconds histogram.install_default_exporter installs the bundled Prometheus recorder (metrics-exporter-prometheus) and exposes a PrometheusHandle for HTTP servers to serve /metrics.tracing spans keyed by tenant and provider identifiers, making it easy to correlate logs, traces, and metrics.Enable the redis feature to persist JWKS payloads between deploys:
let registry = jwks_cache::Registry::builder()
.require_https(true)
.add_allowed_domain("tenant-a.auth0.com")
.with_redis_client(redis::Client::open("redis://127.0.0.1/")?)
.build();
// During startup:
registry.restore_from_persistence().await?;
// On graceful shutdown:
registry.persist_all().await?;
Snapshots store the JWKS body, validators, and expiry metadata, keeping cold starts off identity provider rate limits.
cargo fmtcargo clippy --all-targets --all-featurescargo testcargo test --features redis (integration coverage for Redis persistence)Integration tests rely on wiremock to exercise HTTP caching behaviour, retries, and stale-while-error semantics.
If you find this project helpful and would like to support its development, you can buy me a coffee!
Your support is greatly appreciated and motivates me to keep improving this project.
bc1pedlrf67ss52md29qqkzr2avma6ghyrt4jx9ecp9457qsl75x247sqcp43c0x3e25247CfF03F99a7D83b28F207112234feE73a6156HGo9setPcU2qhFMVWLkcmtCEGySLwNqa3DaEiYSWtte4YThank you for your support!
We would like to extend our heartfelt gratitude to the following projects and contributors:
Grateful for the Rust community and the maintainers of reqwest, http-cache-semantics, metrics, redis, and tracing, whose work makes this cache possible.
Licensed under GPL-3.0.