jsonwebtoken_jwks_cache/cache/
mod.rs1#[cfg(test)]
2mod test;
3
4use core::fmt;
5use core::future::Future;
6
7use super::pem_set::PemMap;
8use jsonwebtoken::jwk::JwkSet;
9use spin::RwLock;
10use std::sync::Arc;
11use std::time::{Duration, SystemTime};
12use tokio::sync::Notify;
13use url::Url;
14
15struct CacheResetGuard {
16 cache_state: Arc<RwLock<JWKSCache>>,
17 notifier: Option<Arc<Notify>>,
18}
19
20impl CacheResetGuard {
21 pub fn finish_state_update(mut self, result: JWKSCache) {
22 if let Some(notifier) = self.notifier.take() {
23 *self.cache_state.write() = result;
24 notifier.notify_waiters();
25 }
26 }
27}
28
29impl Drop for CacheResetGuard {
30 fn drop(&mut self) {
31 if let Some(notifier) = self.notifier.take() {
32 *self.cache_state.write() = JWKSCache::Empty;
33 notifier.notify_waiters();
34 }
35 }
36}
37
38fn get_expiration(now: SystemTime, req: &reqwest::Request, res: &reqwest::Response) -> SystemTime {
39 now + http_cache_semantics::CachePolicy::new(req, res).time_to_live(now)
40}
41
42pub trait JwksSource: Clone + Send + Sync + 'static {
43 type Error: fmt::Debug + Send + Sync + 'static;
44
45 fn get_jwks_within_deadline(
46 self,
47 url: Url,
48 as_pkeys: bool,
49 now: SystemTime,
50 deadline: Duration,
51 ) -> impl Future<Output = Result<(JwkSet, SystemTime), RequestError<Self::Error>>>
52 + Send
53 + Sync
54 + 'static {
55 async move {
56 let result = tokio::time::timeout(deadline, self.get_jwks(url, as_pkeys, now)).await;
57
58 match result {
59 Ok(res) => res.map_err(RequestError::Client),
60 Err(_) => Err(RequestError::Timeout),
61 }
62 }
63 }
64
65 fn get_jwks(
66 self,
67 url: Url,
68 as_pkeys: bool,
69 now: SystemTime,
70 ) -> impl Future<Output = Result<(JwkSet, SystemTime), Self::Error>> + Send + Sync + 'static;
71}
72
73impl JwksSource for reqwest::Client {
74 type Error = reqwest::Error;
75
76 async fn get_jwks(
77 self,
78 url: Url,
79 as_pkeys: bool,
80 now: SystemTime,
81 ) -> Result<(JwkSet, SystemTime), Self::Error> {
82 let req = reqwest::Request::new(http::Method::GET, url.clone());
83 let res = reqwest::Client::builder()
84 .build()?
85 .execute(
86 req.try_clone().expect("Request should be always copyable"),
88 )
89 .await?
90 .error_for_status()?;
91
92 let expiration = get_expiration(now, &req, &res);
93 let jwks = if as_pkeys {
94 res.json::<PemMap>().await?.into_rsa_jwk_set()
95 } else {
96 res.json::<JwkSet>().await?
97 };
98
99 Ok((jwks, expiration))
100 }
101}
102
103#[derive(Debug, Clone, Default)]
105enum JWKSCache {
106 #[default]
108 Empty,
109 Fetching(Arc<Notify>),
112 Refreshing { expires: SystemTime, jwks: JwkSet },
114 Fetched { expires: SystemTime, jwks: JwkSet },
116}
117
118impl JWKSCache {
119 const fn is_refreshing(&self) -> bool {
120 matches!(self, Self::Refreshing { .. })
121 }
122}
123
124#[derive(Debug, thiserror::Error)]
125pub enum RequestError<E: fmt::Debug> {
126 #[error("Client error: {0}")]
127 Client(E),
128 #[error("Timeout for request completion reached")]
129 Timeout,
130}
131
132impl<E: fmt::Debug> RequestError<E> {
133 pub const fn is_timeout(&self) -> bool {
135 matches!(self, Self::Timeout)
136 }
137}
138
139impl<T: fmt::Debug> From<T> for RequestError<T> {
140 fn from(value: T) -> Self {
141 Self::Client(value)
142 }
143}
144
145#[derive(Debug, Clone, Copy)]
146pub struct TimeoutSpec {
147 pub retries: u8,
149 pub retry_after: Duration,
151 pub backoff: Duration,
153 pub deadline: Duration,
155}
156
157impl Default for TimeoutSpec {
158 fn default() -> Self {
159 Self {
160 retries: 0,
161 retry_after: Duration::from_secs(10),
162 backoff: Duration::ZERO,
163 deadline: Duration::from_secs(10),
164 }
165 }
166}
167
168#[derive(Clone)]
169pub struct CachedJWKS<S> {
170 jwks_url: Url,
171 pkeys: bool,
172 update_period: Duration,
173 timeout_spec: TimeoutSpec,
174 cache_state: Arc<RwLock<JWKSCache>>,
175 source: S,
176}
177
178impl CachedJWKS<reqwest::Client> {
179 pub fn new(
180 jwks_url: Url,
181 update_period: Duration,
183 timeout_spec: TimeoutSpec,
184 ) -> Result<Self, reqwest::Error> {
185 Ok(Self::from_source(
186 jwks_url,
187 false,
188 update_period,
189 timeout_spec,
190 reqwest::Client::builder().build()?,
191 ))
192 }
193
194 pub fn new_rsa_pkeys(
196 pkeys_url: Url,
197 update_period: Duration,
199 timeout_spec: TimeoutSpec,
200 ) -> Result<Self, reqwest::Error> {
201 Ok(Self::from_source(
202 pkeys_url,
203 true,
204 update_period,
205 timeout_spec,
206 reqwest::Client::builder().build()?,
207 ))
208 }
209}
210
211impl<S: JwksSource> CachedJWKS<S> {
212 pub fn from_source(
213 jwks_url: Url,
214 pkeys: bool,
215 update_period: Duration,
216 timeout_spec: TimeoutSpec,
217 source: S,
218 ) -> Self {
219 assert!(
220 update_period > timeout_spec.deadline,
221 "Update period should be greater than timeout deadline"
222 );
223
224 Self {
225 jwks_url,
226 pkeys,
227 update_period,
228 timeout_spec,
229 cache_state: Default::default(),
230 source,
231 }
232 }
233
234 async fn request(
235 source: S,
236 url: Url,
237 as_pkeys: bool,
238 now: SystemTime,
239 timeout: TimeoutSpec,
240 ) -> Result<(JwkSet, SystemTime), RequestError<S::Error>> {
241 let perform = async {
242 let mut retries = 0u8;
243 loop {
244 match source
245 .clone()
246 .get_jwks_within_deadline(url.clone(), as_pkeys, now, timeout.retry_after)
247 .await
248 {
249 Ok(res) => return Ok(res),
250 Err(err) => {
251 if retries == timeout.retries {
252 return Err(err);
253 } else {
254 retries += 1;
255 tokio::time::sleep(timeout.backoff).await;
256 continue;
257 }
258 }
259 }
260 }
261 };
262
263 tokio::time::timeout(timeout.deadline, perform)
264 .await
265 .map_err(|_| RequestError::Timeout)?
266 }
267
268 async fn update_notify(
269 &self,
270 now: SystemTime,
271 ) -> Result<Option<JwkSet>, RequestError<S::Error>> {
272 let notifier = if let Some(mut cached_state) = self.cache_state.try_write() {
273 let notifier = Arc::new(Notify::new());
274
275 *cached_state = JWKSCache::Fetching(notifier.clone());
276
277 notifier
278 } else {
279 return Ok(None);
280 };
281 let guard = CacheResetGuard {
282 cache_state: self.cache_state.clone(),
283 notifier: Some(notifier),
284 };
285
286 let result = Self::request(
287 self.source.clone(),
288 self.jwks_url.clone(),
289 self.pkeys,
290 now,
291 self.timeout_spec,
292 )
293 .await;
294
295 match result {
296 Ok((jwks, expires)) => {
297 guard.finish_state_update(JWKSCache::Fetched {
298 expires,
299 jwks: jwks.clone(),
300 });
301
302 Ok(Some(jwks))
303 }
304 Err(err) => Err(err),
306 }
307 }
308
309 fn update_in_background(&self, now: SystemTime, old_jwks: JwkSet, old_expires: SystemTime) {
312 {
313 let mut cache_state = self.cache_state.write();
314
315 if cache_state.is_refreshing() {
318 return;
319 }
320
321 *cache_state = JWKSCache::Refreshing {
322 expires: old_expires,
323 jwks: old_jwks,
324 };
325 }
326
327 let cache_state = self.cache_state.clone();
328 let jwks_url = self.jwks_url.clone();
329 let timeout_spec = self.timeout_spec;
330 let source = self.source.clone();
331 let as_pkeys = self.pkeys;
332
333 tokio::spawn(async move {
334 loop {
335 let result = Self::request(
336 source.clone(),
337 jwks_url.clone(),
338 as_pkeys,
339 now,
340 timeout_spec,
341 )
342 .await;
343
344 if let Err(err) = &result {
345 log::error!("Error while refreshing JWKS in the background: {err:?}");
346 }
347
348 let mut cache_state = cache_state.write();
349
350 let new_state = match cache_state.to_owned() {
351 JWKSCache::Empty => match result {
352 Ok((jwks, expires)) => JWKSCache::Fetched { expires, jwks },
353 Err(_) => JWKSCache::Empty,
354 },
355 JWKSCache::Fetching(notify) => {
356 if let Ok((jwks, expires)) = result {
357 notify.notify_waiters();
358 JWKSCache::Fetched { expires, jwks }
359 } else {
360 JWKSCache::Fetching(notify)
361 }
362 }
363 JWKSCache::Refreshing { expires, .. } => {
364 if let Ok((jwks, expires)) = result {
365 JWKSCache::Fetched { expires, jwks }
366 } else if SystemTime::now() >= expires {
367 JWKSCache::Empty
369 } else {
370 continue;
372 }
373 }
374 JWKSCache::Fetched { expires, jwks } => {
375 if let Ok((jwks, expires)) = result {
376 JWKSCache::Fetched { expires, jwks }
377 } else {
378 JWKSCache::Fetched { expires, jwks }
380 }
381 }
382 };
383
384 *cache_state = new_state;
385 break;
386 }
387 });
388 }
389
390 pub async fn get(&self) -> Result<JwkSet, RequestError<S::Error>> {
391 let now = SystemTime::now();
392 loop {
393 let cached_state = self.cache_state.read().clone();
394
395 match cached_state {
396 JWKSCache::Empty => {
397 if let Some(jwks) = self.update_notify(now).await? {
398 return Ok(jwks);
399 } else {
400 continue;
402 }
403 }
404 JWKSCache::Fetching(notifier) => {
405 notifier.notified().await;
406
407 continue;
409 }
410 JWKSCache::Refreshing { expires: _, jwks } => {
411 return Ok(jwks);
413 }
414 JWKSCache::Fetched { expires, jwks } => {
415 if now >= expires {
416 if let Some(jwks) = self.update_notify(now).await? {
417 return Ok(jwks);
418 } else {
419 continue;
421 }
422 }
423
424 if now + self.update_period >= expires {
425 self.update_in_background(now, jwks.clone(), expires);
426 }
427
428 return Ok(jwks);
429 }
430 }
431 }
432 }
433}