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
36fn 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 }
158}
159
160fn key_algorithm_to_alg(ka: KeyAlgorithm) -> Option<Algorithm> {
163 Some(match ka {
164 KeyAlgorithm::ES256 => Algorithm::ES256,
165 KeyAlgorithm::ES384 => Algorithm::ES384,
166 KeyAlgorithm::RS256 => Algorithm::RS256,
167 KeyAlgorithm::RS384 => Algorithm::RS384,
168 KeyAlgorithm::RS512 => Algorithm::RS512,
169 KeyAlgorithm::PS256 => Algorithm::PS256,
170 KeyAlgorithm::PS384 => Algorithm::PS384,
171 KeyAlgorithm::PS512 => Algorithm::PS512,
172 KeyAlgorithm::EdDSA => Algorithm::EdDSA,
173 _ => return None,
174 })
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn parse_jwks_keeps_asymmetric_keys_and_maps_algorithms() {
183 let set: JwkSet = serde_json::from_value(serde_json::json!({
186 "keys": [{
187 "kty": "RSA",
188 "kid": "rsa-1",
189 "use": "sig",
190 "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368Qen-JS7-zw04o6sJ9qjp6lFm5_T4nzcCqRfMOgRA_g_S0d7e9k7B0v0vqHr0e1V_o-z0ow5dWpql8-zKj4hQp8sg_Pn8O0R5ZQS4t8hUE-3-r3ftt1YzQ",
191 "e": "AQAB"
192 }]
193 })).unwrap();
194 let keys = parse_jwks(&set);
195 assert!(keys.contains_key("rsa-1"));
196 assert_eq!(keys["rsa-1"].algorithm, Algorithm::RS256);
197 }
198
199 #[test]
200 fn algorithm_prefers_explicit_jwk_alg() {
201 let jwk: Jwk = serde_json::from_value(serde_json::json!({
203 "kty": "EC", "crv": "P-384", "alg": "ES384", "kid": "k",
204 "x": "AAAA", "y": "AAAA"
205 }))
206 .unwrap();
207 assert_eq!(algorithm_for(&jwk), Some(Algorithm::ES384));
208 }
209
210 #[test]
211 fn algorithm_falls_back_to_curve_not_es256() {
212 let jwk: Jwk = serde_json::from_value(serde_json::json!({
214 "kty": "EC", "crv": "P-384", "kid": "k", "x": "AAAA", "y": "AAAA"
215 }))
216 .unwrap();
217 assert_eq!(algorithm_for(&jwk), Some(Algorithm::ES384));
218 }
219
220 #[test]
221 fn parse_jwks_skips_symmetric_and_keyless() {
222 let set: JwkSet = serde_json::from_value(serde_json::json!({
223 "keys": [
224 { "kty": "oct", "kid": "hmac", "k": "c2VjcmV0" },
225 { "kty": "RSA", "n": "0vx7ag", "e": "AQAB" }
226 ]
227 }))
228 .unwrap();
229 assert!(parse_jwks(&set).is_empty());
231 }
232}