Inject Codex auth into credential route proxy

This commit is contained in:
Winston Howes
2026-05-15 19:16:20 -07:00
parent dba0f036dc
commit e552ffb176
7 changed files with 86 additions and 14 deletions
+10 -1
View File
@@ -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<String> {
@@ -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()),
};
+41 -13
View File
@@ -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<ResolvedCredentialRoute>,
pub(crate) proxy_headers: Vec<CredentialedRouteProxyHeader>,
pub(crate) proxy_url: Option<String>,
}
@@ -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<MitmHookConfig> {
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<CredentialedRouteProxyHeader> {
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(),
})
+1
View File
@@ -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;
+3
View File
@@ -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)?,
+18
View File
@@ -116,6 +116,7 @@ pub enum SecretSource {
pub struct CredentialedRouteProxyActionConfig {
pub connector_id: String,
pub link_id: String,
pub proxy_headers: Vec<CredentialedRouteProxyHeader>,
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<CredentialedRouteProxyHeader>,
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", &"<redacted>")
.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(),
});
+12
View File
@@ -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"))