From 1bfc4ed49bb39c27caad034a294f1a2f7b5d3f97 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:18:55 -0400 Subject: [PATCH 1/3] fix: address rmcp DNS rebinding advisory --- Cargo.lock | 8 +- Cargo.toml | 2 +- .../apollo-mcp-server/src/host_validation.rs | 327 ++++-------------- .../src/server/states/running.rs | 36 +- .../src/server/states/starting.rs | 19 +- 5 files changed, 101 insertions(+), 291 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d34d351..abdb93da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3887,9 +3887,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6b9d2f0efe2258b23767f1f9e0054cfbcac9c2d6f81a031214143096d7864f" +checksum = "e12ca9067b5ebfbd5b3fcdc4acfceb81aa7d5ab2a879dff7cb75d22434276aad" dependencies = [ "async-trait", "base64", @@ -3918,9 +3918,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e" +checksum = "7caa6743cc0888e433105fe1bc551a7f607940b126a37bc97b478e86064627eb" dependencies = [ "darling 0.23.0", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 0e4ae5e4..1e943a54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ insta = { version = "1.43.1", features = [ "yaml", "glob", ] } -rmcp = { version = "1.2", features = [ +rmcp = { version = "1.6", features = [ "server", "transport-io", "transport-streamable-http-server", diff --git a/crates/apollo-mcp-server/src/host_validation.rs b/crates/apollo-mcp-server/src/host_validation.rs index 26ef686b..ba720c6e 100644 --- a/crates/apollo-mcp-server/src/host_validation.rs +++ b/crates/apollo-mcp-server/src/host_validation.rs @@ -1,26 +1,22 @@ -use std::borrow::Cow; -use std::net::IpAddr; -use std::sync::Arc; - -use axum::{ - body::Body, - extract::{Request, State}, - http::{HeaderValue, StatusCode, header::HOST}, - middleware::Next, - response::{IntoResponse, Response}, -}; +use rmcp::transport::StreamableHttpServerConfig; use schemars::JsonSchema; use serde::Deserialize; -use tracing::warn; /// Configuration for Host header validation to prevent DNS rebinding attacks. +/// +/// Host validation is enforced by rmcp's Streamable HTTP transport via +/// [`StreamableHttpServerConfig::allowed_hosts`]. This struct is a thin, +/// stable surface for our YAML schema; [`HostValidationConfig::apply_to`] +/// translates it onto the rmcp config. #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(default, deny_unknown_fields)] pub struct HostValidationConfig { - /// Enable Host header validation (enabled by default for security) + /// Enable Host header validation (enabled by default for security). pub enabled: bool, - /// Additional allowed hosts beyond localhost, 127.0.0.1, ::1, and 0.0.0.0. + /// Additional allowed hosts beyond the loopback defaults + /// (`localhost`, `127.0.0.1`, `::1`). Entries may be bare hostnames + /// (any port allowed) or `host:port` authorities. pub allowed_hosts: Vec, } @@ -42,268 +38,95 @@ impl HostValidationConfig { allowed_hosts: Vec::new(), } } -} - -/// State for the Host header validation middleware. -#[derive(Clone)] -pub struct HostValidationState { - /// The validation configuration (wrapped in Arc to avoid cloning Vec on each request). - pub config: Arc, - /// The port the server is listening on, used to validate localhost requests. - pub server_port: u16, -} - -impl HostValidationState { - fn is_host_allowed(&self, host: &str) -> bool { - if !self.config.enabled { - return true; - } - - let hostname = host - .rsplit_once(':') - .map(|(h, _)| h) - .unwrap_or(host) - .trim_start_matches('[') - .trim_end_matches(']'); - // Check if hostname is localhost: literal "localhost", loopback (127.0.0.1, ::1), or unspecified (0.0.0.0, ::) - let is_localhost = hostname.eq_ignore_ascii_case("localhost") - || hostname - .parse::() - .map(|ip| ip.is_loopback() || ip.is_unspecified()) - .unwrap_or(false); - - // Localhost: validate port against actual server port - if is_localhost { - if let Some(port_str) = host.rsplit_once(':').map(|(_, p)| p) { - if let Ok(port) = port_str.parse::() { - return port == self.server_port; - } - return false; - } - return true; + /// Apply this host-validation configuration onto an rmcp + /// [`StreamableHttpServerConfig`] and return the updated config. + /// + /// - `enabled = false` -> `disable_allowed_hosts()` (allow any host; not recommended) + /// - `enabled = true, allowed_hosts = []` -> leave rmcp's defaults in place + /// - `enabled = true, allowed_hosts = [..]` -> rmcp's defaults + user-supplied hosts + /// + /// rmcp's `with_allowed_hosts` replaces the list rather than appending, so + /// when the user supplies extra hosts we read rmcp's existing defaults off + /// of `rmcp_config` and merge them back in to preserve loopback access. + #[must_use] + pub(crate) fn apply_to( + &self, + rmcp_config: StreamableHttpServerConfig, + ) -> StreamableHttpServerConfig { + if !self.enabled { + return rmcp_config.disable_allowed_hosts(); } - - // Custom hosts: validate port against config (if specified). - // No port in config means any port is allowed for flexibility with proxies. - for allowed in &self.config.allowed_hosts { - let allowed_hostname = allowed.rsplit_once(':').map(|(h, _)| h).unwrap_or(allowed); - - if hostname.eq_ignore_ascii_case(allowed_hostname) { - if let Some(allowed_port_str) = allowed.rsplit_once(':').map(|(_, p)| p) { - if let Some(host_port_str) = host.rsplit_once(':').map(|(_, p)| p) { - return allowed_port_str == host_port_str; - } - return false; - } - return true; - } + if self.allowed_hosts.is_empty() { + return rmcp_config; } - - false - } -} - -/// Middleware that validates the Host header to prevent DNS rebinding attacks. -pub async fn validate_host( - State(state): State, - request: Request, - next: Next, -) -> Response { - if !state.config.enabled { - return next.run(request).await; - } - - // Extract host from Host header (HTTP/1.1) or URI authority (HTTP/2). - // Use Cow to avoid allocation when Host header is present (common case). - let host: Option> = request - .headers() - .get(HOST) - .and_then(|v| v.to_str().ok()) - .map(Cow::Borrowed) - .or_else(|| { - request.uri().host().map(|h| { - // Include port from URI if present (requires allocation) - match request.uri().port_u16() { - Some(port) => Cow::Owned(format!("{}:{}", h, port)), - None => Cow::Borrowed(h), - } - }) - }); - - match host { - Some(host) => { - if state.is_host_allowed(&host) { - next.run(request).await - } else { - warn!( - host = %host, - "Rejected request with invalid Host header (possible DNS rebinding attack)" - ); - forbidden_response() + let mut merged = rmcp_config.allowed_hosts.clone(); + for host in &self.allowed_hosts { + if !merged.iter().any(|h| h.eq_ignore_ascii_case(host)) { + merged.push(host.clone()); } } - None => { - warn!("Rejected request without Host header"); - forbidden_response() - } + rmcp_config.with_allowed_hosts(merged) } } -fn forbidden_response() -> Response { - ( - StatusCode::FORBIDDEN, - [( - http::header::CONTENT_TYPE, - HeaderValue::from_static("text/plain"), - )], - Body::from("Forbidden: Invalid Host header"), - ) - .into_response() -} - #[cfg(test)] mod tests { use super::*; - use axum::{Router, routing::get}; - use http::{Method, Request, StatusCode}; - use rstest::rstest; - use tower::util::ServiceExt; - fn test_router(config: HostValidationConfig, port: u16) -> Router { - Router::new().route("/test", get(|| async { "ok" })).layer( - axum::middleware::from_fn_with_state( - HostValidationState { - config: Arc::new(config), - server_port: port, - }, - validate_host, - ), - ) + fn rmcp_allowed_hosts_after_apply(config: &HostValidationConfig) -> Vec { + config + .apply_to(StreamableHttpServerConfig::default()) + .allowed_hosts } - fn default_state() -> HostValidationState { - HostValidationState { - config: Arc::new(HostValidationConfig::default()), - server_port: 8000, - } + #[test] + fn default_config_is_enabled() { + let config = HostValidationConfig::default(); + assert!(config.enabled); + assert!(config.allowed_hosts.is_empty()); } - mod is_host_allowed { - use super::*; - - #[rstest] - #[case::localhost("localhost", true)] - #[case::localhost_with_port("localhost:8000", true)] - #[case::ipv4_loopback("127.0.0.1:8000", true)] - #[case::ipv6_loopback("[::1]:8000", true)] - #[case::unspecified_ipv4("0.0.0.0:8000", true)] - #[case::wrong_port("localhost:9999", false)] - #[case::attacker_without_port("attacker.com", false)] - #[case::attacker_with_port("attacker.com:8000", false)] - fn default_config(#[case] host: &str, #[case] expected: bool) { - assert_eq!(default_state().is_host_allowed(host), expected); - } - - #[test] - fn disabled_allows_any_host() { - let state = HostValidationState { - config: Arc::new(HostValidationConfig::disabled()), - server_port: 8000, - }; - assert!(state.is_host_allowed("attacker.com")); - } + #[test] + fn disabled_constructor_returns_disabled_state() { + let config = HostValidationConfig::disabled(); + assert!(!config.enabled); + assert!(config.allowed_hosts.is_empty()); } - mod validate_host_middleware { - use super::*; - - #[rstest] - #[case::localhost("localhost:8000", StatusCode::OK)] - #[case::localhost_without_port("localhost", StatusCode::OK)] - #[case::ipv4_loopback("127.0.0.1:8000", StatusCode::OK)] - #[case::ipv6_loopback("[::1]:8000", StatusCode::OK)] - #[case::unspecified_ipv4("0.0.0.0:8000", StatusCode::OK)] - #[case::case_insensitive("LOCALHOST:8000", StatusCode::OK)] - #[case::attacker("attacker.com", StatusCode::FORBIDDEN)] - #[case::attacker_with_port("attacker.com:8000", StatusCode::FORBIDDEN)] - #[case::wrong_port("localhost:9999", StatusCode::FORBIDDEN)] - #[tokio::test] - async fn default_config(#[case] host: &str, #[case] expected: StatusCode) { - let app = test_router(HostValidationConfig::default(), 8000); - let request = Request::builder() - .method(Method::GET) - .uri("/test") - .header("Host", host) - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - assert_eq!(response.status(), expected); - } - - #[tokio::test] - async fn disabled_allows_any_host() { - let app = test_router(HostValidationConfig::disabled(), 8000); - let request = Request::builder() - .method(Method::GET) - .uri("/test") - .header("Host", "attacker.com") - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[rstest] - #[case::matching_host("mcp.test.com", StatusCode::OK)] - #[case::wrong_host("attacker.com", StatusCode::FORBIDDEN)] - #[tokio::test] - async fn custom_allowed_host(#[case] host: &str, #[case] expected: StatusCode) { - let config = HostValidationConfig { - enabled: true, - allowed_hosts: vec!["mcp.test.com".to_string()], - }; - let app = test_router(config, 8000); - let request = Request::builder() - .method(Method::GET) - .uri("/test") - .header("Host", host) - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - assert_eq!(response.status(), expected); - } + #[test] + fn enabled_default_keeps_rmcp_defaults() { + let hosts = rmcp_allowed_hosts_after_apply(&HostValidationConfig::default()); + assert_eq!(hosts, StreamableHttpServerConfig::default().allowed_hosts); + } - #[rstest] - #[case::matching_port("mcp.test.com:8000", StatusCode::OK)] - #[case::wrong_port("mcp.test.com:9000", StatusCode::FORBIDDEN)] - #[tokio::test] - async fn custom_allowed_host_with_port(#[case] host: &str, #[case] expected: StatusCode) { - let config = HostValidationConfig { - enabled: true, - allowed_hosts: vec!["mcp.test.com:8000".to_string()], - }; - let app = test_router(config, 8000); - let request = Request::builder() - .method(Method::GET) - .uri("/test") - .header("Host", host) - .body(Body::empty()) - .unwrap(); - let response = app.oneshot(request).await.unwrap(); - assert_eq!(response.status(), expected); - } + #[test] + fn disabled_clears_allowed_hosts() { + let hosts = rmcp_allowed_hosts_after_apply(&HostValidationConfig::disabled()); + assert!(hosts.is_empty()); } #[test] - fn default_config_is_enabled() { - let config = HostValidationConfig::default(); - assert!(config.enabled); + fn user_hosts_are_merged_with_rmcp_defaults() { + let config = HostValidationConfig { + enabled: true, + allowed_hosts: vec!["mcp.test.com".into(), "mcp.test.com:8080".into()], + }; + let hosts = rmcp_allowed_hosts_after_apply(&config); + let mut expected = StreamableHttpServerConfig::default().allowed_hosts; + expected.extend(["mcp.test.com".to_string(), "mcp.test.com:8080".to_string()]); + assert_eq!(hosts, expected); } #[test] - fn default_config_has_no_allowed_hosts() { - let config = HostValidationConfig::default(); - assert!(config.allowed_hosts.is_empty()); + fn user_hosts_are_deduplicated_against_defaults() { + let config = HostValidationConfig { + enabled: true, + allowed_hosts: vec!["LOCALHOST".into(), "mcp.test.com".into()], + }; + let hosts = rmcp_allowed_hosts_after_apply(&config); + let mut expected = StreamableHttpServerConfig::default().allowed_hosts; + expected.push("mcp.test.com".to_string()); + assert_eq!(hosts, expected); } } diff --git a/crates/apollo-mcp-server/src/server/states/running.rs b/crates/apollo-mcp-server/src/server/states/running.rs index 0fe48b8a..a78656b4 100644 --- a/crates/apollo-mcp-server/src/server/states/running.rs +++ b/crates/apollo-mcp-server/src/server/states/running.rs @@ -2536,10 +2536,7 @@ mod integration_tests { StreamableHttpService::new( move || Ok(running.clone()), session_manager, - StreamableHttpServerConfig { - stateful_mode: true, - ..Default::default() - }, + StreamableHttpServerConfig::default().with_stateful_mode(true), ) } @@ -2560,6 +2557,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(Body::from(body.to_string())) @@ -2574,6 +2572,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("Mcp-Session-Id", session_id) @@ -2590,6 +2589,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("Mcp-Session-Id", session_id) @@ -2774,10 +2774,7 @@ mod integration_tests { StreamableHttpService::new( move || Ok(running.clone()), session_manager, - StreamableHttpServerConfig { - stateful_mode: true, - ..Default::default() - }, + StreamableHttpServerConfig::default().with_stateful_mode(true), ) } @@ -2798,6 +2795,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(Body::from(body.to_string())) @@ -2812,6 +2810,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("Mcp-Session-Id", session_id) @@ -2832,6 +2831,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("Mcp-Session-Id", session_id) @@ -3006,10 +3006,7 @@ mod integration_tests { StreamableHttpService::new( move || Ok(running.clone()), LocalSessionManager::default().into(), - StreamableHttpServerConfig { - stateful_mode, - ..Default::default() - }, + StreamableHttpServerConfig::default().with_stateful_mode(stateful_mode), ) } @@ -3020,10 +3017,7 @@ mod integration_tests { StreamableHttpService::new( move || Ok(running.clone()), session_manager, - StreamableHttpServerConfig { - stateful_mode: true, - ..Default::default() - }, + StreamableHttpServerConfig::default().with_stateful_mode(true), ) } @@ -3044,6 +3038,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(Body::from(body.to_string())) @@ -3057,6 +3052,7 @@ mod integration_tests { let mut builder = Request::builder() .method("GET") .uri("/mcp") + .header("Host", "localhost:8000") .header("Accept", "text/event-stream"); if let Some(id) = session_id { builder = builder.header("Mcp-Session-Id", id); @@ -3071,6 +3067,7 @@ mod integration_tests { Request::builder() .method("DELETE") .uri("/mcp") + .header("Host", "localhost:8000") .header("Mcp-Session-Id", session_id) .body(Body::empty()) .unwrap() @@ -3329,10 +3326,7 @@ mod integration_tests { StreamableHttpService::new( move || Ok(running.clone()), session_manager, - StreamableHttpServerConfig { - stateful_mode: true, - ..Default::default() - }, + StreamableHttpServerConfig::default().with_stateful_mode(true), ) } @@ -3353,6 +3347,7 @@ mod integration_tests { Request::builder() .method("POST") .uri("/mcp") + .header("Host", "localhost:8000") .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(Body::from(body.to_string())) @@ -3363,6 +3358,7 @@ mod integration_tests { Request::builder() .method("DELETE") .uri("/mcp") + .header("Host", "localhost:8000") .header("Mcp-Session-Id", session_id) .body(Body::empty()) .unwrap() diff --git a/crates/apollo-mcp-server/src/server/states/starting.rs b/crates/apollo-mcp-server/src/server/states/starting.rs index 74bfbdab..c213e72f 100644 --- a/crates/apollo-mcp-server/src/server/states/starting.rs +++ b/crates/apollo-mcp-server/src/server/states/starting.rs @@ -11,7 +11,6 @@ use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info}; -use crate::host_validation::{HostValidationState, validate_host}; use crate::server::states::telemetry::otel_context_middleware; use crate::{ cors::CorsConfig, @@ -205,13 +204,13 @@ impl Starting { info!(port = ?port, address = ?address, "Starting MCP server in Streamable HTTP mode"); let running = running.clone(); let listen_address = SocketAddr::new(address, port); + let http_config = host_validation.apply_to( + StreamableHttpServerConfig::default().with_stateful_mode(stateful_mode), + ); let service = StreamableHttpService::new( move || Ok(running.clone()), LocalSessionManager::default().into(), - StreamableHttpServerConfig { - stateful_mode, - ..Default::default() - }, + http_config, ); let mut router = axum::Router::new().nest_service("/mcp", service); if let Some(auth) = auth { @@ -226,15 +225,7 @@ impl Starting { // include trace context as header into the response .layer(OtelInResponseLayer) // start OpenTelemetry trace on incoming request - .layer(axum::middleware::from_fn(otel_context_middleware)) - // Host header validation to prevent DNS rebinding attacks - .layer(axum::middleware::from_fn_with_state( - HostValidationState { - config: Arc::new(host_validation), - server_port: port, - }, - validate_host, - )); + .layer(axum::middleware::from_fn(otel_context_middleware)); // Add health check endpoint if configured if let Some(health_check) = health_check.filter(|h| h.config().enabled) { From 56dcde18980ba1277fa1680b5e186ab776e22ab2 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:34:58 -0400 Subject: [PATCH 2/3] docs: note rmcp host validation behavior change --- .changeset/bump_rmcp_for_dns_rebinding_advisory.md | 7 +++++++ docs/source/config-file.mdx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/bump_rmcp_for_dns_rebinding_advisory.md diff --git a/.changeset/bump_rmcp_for_dns_rebinding_advisory.md b/.changeset/bump_rmcp_for_dns_rebinding_advisory.md new file mode 100644 index 00000000..24bc592b --- /dev/null +++ b/.changeset/bump_rmcp_for_dns_rebinding_advisory.md @@ -0,0 +1,7 @@ +--- +default: patch +--- + +# Bump `rmcp` to 1.6 to address DNS rebinding advisory + +Updates the `rmcp` Streamable HTTP server transport to 1.6.0, which patches [GHSA-89vp-x53w-74fx](https://github.com/modelcontextprotocol/rust-sdk/security/advisories/GHSA-89vp-x53w-74fx) (CVE-2026-42559). Host header validation is now performed inside `rmcp` itself, with a `tracing::warn!` event on each rejection so log-based alerting on DNS rebinding attempts continues to work; the server's existing `transport.streamable_http.host_validation` configuration is unchanged. diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index 03e16d1a..411e3865 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -285,7 +285,7 @@ These fields are under the `host_validation` key within the `transport` configur -Host validation is only available when using the `streamable_http` transport. Localhost addresses (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) are always allowed when validation is enabled. +Host validation is only available when using the `streamable_http` transport. Loopback addresses (`localhost`, `127.0.0.1`, `::1`) are always allowed when validation is enabled. From 85a96efa64be678a58c1876eca8d5af1e3e7e02e Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Fri, 1 May 2026 13:36:37 -0400 Subject: [PATCH 3/3] chore: install jwks from crates.io --- Cargo.lock | 3 ++- crates/apollo-mcp-server/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abdb93da..429797fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2443,7 +2443,8 @@ dependencies = [ [[package]] name = "jwks" version = "0.5.3" -source = "git+https://github.com/chenhunghan/jwks?tag=v0.5.3#ac248c9ff85dfbeba933916938680da3c3430749" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4cf2e9dd754c698b41139c2ec88304249ecf3dd15cabfeddd48665cdf5c0c" dependencies = [ "base64", "jsonwebtoken", diff --git a/crates/apollo-mcp-server/Cargo.toml b/crates/apollo-mcp-server/Cargo.toml index 68212afb..0cb0d8aa 100644 --- a/crates/apollo-mcp-server/Cargo.toml +++ b/crates/apollo-mcp-server/Cargo.toml @@ -30,7 +30,7 @@ http = "1.3.1" humantime-serde = "1.1.1" jsonschema = "0.42.0" jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } -jwks = { git = "https://github.com/chenhunghan/jwks", tag = "v0.5.3" } +jwks = "0.5.3" lz-str = "0.2.1" opentelemetry = "0.31.0" opentelemetry-otlp = { version = "0.31.0", features = [