5 releases
Uses new Rust 2024
| 0.2.0 | Jan 27, 2026 |
|---|---|
| 0.1.5 | Dec 10, 2025 |
| 0.1.3 | Nov 17, 2025 |
| 0.1.2 | Nov 1, 2025 |
| 0.1.0 | Nov 1, 2025 |
#168 in HTTP client
28 downloads per month
110KB
2.5K
SLoC
jwks-cache
High-performance async JWKS cache with ETag revalidation, early refresh, and multi-tenant support — built for modern Rust identity systems.
Table of Contents
- Why jwks-cache?
- Installation
- Quick Start
- Validating Tokens
- Registry Configuration
- Observability
- Persistence & Warm Starts
- Development
- Support
- Acknowledgements
- License
Why jwks-cache?
- HTTP-aware caching: honours
Cache-Control,Expires,ETag, andLast-Modifiedheaders viahttp-cache-semantics, so refresh cadence tracks the upstream contract instead of guessing TTLs. - Resilient refresh loop: background workers use single-flight guards, exponential backoff with jitter, and bounded stale-while-error windows to minimise pressure on identity providers.
- Multi-tenant registry: isolate registrations per tenant, enforce HTTPS, and restrict redirect targets with domain allowlists or SPKI pinning.
- Built-in observability: metrics, traces, and status snapshots are emitted with tenant/provider labels to simplify debugging and SLO tracking.
- Optional persistence: Redis-backed snapshots allow the cache to warm-start without stampeding third-party JWKS endpoints after deploys or restarts.
Installation
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.
Quick Start
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
// Optional Prometheus exporter (requires the `prometheus` feature).
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(())
}
Validating Tokens
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 Configuration
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 |
Multi-tenant operations
register/unregisterkeep provider state scoped to each tenant.resolveserves cached JWKS payloads with per-tenant metrics tagging.refreshtriggers an immediate background refresh without waiting for TTL expiry.provider_statusandall_statusesexpose lifecycle state, expiry, and error counters, plus hit rates and status metrics when themetricsfeature is enabled.
Security controls
RegistryBuilder::require_https(true)(default) enforces HTTPS for every registration.- Domain allowlists can be applied globally (
add_allowed_domain) or per registration (allowed_domains). - Provide
pinned_spkivalues (base64 SHA-256) to guard against certificate substitution.
Feature flags
- The
redisfeature enables Redis-backed snapshots forpersist_allandrestore_from_persistence. When disabled, these methods are cheap no-ops so lifecycle code can stay shared. - The
metricsfeature enables metrics emission through themetricsfacade. - The
prometheusfeature enablesinstall_default_exporterto install the bundled Prometheus recorder (impliesmetrics). - The default features include
prometheusandmetrics; disable them withdefault-features = false.
Observability
- Metrics emitted via the
metricsfacade (requires themetricsfeature) includejwks_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 thejwks_cache_refresh_duration_secondshistogram. - The
install_default_exporterfunction installs the bundled Prometheus recorder (metrics-exporter-prometheus) and exposes aPrometheusHandlefor HTTP servers to serve/metrics(requires theprometheusfeature). - Every cache operation is instrumented with
tracingspans keyed by tenant and provider identifiers, making it easy to correlate logs, traces, and metrics.
Persistence & Warm Starts
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.
Development
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.
Support Me
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.
- Fiat
- Crypto
- Bitcoin
bc1pedlrf67ss52md29qqkzr2avma6ghyrt4jx9ecp9457qsl75x247sqcp43c
- Ethereum
0x3e25247CfF03F99a7D83b28F207112234feE73a6
- Polkadot
156HGo9setPcU2qhFMVWLkcmtCEGySLwNqa3DaEiYSWtte4Y
- Bitcoin
Thank you for your support!
Appreciation
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.
Additional Acknowledgements
- TODO
License
Licensed under GPL-3.0.
Dependencies
~92MB
~2M SLoC