1use core::fmt;
4use std::collections::HashMap;
5use std::sync::{Arc, PoisonError, RwLock};
6use std::time::{Duration, Instant};
7
8use serde::Deserialize;
9use serde_json::Value;
10use url::Url;
11
12use super::{KeyReference, KeyResolutionError, KeyResolver, KeyRole};
13use crate::jose::Jwk;
14use crate::methods::web::{HttpClient, HttpError};
15
16pub struct JwksResolver<C> {
70 client: C,
71 ttl: Duration,
72 capacity: usize,
73 cache: RwLock<HashMap<Url, Entry>>,
74}
75
76struct Entry {
78 keys: Arc<[Value]>,
79 expires_at: Option<Instant>,
80}
81
82impl Entry {
83 fn is_live(&self, now: Instant) -> bool {
84 self.expires_at.is_none_or(|expires_at| expires_at > now)
85 }
86}
87
88#[derive(Deserialize)]
90struct Metadata {
91 issuer: String,
92 #[serde(default)]
93 jwks: Option<KeySet>,
94 #[serde(default)]
95 jwks_uri: Option<String>,
96}
97
98#[derive(Deserialize)]
101struct KeySet {
102 keys: Vec<Value>,
103}
104
105impl<C> JwksResolver<C> {
106 pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
108
109 pub const DEFAULT_CAPACITY: usize = 256;
111
112 pub fn new(client: C) -> Self {
115 Self {
116 client,
117 ttl: Self::DEFAULT_TTL,
118 capacity: Self::DEFAULT_CAPACITY,
119 cache: RwLock::new(HashMap::new()),
120 }
121 }
122
123 #[must_use]
125 pub fn with_ttl(mut self, ttl: Duration) -> Self {
126 self.ttl = ttl;
127 self
128 }
129
130 #[must_use]
132 pub fn with_capacity(mut self, capacity: usize) -> Self {
133 self.capacity = capacity.max(1);
134 self
135 }
136
137 pub fn ttl(&self) -> Duration {
139 self.ttl
140 }
141
142 pub fn client(&self) -> &C {
144 &self.client
145 }
146
147 fn cached(&self, issuer: &Url) -> Option<Arc<[Value]>> {
149 let cache = self.cache.read().unwrap_or_else(PoisonError::into_inner);
150 cache
151 .get(issuer)
152 .filter(|entry| entry.is_live(Instant::now()))
153 .map(|entry| Arc::clone(&entry.keys))
154 }
155
156 fn store(&self, issuer: &Url, keys: Arc<[Value]>) {
159 if self.ttl.is_zero() {
160 return;
161 }
162 let now = Instant::now();
163 let mut cache = self.cache.write().unwrap_or_else(PoisonError::into_inner);
164 if !cache.contains_key(issuer) && cache.len() >= self.capacity {
165 cache.retain(|_, entry| entry.is_live(now));
166 if cache.len() >= self.capacity
167 && let Some(soonest) = cache
168 .iter()
169 .min_by_key(|(_, entry)| entry.expires_at)
170 .map(|(issuer, _)| issuer.clone())
171 {
172 cache.remove(&soonest);
173 }
174 }
175 cache.insert(
176 issuer.clone(),
177 Entry {
178 keys,
179 expires_at: now.checked_add(self.ttl),
180 },
181 );
182 }
183}
184
185impl<C: HttpClient> JwksResolver<C> {
186 async fn key_set(&self, issuer: &Url) -> Result<Arc<[Value]>, JwksError> {
188 if let Some(keys) = self.cached(issuer) {
189 return Ok(keys);
190 }
191 let url = metadata_url(issuer)?;
192 let metadata: Metadata = self.fetch_json(&url).await?;
193 if Url::parse(&metadata.issuer).ok().as_ref() != Some(issuer) {
194 return Err(JwksError::IssuerMismatch {
195 expected: issuer.clone(),
196 found: metadata.issuer,
197 });
198 }
199 let keys = match (metadata.jwks, metadata.jwks_uri) {
200 (Some(jwks), None) => jwks.keys,
201 (None, Some(jwks_uri)) => {
202 let jwks_uri = Url::parse(&jwks_uri).map_err(|e| JwksError::InvalidMetadata {
203 url: url.clone(),
204 reason: format!("`jwks_uri` {jwks_uri:?}: {e}"),
205 })?;
206 if jwks_uri.scheme() != "https" {
207 return Err(JwksError::NotHttps(jwks_uri));
208 }
209 self.fetch_json::<KeySet>(&jwks_uri).await?.keys
210 }
211 (Some(_), Some(_)) => {
212 return Err(JwksError::InvalidMetadata {
213 url,
214 reason: "both `jwks` and `jwks_uri` are present".to_owned(),
215 });
216 }
217 (None, None) => {
218 return Err(JwksError::InvalidMetadata {
219 url,
220 reason: "neither `jwks` nor `jwks_uri` is present".to_owned(),
221 });
222 }
223 };
224 let keys: Arc<[Value]> = keys.into();
225 self.store(issuer, Arc::clone(&keys));
226 Ok(keys)
227 }
228
229 async fn fetch_json<T: serde::de::DeserializeOwned>(&self, url: &Url) -> Result<T, JwksError> {
231 let response = self
232 .client
233 .get(url)
234 .await
235 .map_err(|source| JwksError::Http {
236 url: url.clone(),
237 source,
238 })?;
239 if !(200..300).contains(&response.status) {
240 return Err(JwksError::HttpStatus {
241 url: url.clone(),
242 status: response.status,
243 });
244 }
245 serde_json::from_slice(&response.body).map_err(|e| JwksError::InvalidMetadata {
246 url: url.clone(),
247 reason: e.to_string(),
248 })
249 }
250}
251
252fn metadata_url(issuer: &Url) -> Result<Url, JwksError> {
254 if issuer.scheme() != "https" {
255 return Err(JwksError::NotHttps(issuer.clone()));
256 }
257 if issuer.query().is_some()
258 || issuer.fragment().is_some()
259 || issuer.host().is_none()
260 || !issuer.username().is_empty()
261 || issuer.password().is_some()
262 {
263 return Err(JwksError::InvalidIssuer(issuer.clone()));
264 }
265 let path = issuer.path().trim_end_matches('/');
266 let mut url = issuer.clone();
267 url.set_path(&format!("/.well-known/jwt-vc-issuer{path}"));
268 Ok(url)
269}
270
271fn select(keys: &[Value], issuer: &Url, kid: &str) -> Result<Jwk, JwksError> {
273 let mut matching = keys
274 .iter()
275 .filter(|key| key.get("kid").and_then(Value::as_str) == Some(kid));
276 let key = matching.next().ok_or_else(|| JwksError::UnknownKid {
277 issuer: issuer.clone(),
278 kid: kid.to_owned(),
279 })?;
280 if matching.next().is_some() {
281 return Err(JwksError::DuplicateKid {
282 issuer: issuer.clone(),
283 kid: kid.to_owned(),
284 });
285 }
286 let jwk: Jwk = serde_json::from_value(key.clone()).map_err(|e| JwksError::InvalidKey {
287 kid: kid.to_owned(),
288 reason: e.to_string(),
289 })?;
290 if jwk.is_private() {
291 return Err(JwksError::InvalidKey {
292 kid: kid.to_owned(),
293 reason: "the key set publishes private material".to_owned(),
294 });
295 }
296 Ok(jwk)
297}
298
299impl<C> fmt::Debug for JwksResolver<C>
300where
301 C: fmt::Debug,
302{
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 f.debug_struct("JwksResolver")
305 .field("client", &self.client)
306 .field("ttl", &self.ttl)
307 .field("capacity", &self.capacity)
308 .finish_non_exhaustive()
309 }
310}
311
312#[async_trait::async_trait]
313impl<C: HttpClient> KeyResolver for JwksResolver<C> {
314 async fn resolve_key(&self, key: &KeyReference, _: KeyRole) -> Result<Jwk, KeyResolutionError> {
315 let KeyReference::Jwks { issuer, kid } = key else {
316 return Err(KeyResolutionError::unsupported(key));
317 };
318 let keys = self.key_set(issuer).await?;
319 Ok(select(&keys, issuer, kid)?)
320 }
321}
322
323#[derive(Debug, thiserror::Error)]
325#[non_exhaustive]
326pub enum JwksError {
327 #[error("`{0}` is not an HTTPS URL")]
329 NotHttps(Url),
330 #[error(
332 "`{0}` is not an issuer identifier: it has a query, a fragment or credentials, or no host"
333 )]
334 InvalidIssuer(Url),
335 #[error("fetching `{url}` failed: {source}")]
337 Http {
338 url: Url,
340 #[source]
342 source: HttpError,
343 },
344 #[error("fetching `{url}` returned HTTP {status}")]
346 HttpStatus {
347 url: Url,
349 status: u16,
351 },
352 #[error("`{url}`: {reason}")]
354 InvalidMetadata {
355 url: Url,
357 reason: String,
359 },
360 #[error("the metadata of `{expected}` names the issuer {found:?}")]
362 IssuerMismatch {
363 expected: Url,
365 found: String,
367 },
368 #[error("issuer `{issuer}` has no key `{kid}`")]
370 UnknownKid {
371 issuer: Url,
373 kid: String,
375 },
376 #[error("issuer `{issuer}` has several keys `{kid}`")]
378 DuplicateKid {
379 issuer: Url,
381 kid: String,
383 },
384 #[error("key `{kid}`: {reason}")]
386 InvalidKey {
387 kid: String,
389 reason: String,
391 },
392}
393
394impl From<JwksError> for KeyResolutionError {
395 fn from(error: JwksError) -> Self {
396 Self::new(error)
397 }
398}