use crate::error::OAuthError;
use jsonwebtoken::{jwk::JwkSet, DecodingKey};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub(crate) struct Cache {
inner: Arc<RwLock<HashMap<String, DecodingKey>>>,
http: reqwest::Client,
jwks_uri: String,
}
impl Cache {
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
http: reqwest::Client::new(),
jwks_uri: "http://localhost/not-used".into(),
}
}
pub(crate) async fn fetch(http: reqwest::Client, jwks_uri: String) -> Result<Self, OAuthError> {
let cache = Self {
inner: Arc::new(RwLock::new(HashMap::new())),
http,
jwks_uri,
};
cache.refresh().await?;
Ok(cache)
}
pub(crate) async fn refresh(&self) -> Result<(), OAuthError> {
let resp = self
.http
.get(&self.jwks_uri)
.send()
.await
.map_err(|e| OAuthError::JwksFetch(e.to_string()))?;
if !resp.status().is_success() {
return Err(OAuthError::JwksFetch(format!("status {}", resp.status())));
}
let set: JwkSet = resp
.json()
.await
.map_err(|e| OAuthError::JwksFetch(e.to_string()))?;
let next = parse_jwk_set(&set)?;
let mut guard = self.inner.write().await;
*guard = next;
Ok(())
}
pub(crate) async fn get(&self, kid: &str) -> Option<DecodingKey> {
self.inner.read().await.get(kid).cloned()
}
}
fn parse_jwk_set(set: &JwkSet) -> Result<HashMap<String, DecodingKey>, OAuthError> {
let mut next: HashMap<String, DecodingKey> = HashMap::new();
for jwk in &set.keys {
let Some(kid) = jwk.common.key_id.as_ref() else {
continue;
};
let dk = DecodingKey::from_jwk(jwk)
.map_err(|e| OAuthError::JwksFetch(format!("from_jwk: {e}")))?;
next.insert(kid.clone(), dk);
}
Ok(next)
}