structured_proxy/auth/jwks.rs
1//! JWKS fetching and key cache.
2//!
3//! Keys are fetched from the configured JWKS URI and cached by `kid`. An unknown
4//! `kid` triggers a refresh (throttled), which is how key rotation is picked up.
5//! The cached set also ages out: a lookup after [`JwksCache::with_max_age`]
6//! refetches it, so a key the provider has removed stops verifying tokens even
7//! when no request ever names an unknown `kid`.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use jsonwebtoken::jwk::{AlgorithmParameters, EllipticCurve, Jwk, JwkSet, KeyAlgorithm};
14use jsonwebtoken::{Algorithm, DecodingKey};
15use tokio::sync::{Mutex, RwLock};
16
17/// A decoding key plus the signature algorithm it is valid for.
18#[derive(Clone)]
19pub struct VerifyingKey {
20 pub key: Arc<DecodingKey>,
21 pub algorithm: Algorithm,
22}
23
24/// The keys of the last successful fetch.
25#[derive(Default)]
26struct KeySet {
27 keys: HashMap<String, VerifyingKey>,
28 /// When they were fetched; `None` before the first fetch.
29 fetched: Option<Instant>,
30}
31
32/// Fetches and caches JWKS keys by `kid`.
33pub struct JwksCache {
34 uri: String,
35 client: reqwest::Client,
36 set: RwLock<KeySet>,
37 last_refresh: Mutex<Option<Instant>>,
38 max_age: Duration,
39 min_refresh_interval: Duration,
40}
41
42/// Minimum spacing between refreshes, so a flood of bogus `kid`s cannot hammer
43/// the JWKS endpoint.
44pub(crate) const MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
45
46/// Default age after which the cached keys are refetched: the `cache_duration`
47/// Envoy's `remote_jwks` uses.
48pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(300);
49
50/// Bound the worst-case latency of a slow/stalled JWKS endpoint.
51const JWKS_HTTP_TIMEOUT: Duration = Duration::from_secs(5);
52
53impl JwksCache {
54 /// Create a cache for `uri` (keys are loaded lazily on first lookup), whose
55 /// keys are refetched after [`DEFAULT_MAX_AGE`].
56 ///
57 /// # Errors
58 ///
59 /// The HTTPS client cannot be built: the rustls crypto provider installed
60 /// for the process supports neither TLS 1.2 nor TLS 1.3.
61 pub fn new(uri: String) -> Result<Self, String> {
62 let client = reqwest::Client::builder()
63 .timeout(JWKS_HTTP_TIMEOUT)
64 // Hand reqwest a fully preconfigured rustls backend rather than
65 // relying on reqwest to find a provider: no install ordering
66 // constraint, no global side effect, safe for library/test callers.
67 .tls_backend_preconfigured(crate::tls::client_config()?)
68 .build()
69 .map_err(|e| format!("invalid JWKS client: {e}"))?;
70 Ok(Self {
71 uri,
72 client,
73 set: RwLock::new(KeySet::default()),
74 last_refresh: Mutex::new(None),
75 max_age: DEFAULT_MAX_AGE,
76 min_refresh_interval: MIN_REFRESH_INTERVAL,
77 })
78 }
79
80 /// Refetch the keys once they are older than `max_age`. Refreshes stay
81 /// at least a minute apart, so a shorter age behaves as one minute.
82 pub fn with_max_age(mut self, max_age: Duration) -> Self {
83 self.max_age = max_age;
84 self
85 }
86
87 #[cfg(test)]
88 fn with_min_refresh_interval(mut self, interval: Duration) -> Self {
89 self.min_refresh_interval = interval;
90 self
91 }
92
93 /// Resolve the verifying key for `kid`, refreshing from the JWKS endpoint
94 /// once (throttled) if it is not cached or the cached keys are too old.
95 pub async fn key_for(&self, kid: &str) -> Option<VerifyingKey> {
96 {
97 let set = self.set.read().await;
98 if set.fetched.is_some_and(|t| t.elapsed() < self.max_age) {
99 if let Some(key) = set.keys.get(kid) {
100 return Some(key.clone());
101 }
102 }
103 }
104 if let Err(reason) = self.refresh().await {
105 tracing::debug!(%reason, "JWKS not refetched; using the keys already known");
106 }
107 // Answer from the current set whatever the refresh did: another
108 // request may have replaced it meanwhile, and a key it dropped must
109 // not survive in an earlier copy. An unreachable endpoint leaves the
110 // set as it was, so the keys already known keep working: failing every
111 // token while the provider is down would turn its outage into ours.
112 self.set.read().await.keys.get(kid).cloned()
113 }
114
115 /// Fetch the JWKS and replace the cache. Throttled by the minimum refresh
116 /// interval unless no fetch has succeeded yet (first load); a provider
117 /// that answered with no keys has been loaded.
118 async fn refresh(&self) -> Result<(), String> {
119 // The refresh slot is held until the new set is in place: a lookup
120 // that arrives meanwhile waits for it, then finds the refresh throttled
121 // and answers from the new set, never from the aged one a removed key
122 // may still sit in. Only lookups that need a refresh wait; a fresh key
123 // is served before the slot is touched.
124 let mut last = self.last_refresh.lock().await;
125 if let Some(t) = *last {
126 let loaded = self.set.read().await.fetched.is_some();
127 if loaded && t.elapsed() < self.min_refresh_interval {
128 return Err("refresh throttled".to_string());
129 }
130 }
131 *last = Some(Instant::now());
132
133 let response = self
134 .client
135 .get(&self.uri)
136 .send()
137 .await
138 .and_then(reqwest::Response::error_for_status)
139 .map_err(|e| format!("JWKS fetch failed: {e}"))?;
140 let set: JwkSet = response
141 .json()
142 .await
143 .map_err(|e| format!("JWKS decode failed: {e}"))?;
144
145 *self.set.write().await = KeySet {
146 keys: parse_jwks(&set),
147 fetched: Some(Instant::now()),
148 };
149 Ok(())
150 }
151}
152
153/// Build the `kid → VerifyingKey` map from a JWK set, skipping keys without a
154/// `kid`, symmetric keys, or those that fail to convert.
155fn parse_jwks(set: &JwkSet) -> HashMap<String, VerifyingKey> {
156 let mut map = HashMap::new();
157 for jwk in &set.keys {
158 let Some(kid) = jwk.common.key_id.clone() else {
159 continue;
160 };
161 let Some(algorithm) = algorithm_for(jwk) else {
162 continue;
163 };
164 if let Ok(key) = DecodingKey::from_jwk(jwk) {
165 map.insert(
166 kid,
167 VerifyingKey {
168 key: Arc::new(key),
169 algorithm,
170 },
171 );
172 }
173 }
174 map
175}
176
177/// Pick the signature algorithm for a key.
178///
179/// The JWK's explicit `alg` is authoritative (so ES384 / RS512 / PS256 keys are
180/// not mis-pinned). Without it, fall back to the key type and EC curve.
181/// Symmetric keys (`OctetKey`) and unsupported variants are rejected.
182fn algorithm_for(jwk: &Jwk) -> Option<Algorithm> {
183 if let Some(alg) = jwk.common.key_algorithm.and_then(key_algorithm_to_alg) {
184 return Some(alg);
185 }
186 match &jwk.algorithm {
187 AlgorithmParameters::RSA(_) => Some(Algorithm::RS256),
188 AlgorithmParameters::EllipticCurve(ec) => match ec.curve {
189 EllipticCurve::P256 => Some(Algorithm::ES256),
190 EllipticCurve::P384 => Some(Algorithm::ES384),
191 // P-521 (ES512) is not supported by the verifier.
192 _ => None,
193 },
194 AlgorithmParameters::OctetKeyPair(_) => Some(Algorithm::EdDSA),
195 AlgorithmParameters::OctetKey(_) => None,
196 // The enum is non-exhaustive: key types added upstream are not
197 // verifiable here until they are mapped explicitly.
198 _ => None,
199 }
200}
201
202/// Map a JWK signature `alg` to a verifier algorithm, rejecting symmetric and
203/// encryption algorithms (only asymmetric signatures are usable from a JWKS).
204fn key_algorithm_to_alg(ka: KeyAlgorithm) -> Option<Algorithm> {
205 Some(match ka {
206 KeyAlgorithm::ES256 => Algorithm::ES256,
207 KeyAlgorithm::ES384 => Algorithm::ES384,
208 KeyAlgorithm::RS256 => Algorithm::RS256,
209 KeyAlgorithm::RS384 => Algorithm::RS384,
210 KeyAlgorithm::RS512 => Algorithm::RS512,
211 KeyAlgorithm::PS256 => Algorithm::PS256,
212 KeyAlgorithm::PS384 => Algorithm::PS384,
213 KeyAlgorithm::PS512 => Algorithm::PS512,
214 KeyAlgorithm::EdDSA => Algorithm::EdDSA,
215 _ => return None,
216 })
217}
218
219#[cfg(test)]
220mod tests;