use std::time::{Duration, Instant};
use jsonwebtoken::DecodingKey;
use serde::Deserialize;
use crate::security::{
errors::{Result, SecurityError},
oidc::token::OidcValidator,
};
pub const MAX_JWKS_RESPONSE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Deserialize)]
pub struct OidcDiscoveryDocument {
pub issuer: String,
pub jwks_uri: String,
#[serde(default)]
pub id_token_signing_alg_values_supported: Vec<String>,
#[serde(default)]
pub authorization_endpoint: Option<String>,
#[serde(default)]
pub token_endpoint: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Jwks {
pub keys: Vec<Jwk>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Jwk {
pub kty: String,
pub kid: Option<String>,
#[serde(default)]
pub alg: Option<String>,
#[serde(rename = "use")]
pub key_use: Option<String>,
pub n: Option<String>,
pub e: Option<String>,
#[serde(default)]
pub x5c: Vec<String>,
}
#[derive(Debug)]
pub struct CachedJwks {
pub(super) jwks: Jwks,
pub(super) fetched_at: Instant,
pub(super) ttl: Duration,
}
impl CachedJwks {
pub(super) fn is_expired(&self) -> bool {
self.fetched_at.elapsed() > self.ttl
}
}
impl OidcValidator {
pub(super) async fn get_decoding_key(&self, kid: &str) -> Result<DecodingKey> {
{
let cache = self.jwks_cache.read();
if let Some(ref cached) = *cache {
if !cached.is_expired() {
if let Some(key) = self.find_key(&cached.jwks, kid) {
return self.jwk_to_decoding_key(key);
}
}
}
}
let jwks = self.fetch_jwks().await?;
if self.detect_key_rotation(&jwks) {
tracing::warn!(
"OIDC key rotation detected: previously cached keys are no longer published \
by the provider; evicting them so tokens signed by rotated-out keys are rejected"
);
}
let key = jwks.keys.iter().find(|k| k.kid.as_deref() == Some(kid)).cloned();
{
let mut cache = self.jwks_cache.write();
*cache = Some(CachedJwks {
jwks,
fetched_at: Instant::now(),
ttl: Duration::from_secs(self.config.jwks_cache_ttl_secs),
});
}
let key = key.ok_or_else(|| {
tracing::debug!(kid = %kid, "Key not found in JWKS");
SecurityError::InvalidToken
})?;
self.jwk_to_decoding_key(&key)
}
pub fn invalidate_jwks_cache(&self) {
*self.jwks_cache.write() = None;
tracing::info!(
"JWKS cache invalidated; next token validation will refetch keys from the provider"
);
}
pub async fn refresh_jwks(&self) -> Result<usize> {
let jwks = self.fetch_jwks().await?;
let key_count = jwks.keys.len();
{
let mut cache = self.jwks_cache.write();
*cache = Some(CachedJwks {
jwks,
fetched_at: Instant::now(),
ttl: Duration::from_secs(self.config.jwks_cache_ttl_secs),
});
}
tracing::info!(key_count, "JWKS cache force-refreshed from provider");
Ok(key_count)
}
async fn fetch_jwks(&self) -> Result<Jwks> {
tracing::debug!(uri = %self.jwks_uri, "Fetching JWKS");
let response = self.http_client.get(&self.jwks_uri).send().await.map_err(|e| {
tracing::error!(error = %e, "Failed to fetch JWKS");
SecurityError::SecurityConfigError(format!("Failed to fetch JWKS: {e}"))
})?;
if !response.status().is_success() {
return Err(SecurityError::SecurityConfigError(format!(
"JWKS fetch failed with status: {}",
response.status()
)));
}
let body_bytes = response.bytes().await.map_err(|e| {
SecurityError::SecurityConfigError(format!("Failed to read JWKS response body: {e}"))
})?;
if body_bytes.len() > MAX_JWKS_RESPONSE_BYTES {
return Err(SecurityError::SecurityConfigError(format!(
"JWKS response body too large ({} bytes, max {MAX_JWKS_RESPONSE_BYTES})",
body_bytes.len()
)));
}
let jwks: Jwks = serde_json::from_slice(&body_bytes).map_err(|e| {
SecurityError::SecurityConfigError(format!("Invalid JWKS response: {e}"))
})?;
tracing::debug!(key_count = jwks.keys.len(), "JWKS fetched successfully");
Ok(jwks)
}
pub(super) fn find_key<'a>(&self, jwks: &'a Jwks, kid: &str) -> Option<&'a Jwk> {
jwks.keys.iter().find(|k| k.kid.as_deref() == Some(kid))
}
pub(super) fn detect_key_rotation(&self, new_jwks: &Jwks) -> bool {
let cache = self.jwks_cache.read();
if let Some(ref cached) = *cache {
let old_kids: std::collections::HashSet<_> =
cached.jwks.keys.iter().filter_map(|k| k.kid.as_deref()).collect();
let new_kids: std::collections::HashSet<_> =
new_jwks.keys.iter().filter_map(|k| k.kid.as_deref()).collect();
!old_kids.is_subset(&new_kids)
} else {
false
}
}
pub(super) fn jwk_to_decoding_key(&self, jwk: &Jwk) -> Result<DecodingKey> {
match jwk.kty.as_str() {
"RSA" => {
let n = jwk.n.as_ref().ok_or(SecurityError::InvalidToken)?;
let e = jwk.e.as_ref().ok_or(SecurityError::InvalidToken)?;
DecodingKey::from_rsa_components(n, e).map_err(|e| {
tracing::debug!(error = %e, "Failed to create RSA decoding key");
SecurityError::InvalidToken
})
},
other => {
tracing::debug!(key_type = %other, "Unsupported key type");
Err(SecurityError::InvalidTokenAlgorithm {
algorithm: other.to_string(),
})
},
}
}
}