use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use bytes::Bytes;
use http::{HeaderMap, StatusCode};
use crate::error::{Error, Result};
use crate::jwk::Jwks;
use crate::metadata::{metadata_url, KeyedMetadata};
pub const MAX_DOCUMENT_BYTES: usize = 1 << 20;
#[async_trait]
pub trait KeyResolver: Send + Sync {
async fn jwks(&self, issuer: &str, dwk: &str) -> Result<Jwks>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoKeys;
#[async_trait]
impl KeyResolver for NoKeys {
async fn jwks(&self, _issuer: &str, _dwk: &str) -> Result<Jwks> {
Err(Error::Discovery("this verifier looks up no keys".into()))
}
}
#[derive(Clone, Debug)]
pub struct Fetched {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Bytes,
}
#[async_trait]
pub trait Fetch: Send + Sync {
async fn get(&self, url: &str, headers: HeaderMap) -> Result<Fetched>;
}
const MAX_CACHED: usize = 256;
pub struct JwksResolver {
fetch: Arc<dyn Fetch>,
ttl: Duration,
cache: Mutex<HashMap<String, (Instant, Jwks)>>,
}
impl std::fmt::Debug for JwksResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwksResolver")
.field("ttl", &self.ttl)
.finish_non_exhaustive()
}
}
impl JwksResolver {
pub fn new(fetch: Arc<dyn Fetch>) -> Self {
Self {
fetch,
ttl: Duration::from_secs(600),
cache: Mutex::new(HashMap::new()),
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
}
async fn json<T: serde::de::DeserializeOwned>(&self, url: &str) -> Result<T> {
let fetched = self.fetch.get(url, HeaderMap::new()).await?;
if !fetched.status.is_success() {
return Err(Error::Discovery(format!(
"{url} answered {}",
fetched.status
)));
}
serde_json::from_slice(&fetched.body)
.map_err(|e| Error::Discovery(format!("{url} is unreadable: {e}")))
}
}
#[async_trait]
impl KeyResolver for JwksResolver {
async fn jwks(&self, issuer: &str, dwk: &str) -> Result<Jwks> {
if !crate::identifier::is_server_identifier(issuer) {
return Err(Error::Discovery(format!(
"{issuer} is not a server identifier"
)));
}
if !crate::metadata::dwk::is_known(dwk) {
return Err(Error::Discovery(format!(
"{dwk} is not a metadata document"
)));
}
let url = metadata_url(issuer, dwk);
if let Some((at, jwks)) = self.cache.lock().ok().and_then(|c| c.get(&url).cloned()) {
if at.elapsed() < self.ttl {
return Ok(jwks);
}
}
let metadata: KeyedMetadata = self.json(&url).await?;
match &metadata.issuer {
Some(named) if named == issuer => {}
Some(named) => {
return Err(Error::Discovery(format!(
"{url} names another issuer, {named}"
)))
}
None => return Err(Error::Discovery(format!("{url} names no issuer"))),
}
let jwks = match (metadata.jwks, metadata.jwks_uri) {
(Some(jwks), _) => jwks,
(None, Some(uri)) if crate::identifier::same_origin(issuer, &uri) => {
self.json(&uri).await?
}
(None, Some(uri)) => {
return Err(Error::Discovery(format!(
"{url} puts its keys on another origin: {uri}"
)))
}
(None, None) => {
return Err(Error::Discovery(format!(
"{url} has neither jwks nor jwks_uri"
)))
}
};
if let Ok(mut cache) = self.cache.lock() {
if cache.len() >= MAX_CACHED {
let oldest = cache
.iter()
.min_by_key(|(_, (at, _))| *at)
.map(|(k, _)| k.clone());
if let Some(oldest) = oldest {
cache.remove(&oldest);
}
}
cache.insert(url, (Instant::now(), jwks.clone()));
}
Ok(jwks)
}
}
#[cfg(feature = "client")]
pub(crate) async fn read_limited(mut response: reqwest::Response, limit: usize) -> Result<Bytes> {
if response
.content_length()
.is_some_and(|len| len > limit as u64)
{
return Err(Error::Http(format!(
"response is larger than {limit} bytes"
)));
}
let mut body = bytes::BytesMut::new();
while let Some(chunk) = response
.chunk()
.await
.map_err(|e| Error::Http(e.to_string()))?
{
if body.len() + chunk.len() > limit {
return Err(Error::Http(format!(
"response is larger than {limit} bytes"
)));
}
body.extend_from_slice(&chunk);
}
Ok(body.freeze())
}
#[cfg(feature = "client")]
#[derive(Clone, Debug)]
pub struct ReqwestFetch {
client: reqwest::Client,
guarded: bool,
}
#[cfg(feature = "client")]
impl ReqwestFetch {
pub fn guarded(denied: crate::client::DeniedNetworks) -> Result<Self> {
Ok(Self {
client: crate::client::guarded_client(denied)?,
guarded: true,
})
}
pub fn new(client: reqwest::Client) -> Self {
Self {
client,
guarded: false,
}
}
}
#[cfg(feature = "client")]
#[async_trait]
impl Fetch for ReqwestFetch {
async fn get(&self, url: &str, headers: HeaderMap) -> Result<Fetched> {
if self.guarded {
if !url.starts_with("https://") {
return Err(Error::Url(format!("refusing to fetch {url}")));
}
crate::client::guard::admit_host(url)?;
}
let response = self
.client
.get(url)
.headers(headers)
.send()
.await
.map_err(|e| Error::Http(format!("{url}: {e}")))?;
let status = response.status();
let headers = response.headers().clone();
let body = read_limited(response, MAX_DOCUMENT_BYTES).await?;
Ok(Fetched {
status,
headers,
body,
})
}
}
#[derive(Clone, Debug, Default)]
pub struct StaticFetch {
documents: Arc<Mutex<HashMap<String, serde_json::Value>>>,
}
impl StaticFetch {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, url: impl Into<String>, document: serde_json::Value) {
if let Ok(mut documents) = self.documents.lock() {
documents.insert(url.into(), document);
}
}
}
#[async_trait]
impl Fetch for StaticFetch {
async fn get(&self, url: &str, _headers: HeaderMap) -> Result<Fetched> {
let document = self.documents.lock().ok().and_then(|d| d.get(url).cloned());
Ok(match document {
Some(document) => Fetched {
status: StatusCode::OK,
headers: HeaderMap::new(),
body: Bytes::from(document.to_string()),
},
None => Fetched {
status: StatusCode::NOT_FOUND,
headers: HeaderMap::new(),
body: Bytes::new(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn keys_are_found_through_the_metadata_document() {
let fetch = StaticFetch::new();
fetch.insert(
"https://ap.example/.well-known/aauth-agent.json",
json!({ "issuer": "https://ap.example", "jwks_uri": "https://ap.example/jwks.json" }),
);
fetch.insert(
"https://ap.example/jwks.json",
json!({ "keys": [{ "kty": "OKP", "kid": "k" }] }),
);
let resolver = JwksResolver::new(Arc::new(fetch));
let jwks = resolver
.jwks("https://ap.example", "aauth-agent.json")
.await
.unwrap();
assert_eq!(jwks.keys[0].kid.as_deref(), Some("k"));
}
#[tokio::test]
async fn a_document_naming_another_issuer_is_refused() {
let fetch = StaticFetch::new();
fetch.insert(
"https://ap.example/.well-known/aauth-agent.json",
json!({ "issuer": "https://evil.example", "jwks": { "keys": [] } }),
);
let resolver = JwksResolver::new(Arc::new(fetch));
let refused = resolver
.jwks("https://ap.example", "aauth-agent.json")
.await
.unwrap_err();
assert!(refused.to_string().contains("another issuer"), "{refused}");
}
#[tokio::test]
async fn keys_are_cached_until_cleared() {
let fetch = StaticFetch::new();
let url = "https://ap.example/.well-known/aauth-agent.json";
fetch.insert(
url,
json!({ "issuer": "https://ap.example", "jwks": { "keys": [{ "kty": "OKP", "kid": "a" }] } }),
);
let resolver = JwksResolver::new(Arc::new(fetch.clone()));
resolver
.jwks("https://ap.example", "aauth-agent.json")
.await
.unwrap();
fetch.insert(
url,
json!({ "issuer": "https://ap.example", "jwks": { "keys": [{ "kty": "OKP", "kid": "b" }] } }),
);
let cached = resolver
.jwks("https://ap.example", "aauth-agent.json")
.await
.unwrap();
assert_eq!(cached.keys[0].kid.as_deref(), Some("a"));
resolver.clear();
let fresh = resolver
.jwks("https://ap.example", "aauth-agent.json")
.await
.unwrap();
assert_eq!(fresh.keys[0].kid.as_deref(), Some("b"));
}
}