use std::sync::Arc;
use serde_json::Value;
use crate::cache::document_cache::{DocumentCache, DocumentChangeCallback, DocumentFetcherFn};
use crate::transport::validate_fetch_url;
use crate::{AuthplaneError, FetchSettings};
pub type MetadataChangeCallback = DocumentChangeCallback;
#[derive(Clone)]
pub struct MetadataCache {
inner: Arc<DocumentCache>,
expected_issuer: String,
fetch_settings: FetchSettings,
}
impl std::fmt::Debug for MetadataCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetadataCache")
.field("expected_issuer", &self.expected_issuer)
.field("inner", &self.inner)
.finish()
}
}
impl MetadataCache {
pub fn new(
fetcher: DocumentFetcherFn,
expected_issuer: impl Into<String>,
fetch_settings: FetchSettings,
refresh_seconds: u64,
on_change: Option<MetadataChangeCallback>,
) -> Self {
let inner = DocumentCache::with_error_factory(
fetcher,
refresh_seconds,
"metadata",
on_change,
Box::new(metadata_error_factory),
);
Self {
inner,
expected_issuer: crate::errors::normalize_issuer(&expected_issuer.into()).to_string(),
fetch_settings,
}
}
pub fn document_cache(&self) -> Arc<DocumentCache> {
self.inner.clone()
}
pub async fn aclose(&self) {
self.inner.aclose().await;
}
pub async fn get_metadata(&self) -> Result<Value, AuthplaneError> {
let document = self.inner.get(false).await?;
self.validate_issuer(&document)?;
Ok(document)
}
pub async fn refresh(&self) -> Result<Value, AuthplaneError> {
let document = self.inner.get(true).await?;
self.validate_issuer(&document)?;
Ok(document)
}
pub(crate) async fn refresh_strict(&self) -> Result<Value, AuthplaneError> {
let document = self.inner.refresh_strict().await?;
self.validate_issuer(&document)?;
Ok(document)
}
pub async fn endpoint(&self, key: &str) -> Result<String, AuthplaneError> {
let metadata = self.get_metadata().await?;
let value = metadata
.get(key)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
metadata_error_factory(&format!("AS metadata missing required '{key}' endpoint"))
})?
.to_string();
validate_fetch_url(&value, &self.fetch_settings, &format!("{key} URL"))?;
Ok(value)
}
pub async fn get_jwks_uri(&self) -> Result<String, AuthplaneError> {
self.endpoint("jwks_uri").await
}
pub async fn get_token_endpoint(&self) -> Result<String, AuthplaneError> {
self.endpoint("token_endpoint").await
}
pub async fn get_introspection_endpoint(&self) -> Result<String, AuthplaneError> {
self.endpoint("introspection_endpoint").await
}
pub async fn get_revocation_endpoint(&self) -> Result<String, AuthplaneError> {
self.endpoint("revocation_endpoint").await
}
fn validate_issuer(&self, document: &Value) -> Result<(), AuthplaneError> {
let discovered = document
.get("issuer")
.and_then(Value::as_str)
.map(crate::errors::normalize_issuer)
.ok_or_else(|| metadata_error_factory("AS metadata missing 'issuer' field"))?;
if discovered != self.expected_issuer {
return Err(metadata_error_factory(&format!(
"AS metadata issuer mismatch: configured {:?}, discovered {:?}",
self.expected_issuer, discovered
)));
}
Ok(())
}
}
fn metadata_error_factory(message: &str) -> AuthplaneError {
crate::errors::metadata_error(message)
}