From e552ffb17629c6a35873580c0c3dc429fe390d6c Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Thu, 14 May 2026 22:15:06 -0700 Subject: [PATCH] Inject Codex auth into credential route proxy --- codex-rs/backend-client/src/client.rs | 11 +++- .../src/config/network_proxy_spec_tests.rs | 1 + codex-rs/core/src/credentialed_routes.rs | 54 ++++++++++++++----- codex-rs/network-proxy/src/lib.rs | 1 + codex-rs/network-proxy/src/mitm.rs | 3 ++ codex-rs/network-proxy/src/mitm_hook.rs | 18 +++++++ codex-rs/network-proxy/src/mitm_tests.rs | 12 +++++ 7 files changed, 86 insertions(+), 14 deletions(-) diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 43d6d14643..ce28d3dad9 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -204,12 +204,17 @@ impl Client { } fn headers(&self) -> HeaderMap { - let mut h = HeaderMap::new(); + let mut h = self.auth_headers(); if let Some(ua) = &self.user_agent { h.insert(USER_AGENT, ua.clone()); } else { h.insert(USER_AGENT, HeaderValue::from_static("codex-cli")); } + h + } + + fn auth_headers(&self) -> HeaderMap { + let mut h = HeaderMap::new(); self.auth_provider.add_auth_headers(&mut h); if let Some(acc) = &self.chatgpt_account_id && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") @@ -423,6 +428,10 @@ impl Client { } } + pub fn credential_routes_proxy_auth_headers(&self) -> HeaderMap { + self.auth_headers() + } + /// Create a new task (user turn) by POSTing to the appropriate backend path /// based on `path_style`. Returns the created task id. pub async fn create_task(&self, request_body: serde_json::Value) -> Result { diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index c0be217b36..1d6dedc7ec 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -97,6 +97,7 @@ fn credentialed_routes_add_runtime_allowlist_and_mitm_hooks() { auth_type: CredentialRouteAuthType::OAuth, base_url: "https://api.example.com/v1".to_string(), }], + proxy_headers: Vec::new(), proxy_url: Some("https://chatgpt.com/backend-api/wham/credential_routes/proxy".to_string()), }; diff --git a/codex-rs/core/src/credentialed_routes.rs b/codex-rs/core/src/credentialed_routes.rs index 70e07de492..c6e8c484b2 100644 --- a/codex-rs/core/src/credentialed_routes.rs +++ b/codex-rs/core/src/credentialed_routes.rs @@ -2,9 +2,11 @@ use codex_backend_client::Client as BackendClient; use codex_backend_client::ResolvedCredentialRoute; use codex_login::CodexAuth; use codex_network_proxy::CredentialedRouteProxyActionConfig; +use codex_network_proxy::CredentialedRouteProxyHeader; use codex_network_proxy::MitmHookActionsConfig; use codex_network_proxy::MitmHookConfig; use codex_network_proxy::MitmHookMatchConfig; +use http::HeaderMap; use tracing::debug; use tracing::warn; use url::Url; @@ -12,6 +14,7 @@ use url::Url; #[derive(Debug, Clone, Default)] pub(crate) struct CredentialedRoutesSessionConfig { pub(crate) routes: Vec, + pub(crate) proxy_headers: Vec, pub(crate) proxy_url: Option, } @@ -39,6 +42,9 @@ pub(crate) async fn load_for_session( ); CredentialedRoutesSessionConfig { routes: response.routes, + proxy_headers: credentialed_route_proxy_headers( + client.credential_routes_proxy_auth_headers(), + ), proxy_url: Some(client.credential_routes_proxy_url()), } } @@ -57,25 +63,28 @@ impl CredentialedRoutesSessionConfig { self.routes .iter() - .filter_map(|route| match route_mitm_hook(route, proxy_url) { - Ok(hook) => Some(hook), - Err(err) => { - warn!( - connector_id = %route.connector_id, - link_id = %route.link_id, - base_url = %route.base_url, - error = %err, - "skipping invalid credentialed route" - ); - None - } - }) + .filter_map( + |route| match route_mitm_hook(route, &self.proxy_headers, proxy_url) { + Ok(hook) => Some(hook), + Err(err) => { + warn!( + connector_id = %route.connector_id, + link_id = %route.link_id, + base_url = %route.base_url, + error = %err, + "skipping invalid credentialed route" + ); + None + } + }, + ) .collect() } } fn route_mitm_hook( route: &ResolvedCredentialRoute, + proxy_headers: &[CredentialedRouteProxyHeader], proxy_url: &str, ) -> anyhow::Result { let base_url = Url::parse(&route.base_url)?; @@ -117,6 +126,7 @@ fn route_mitm_hook( credentialed_route_proxy: Some(CredentialedRouteProxyActionConfig { connector_id: route.connector_id.clone(), link_id: route.link_id.clone(), + proxy_headers: proxy_headers.to_vec(), proxy_url: proxy_url.to_string(), }), ..MitmHookActionsConfig::default() @@ -124,6 +134,16 @@ fn route_mitm_hook( }) } +fn credentialed_route_proxy_headers(headers: HeaderMap) -> Vec { + headers + .iter() + .map(|(name, value)| CredentialedRouteProxyHeader { + name: name.clone(), + value: value.clone(), + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -139,6 +159,10 @@ mod tests { auth_type: CredentialRouteAuthType::OAuth, base_url: "https://api.example.com/v1".to_string(), }], + proxy_headers: vec![CredentialedRouteProxyHeader { + name: http::header::AUTHORIZATION, + value: http::HeaderValue::from_static("Bearer codex-token"), + }], proxy_url: Some( "https://chatgpt.com/backend-api/wham/credential_routes/proxy".to_string(), ), @@ -154,6 +178,10 @@ mod tests { Some(CredentialedRouteProxyActionConfig { connector_id: "connector_123".to_string(), link_id: "link_123".to_string(), + proxy_headers: vec![CredentialedRouteProxyHeader { + name: http::header::AUTHORIZATION, + value: http::HeaderValue::from_static("Bearer codex-token"), + }], proxy_url: "https://chatgpt.com/backend-api/wham/credential_routes/proxy" .to_string(), }) diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 637f6a4682..e918aee6ba 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -25,6 +25,7 @@ pub use config::NetworkUnixSocketPermission; pub use config::NetworkUnixSocketPermissions; pub use config::host_and_port_from_network_addr; pub use mitm_hook::CredentialedRouteProxyActionConfig; +pub use mitm_hook::CredentialedRouteProxyHeader; pub use mitm_hook::InjectedHeaderConfig; pub use mitm_hook::MitmHookActionsConfig; pub use mitm_hook::MitmHookBodyConfig; diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs index af2e0ac066..f1aafddb23 100644 --- a/codex-rs/network-proxy/src/mitm.rs +++ b/codex-rs/network-proxy/src/mitm.rs @@ -584,6 +584,9 @@ fn apply_credentialed_route_proxy_headers( action: &crate::mitm_hook::CredentialedRouteProxyAction, request_uri: &Uri, ) -> Result<()> { + for proxy_header in &action.proxy_headers { + headers.insert(proxy_header.name.clone(), proxy_header.value.clone()); + } headers.insert( HeaderName::from_static(CREDENTIAL_ROUTE_CONNECTOR_ID_HEADER), HeaderValue::from_str(&action.connector_id)?, diff --git a/codex-rs/network-proxy/src/mitm_hook.rs b/codex-rs/network-proxy/src/mitm_hook.rs index 7fe4d81766..ac8d61c28d 100644 --- a/codex-rs/network-proxy/src/mitm_hook.rs +++ b/codex-rs/network-proxy/src/mitm_hook.rs @@ -116,6 +116,7 @@ pub enum SecretSource { pub struct CredentialedRouteProxyActionConfig { pub connector_id: String, pub link_id: String, + pub proxy_headers: Vec, pub proxy_url: String, } @@ -123,9 +124,25 @@ pub struct CredentialedRouteProxyActionConfig { pub struct CredentialedRouteProxyAction { pub connector_id: String, pub link_id: String, + pub proxy_headers: Vec, pub proxy_url: String, } +#[derive(Clone, PartialEq, Eq)] +pub struct CredentialedRouteProxyHeader { + pub name: HeaderName, + pub value: HeaderValue, +} + +impl std::fmt::Debug for CredentialedRouteProxyHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CredentialedRouteProxyHeader") + .field("name", &self.name) + .field("value", &"") + .finish() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MitmHookBodyMatcher { pub raw: serde_json::Value, @@ -339,6 +356,7 @@ where .map(|action| CredentialedRouteProxyAction { connector_id: action.connector_id.clone(), link_id: action.link_id.clone(), + proxy_headers: action.proxy_headers.clone(), proxy_url: action.proxy_url.clone(), }); diff --git a/codex-rs/network-proxy/src/mitm_tests.rs b/codex-rs/network-proxy/src/mitm_tests.rs index e0e9d0bb2d..b68cc2d900 100644 --- a/codex-rs/network-proxy/src/mitm_tests.rs +++ b/codex-rs/network-proxy/src/mitm_tests.rs @@ -282,10 +282,18 @@ fn credentialed_route_proxy_request_rewrites_upstream_target() { let action = crate::mitm_hook::CredentialedRouteProxyAction { connector_id: "connector_123".to_string(), link_id: "link_123".to_string(), + proxy_headers: vec![crate::mitm_hook::CredentialedRouteProxyHeader { + name: HeaderName::from_static("authorization"), + value: HeaderValue::from_static("Bearer codex-token"), + }], proxy_url: "http://localhost:8080/api/codex/credential_routes/proxy".to_string(), }; let request_uri: Uri = "https://api.example.com/v1/items?limit=5".parse().unwrap(); let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_static("authorization"), + HeaderValue::from_static("Bearer provider-token"), + ); headers.insert( HeaderName::from_static(CREDENTIAL_ROUTE_REQUEST_URL_HEADER), HeaderValue::from_static("https://spoofed.example.com"), @@ -299,6 +307,10 @@ fn credentialed_route_proxy_request_rewrites_upstream_target() { "http://localhost:8080/api/codex/credential_routes/proxy" ); assert_eq!(proxy_authority, "localhost:8080"); + assert_eq!( + headers.get("authorization"), + Some(&HeaderValue::from_static("Bearer codex-token")) + ); assert_eq!( headers.get(CREDENTIAL_ROUTE_CONNECTOR_ID_HEADER), Some(&HeaderValue::from_static("connector_123"))