structured_proxy/auth/
jwks.rs1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use jsonwebtoken::jwk::{AlgorithmParameters, EllipticCurve, Jwk, JwkSet, KeyAlgorithm};
11use jsonwebtoken::{Algorithm, DecodingKey};
12use tokio::sync::{Mutex, RwLock};
13
14#[derive(Clone)]
16pub struct VerifyingKey {
17 pub key: Arc<DecodingKey>,
18 pub algorithm: Algorithm,
19}
20
21pub struct JwksCache {
23 uri: String,
24 client: reqwest::Client,
25 keys: RwLock<HashMap<String, VerifyingKey>>,
26 last_refresh: Mutex<Option<Instant>>,
27}
28
29const MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
32
33const JWKS_HTTP_TIMEOUT: Duration = Duration::from_secs(5);
35
36pub(crate) fn build_tls_config() -> rustls::ClientConfig {
42 let mut roots = rustls::RootCertStore::empty();
43 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
44 rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
45 .with_safe_default_protocol_versions()
46 .expect("ring provider supports the default TLS protocol versions")
47 .with_root_certificates(roots)
48 .with_no_client_auth()
49}
50
51impl JwksCache {
52 pub fn new(uri: String) -> Self {
54 let client = reqwest::Client::builder()
55 .timeout(JWKS_HTTP_TIMEOUT)
56 .tls_backend_preconfigured(build_tls_config())
60 .build()
61 .unwrap_or_default();
62 Self {
63 uri,
64 client,
65 keys: RwLock::new(HashMap::new()),
66 last_refresh: Mutex::new(None),
67 }
68 }
69
70 pub async fn key_for(&self, kid: &str) -> Option<VerifyingKey> {
73 if let Some(k) = self.keys.read().await.get(kid).cloned() {
74 return Some(k);
75 }
76 if self.refresh().await.is_err() {
77 return None;
78 }
79 self.keys.read().await.get(kid).cloned()
80 }
81
82 async fn refresh(&self) -> Result<(), String> {
85 {
88 let mut last = self.last_refresh.lock().await;
89 if let Some(t) = *last {
90 let empty = self.keys.read().await.is_empty();
91 if !empty && t.elapsed() < MIN_REFRESH_INTERVAL {
92 return Err("refresh throttled".to_string());
93 }
94 }
95 *last = Some(Instant::now());
96 }
97
98 let set: JwkSet = self
99 .client
100 .get(&self.uri)
101 .send()
102 .await
103 .map_err(|e| format!("JWKS fetch failed: {e}"))?
104 .json()
105 .await
106 .map_err(|e| format!("JWKS decode failed: {e}"))?;
107
108 let new_keys = parse_jwks(&set);
109 *self.keys.write().await = new_keys;
110 Ok(())
111 }
112}
113
114fn parse_jwks(set: &JwkSet) -> HashMap<String, VerifyingKey> {
117 let mut map = HashMap::new();
118 for jwk in &set.keys {
119 let Some(kid) = jwk.common.key_id.clone() else {
120 continue;
121 };
122 let Some(algorithm) = algorithm_for(jwk) else {
123 continue;
124 };
125 if let Ok(key) = DecodingKey::from_jwk(jwk) {
126 map.insert(
127 kid,
128 VerifyingKey {
129 key: Arc::new(key),
130 algorithm,
131 },
132 );
133 }
134 }
135 map
136}
137
138fn algorithm_for(jwk: &Jwk) -> Option<Algorithm> {
144 if let Some(alg) = jwk.common.key_algorithm.and_then(key_algorithm_to_alg) {
145 return Some(alg);
146 }
147 match &jwk.algorithm {
148 AlgorithmParameters::RSA(_) => Some(Algorithm::RS256),
149 AlgorithmParameters::EllipticCurve(ec) => match ec.curve {
150 EllipticCurve::P256 => Some(Algorithm::ES256),
151 EllipticCurve::P384 => Some(Algorithm::ES384),
152 _ => None,
154 },
155 AlgorithmParameters::OctetKeyPair(_) => Some(Algorithm::EdDSA),
156 AlgorithmParameters::OctetKey(_) => None,
157 _ => None,
160 }
161}
162
163fn key_algorithm_to_alg(ka: KeyAlgorithm) -> Option<Algorithm> {
166 Some(match ka {
167 KeyAlgorithm::ES256 => Algorithm::ES256,
168 KeyAlgorithm::ES384 => Algorithm::ES384,
169 KeyAlgorithm::RS256 => Algorithm::RS256,
170 KeyAlgorithm::RS384 => Algorithm::RS384,
171 KeyAlgorithm::RS512 => Algorithm::RS512,
172 KeyAlgorithm::PS256 => Algorithm::PS256,
173 KeyAlgorithm::PS384 => Algorithm::PS384,
174 KeyAlgorithm::PS512 => Algorithm::PS512,
175 KeyAlgorithm::EdDSA => Algorithm::EdDSA,
176 _ => return None,
177 })
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn parse_jwks_keeps_asymmetric_keys_and_maps_algorithms() {
186 let set: JwkSet = serde_json::from_value(serde_json::json!({
189 "keys": [{
190 "kty": "RSA",
191 "kid": "rsa-1",
192 "use": "sig",
193 "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368Qen-JS7-zw04o6sJ9qjp6lFm5_T4nzcCqRfMOgRA_g_S0d7e9k7B0v0vqHr0e1V_o-z0ow5dWpql8-zKj4hQp8sg_Pn8O0R5ZQS4t8hUE-3-r3ftt1YzQ",
194 "e": "AQAB"
195 }]
196 })).unwrap();
197 let keys = parse_jwks(&set);
198 assert!(keys.contains_key("rsa-1"));
199 assert_eq!(keys["rsa-1"].algorithm, Algorithm::RS256);
200 }
201
202 #[test]
203 fn algorithm_prefers_explicit_jwk_alg() {
204 let jwk: Jwk = serde_json::from_value(serde_json::json!({
206 "kty": "EC", "crv": "P-384", "alg": "ES384", "kid": "k",
207 "x": "AAAA", "y": "AAAA"
208 }))
209 .unwrap();
210 assert_eq!(algorithm_for(&jwk), Some(Algorithm::ES384));
211 }
212
213 #[test]
214 fn algorithm_falls_back_to_curve_not_es256() {
215 let jwk: Jwk = serde_json::from_value(serde_json::json!({
217 "kty": "EC", "crv": "P-384", "kid": "k", "x": "AAAA", "y": "AAAA"
218 }))
219 .unwrap();
220 assert_eq!(algorithm_for(&jwk), Some(Algorithm::ES384));
221 }
222
223 #[test]
224 fn parse_jwks_skips_symmetric_and_keyless() {
225 let set: JwkSet = serde_json::from_value(serde_json::json!({
226 "keys": [
227 { "kty": "oct", "kid": "hmac", "k": "c2VjcmV0" },
228 { "kty": "RSA", "n": "0vx7ag", "e": "AQAB" }
229 ]
230 }))
231 .unwrap();
232 assert!(parse_jwks(&set).is_empty());
234 }
235}