From 9c303ba2c04081bd30a9bd05155a0541eca93a7c Mon Sep 17 00:00:00 2001 From: Anirudh Konidala Date: Sat, 20 Jun 2026 09:43:09 -0500 Subject: [PATCH 1/2] lore-server: support file:// JWKS endpoints in JWK service Signed-off-by: Anirudh Konidala --- lore-server/src/auth/jwk.rs | 115 +++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/lore-server/src/auth/jwk.rs b/lore-server/src/auth/jwk.rs index 57bacab..700caba 100644 --- a/lore-server/src/auth/jwk.rs +++ b/lore-server/src/auth/jwk.rs @@ -90,41 +90,56 @@ impl JwkServiceImpl { return Ok(()); } - let client = reqwest::Client::builder() - .user_agent(user_agent()) - .build() - .map_err(|e| { - warn!("Failed to construct HTTP client: {e:?}"); + let endpoint = reqwest::Url::parse(&self.settings.endpoint).map_err(|e| { + warn!("failed to parse JWKS endpoint as a URL: {e:?}"); + JWKServiceError::InternalError + })?; + + let response_body = if endpoint.scheme() == "file" { + let path = endpoint.to_file_path().map_err(|_err| { + warn!("failed to resolve JWKS file:// endpoint to a path: {endpoint}"); JWKServiceError::InternalError })?; - let response = timed!( - self.latency_histogram_ms(METRICS_OPERATION_LATENCY_METRIC_NAME), - &self.get_labels_for_operation_context("get_keys"), - { - client - .get(&self.settings.endpoint) - .send() - .await - .map_err(|e| { - warn!("Failed to fetch JWKS endpoint: {e:?}"); + tokio::fs::read_to_string(&path).await.map_err(|e| { + warn!("failed to read JWKS file at {}: {e:?}", path.display()); + JWKServiceError::InternalError + })? + } else { + let client = reqwest::Client::builder() + .user_agent(user_agent()) + .build() + .map_err(|e| { + warn!("failed to construct HTTP client: {e:?}"); + JWKServiceError::InternalError + })?; + + let response = timed!( + self.latency_histogram_ms(METRICS_OPERATION_LATENCY_METRIC_NAME), + &self.get_labels_for_operation_context("get_keys"), + { + client.get(endpoint).send().await.map_err(|e| { + warn!("failed to fetch JWKS endpoint: {e:?}"); JWKServiceError::InternalError }) - } - ) - .result?; + } + ) + .result?; - let status = response.status(); - let response_body = response.text().await.map_err(|e| { - warn!("Failed to get response body from JWKS endpoint result: {e:?}"); - JWKServiceError::InternalError - })?; + let status = response.status(); + let body = response.text().await.map_err(|e| { + warn!("failed to get response body from JWKS endpoint result: {e:?}"); + JWKServiceError::InternalError + })?; - if !status.is_success() { - warn!("JWKS endpoint returned error. Status: {status}, response: {response_body}"); + if !status.is_success() { + warn!("JWKS endpoint returned error. Status: {status}, response: {body}"); - return Err(JWKServiceError::InternalError); - } + return Err(JWKServiceError::InternalError); + } + + body + }; let new_jwks: JwkSet = serde_json::from_str(response_body.as_str()).map_err(|e| { warn!("Failed to parse JWKS response: {response_body}"); @@ -189,3 +204,49 @@ impl InstrumentProvider for JwkServiceImpl { "urc.auth.jwk_service" } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn loads_keys_from_file_url() { + let temp_dir = std::env::temp_dir(); + let jwks_path = temp_dir.join("jwk_test_loads_keys_from_file_url.json"); + + std::fs::write( + &jwks_path, + r#"{"keys":[{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFGI","alg":"ES256","kid":"test-key-1"}]}"#, + ) + .unwrap(); + + let endpoint = reqwest::Url::from_file_path(&jwks_path) + .unwrap() + .to_string(); + let settings = JWKServiceSettings { endpoint }; + let service = JwkServiceImpl::new(settings); + + let result = service.fetch_new_keys(None).await; + assert!(result.is_ok(), "{result:?}"); + + let (_, algorithm) = service + .get_key("test-key-1") + .await + .expect("key should be cached after loading from file"); + assert_eq!(algorithm, jsonwebtoken::Algorithm::ES256); + + std::fs::remove_file(&jwks_path).ok(); + } + + #[tokio::test] + async fn file_url_missing_file_returns_error() { + let settings = JWKServiceSettings { + endpoint: "file:///tmp/jwk_test_file_that_does_not_exist.json".to_string(), + }; + let service = JwkServiceImpl::new(settings); + + let result = service.fetch_new_keys(None).await; + + assert!(result.is_err()); + } +} From b6acd9085d1c7ba31e8cd670bc055c76c9cd4bd5 Mon Sep 17 00:00:00 2001 From: Anirudh Konidala Date: Mon, 22 Jun 2026 02:47:43 -0500 Subject: [PATCH 2/2] lore-server: Split JWKS parse-error warning by endpoint scheme Per review feedback, the file:// and http(s):// JWKS load paths shared one generic parse-error warning. Split it so the file case logs "invalid JWKS file contents" and the remote case keeps "failed to parse JWKS response". Signed-off-by: Anirudh Konidala --- lore-server/src/auth/jwk.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lore-server/src/auth/jwk.rs b/lore-server/src/auth/jwk.rs index 700caba..37261a7 100644 --- a/lore-server/src/auth/jwk.rs +++ b/lore-server/src/auth/jwk.rs @@ -95,7 +95,9 @@ impl JwkServiceImpl { JWKServiceError::InternalError })?; - let response_body = if endpoint.scheme() == "file" { + let is_file = endpoint.scheme() == "file"; + + let response_body = if is_file { let path = endpoint.to_file_path().map_err(|_err| { warn!("failed to resolve JWKS file:// endpoint to a path: {endpoint}"); JWKServiceError::InternalError @@ -142,7 +144,11 @@ impl JwkServiceImpl { }; let new_jwks: JwkSet = serde_json::from_str(response_body.as_str()).map_err(|e| { - warn!("Failed to parse JWKS response: {response_body}"); + if is_file { + warn!("invalid JWKS file contents: {response_body}"); + } else { + warn!("failed to parse JWKS response: {response_body}"); + } JWKServiceError::ParseError(e) })?;