Route auth proxy resolution by actual request URL

This commit is contained in:
canvrno-oai
2026-05-27 17:57:14 -07:00
parent 28753b3487
commit 8ff65cb09c
5 changed files with 64 additions and 64 deletions

View File

@@ -254,13 +254,6 @@ impl RequestOrigin {
.or_else(|| default_port_for_scheme(&scheme))?;
Some(Self { scheme, host, port })
}
fn cache_key(&self, include_auto_detect: bool) -> String {
format!(
"{}://{}:{}:auto_detect={include_auto_detect}",
self.scheme, self.host, self.port
)
}
}
fn default_port_for_scheme(scheme: &str) -> Option<u16> {
@@ -292,12 +285,12 @@ fn resolve_system_proxy(
origin: &RequestOrigin,
include_auto_detect: bool,
) -> SystemProxyDecision {
if let Some(decision) = cached_system_proxy_decision(origin, include_auto_detect) {
if let Some(decision) = cached_system_proxy_decision(request_url, include_auto_detect) {
return decision;
}
let decision = resolve_platform_system_proxy(request_url, origin, include_auto_detect);
cache_system_proxy_decision(origin, include_auto_detect, decision.clone());
cache_system_proxy_decision(request_url, include_auto_detect, decision.clone());
decision
}
@@ -332,12 +325,12 @@ static SYSTEM_PROXY_CACHE: OnceLock<Mutex<HashMap<String, CachedSystemProxyDecis
OnceLock::new();
fn cached_system_proxy_decision(
origin: &RequestOrigin,
request_url: &str,
include_auto_detect: bool,
) -> Option<SystemProxyDecision> {
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().ok()?;
let key = origin.cache_key(include_auto_detect);
let key = system_proxy_cache_key(request_url, include_auto_detect);
let cached = cache.get(&key)?;
if cached.expires_at > Instant::now() {
return Some(cached.decision.clone());
@@ -347,14 +340,14 @@ fn cached_system_proxy_decision(
}
fn cache_system_proxy_decision(
origin: &RequestOrigin,
request_url: &str,
include_auto_detect: bool,
decision: SystemProxyDecision,
) {
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut cache) = cache.lock() {
cache.insert(
origin.cache_key(include_auto_detect),
system_proxy_cache_key(request_url, include_auto_detect),
CachedSystemProxyDecision {
decision,
expires_at: Instant::now() + SYSTEM_PROXY_CACHE_TTL,
@@ -363,6 +356,10 @@ fn cache_system_proxy_decision(
}
}
fn system_proxy_cache_key(request_url: &str, include_auto_detect: bool) -> String {
format!("{request_url}:auto_detect={include_auto_detect}")
}
fn conventional_proxy_env_present() -> bool {
proxy_env_value("HTTPS_PROXY").is_some()
|| proxy_env_value("HTTP_PROXY").is_some()
@@ -628,4 +625,12 @@ mod tests {
assert!(no_proxy_matches_origin("auth.openai.com:443", &origin));
assert!(!no_proxy_matches_origin("auth.openai.com:8443", &origin));
}
#[test]
fn system_proxy_cache_key_preserves_url_specific_pac_decisions() {
assert_ne!(
system_proxy_cache_key("https://auth.openai.com/oauth/token", false),
system_proxy_cache_key("https://auth.openai.com/oauth/revoke", false)
);
}
}

View File

@@ -1,10 +1,11 @@
use codex_agent_identity::AgentIdentityKey;
use codex_agent_identity::agent_task_registration_url;
use codex_agent_identity::register_agent_task;
use codex_client::OutboundProxyConfig;
use codex_protocol::account::PlanType as AccountPlanType;
use std::env;
use crate::default_client::build_auth_reqwest_client_with_proxy_config;
use crate::default_client::build_default_reqwest_client_for_auth_route;
use super::storage::AgentIdentityAuthRecord;
@@ -27,8 +28,10 @@ impl AgentIdentityAuth {
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> std::io::Result<Self> {
let agent_identity_authapi_base_url = agent_identity_authapi_base_url();
let client = build_auth_reqwest_client_with_proxy_config(
&agent_identity_authapi_base_url,
let task_registration_url =
agent_task_registration_url(&agent_identity_authapi_base_url, &record.agent_runtime_id);
let client = build_default_reqwest_client_for_auth_route(
&task_registration_url,
outbound_proxy_config,
)?;
let process_task_id =

View File

@@ -4,6 +4,7 @@
//! Use [`crate::default_client`] or [`codex_login::default_client`] from other crates in this
//! workspace.
use codex_client::BuildCustomCaTransportError;
use codex_client::BuildProxiedHttpClientError;
use codex_client::CodexHttpClient;
pub use codex_client::CodexRequestBuilder;
@@ -39,7 +40,6 @@ pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(||
pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency";
const DEFAULT_AUTH_ROUTE_URL: &str = "https://auth.openai.com/oauth/token";
pub use codex_config::ResidencyRequirement;
@@ -198,13 +198,6 @@ pub fn create_client() -> CodexHttpClient {
CodexHttpClient::new(inner)
}
pub fn create_client_with_proxy_config(
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> CodexHttpClient {
let inner = build_reqwest_client_with_proxy_config(outbound_proxy_config);
CodexHttpClient::new(inner)
}
/// Builds the default reqwest client used for ordinary Codex HTTP traffic.
///
/// This starts from the standard Codex user agent, default headers, and sandbox-specific proxy
@@ -212,13 +205,7 @@ pub fn create_client_with_proxy_config(
/// `SSL_CERT_FILE`. The function remains infallible for compatibility with existing call sites, so
/// a custom-CA or builder failure is logged and falls back to `reqwest::Client::new()`.
pub fn build_reqwest_client() -> reqwest::Client {
build_reqwest_client_with_proxy_config(/*outbound_proxy_config*/ None)
}
pub fn build_reqwest_client_with_proxy_config(
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> reqwest::Client {
try_build_reqwest_client_with_proxy_config(outbound_proxy_config).unwrap_or_else(|error| {
try_build_reqwest_client().unwrap_or_else(|error| {
tracing::warn!(error = %error, "failed to build default reqwest client");
with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder())
.build()
@@ -236,25 +223,16 @@ pub fn build_reqwest_client_with_proxy_config(
///
/// Callers that need a structured CA-loading failure instead of the legacy logged fallback can use
/// this method directly.
pub fn try_build_reqwest_client() -> Result<reqwest::Client, BuildProxiedHttpClientError> {
try_build_reqwest_client_with_proxy_config(/*outbound_proxy_config*/ None)
pub fn try_build_reqwest_client() -> Result<reqwest::Client, BuildCustomCaTransportError> {
build_reqwest_client_with_custom_ca(default_reqwest_client_builder())
}
pub fn try_build_reqwest_client_with_proxy_config(
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> Result<reqwest::Client, BuildProxiedHttpClientError> {
fn default_reqwest_client_builder() -> reqwest::ClientBuilder {
let mut builder = reqwest::Client::builder().default_headers(default_headers());
let sandboxed = is_sandboxed();
if sandboxed {
if is_sandboxed() {
builder = builder.no_proxy();
}
builder = with_chatgpt_cloudflare_cookie_store(builder);
if sandboxed {
return build_reqwest_client_with_custom_ca(builder).map_err(Into::into);
}
build_auth_reqwest_client_with_builder(builder, DEFAULT_AUTH_ROUTE_URL, outbound_proxy_config)
with_chatgpt_cloudflare_cookie_store(builder)
}
pub(crate) fn build_auth_reqwest_client_with_proxy_config(
@@ -268,6 +246,26 @@ pub(crate) fn build_auth_reqwest_client_with_proxy_config(
)
}
/// Builds the standard Codex reqwest client for a known auth destination.
pub(crate) fn build_default_reqwest_client_for_auth_route(
endpoint: &str,
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> Result<reqwest::Client, BuildProxiedHttpClientError> {
let builder = default_reqwest_client_builder();
if is_sandboxed() {
return build_reqwest_client_with_custom_ca(builder).map_err(Into::into);
}
build_auth_reqwest_client_with_builder(builder, endpoint, outbound_proxy_config)
}
pub(crate) fn create_client_for_auth_route(
endpoint: &str,
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> Result<CodexHttpClient, BuildProxiedHttpClientError> {
build_default_reqwest_client_for_auth_route(endpoint, outbound_proxy_config)
.map(CodexHttpClient::new)
}
fn build_auth_reqwest_client_with_builder(
builder: reqwest::ClientBuilder,
endpoint: &str,

View File

@@ -17,6 +17,7 @@ use std::sync::atomic::Ordering;
use tokio::sync::Semaphore;
use tokio::sync::watch;
use codex_agent_identity::agent_identity_jwks_url;
use codex_agent_identity::decode_agent_identity_jwt;
use codex_agent_identity::fetch_agent_identity_jwks;
use codex_app_server_protocol::AuthMode;
@@ -32,14 +33,12 @@ pub use crate::auth::storage::AuthDotJson;
use crate::auth::storage::AuthStorageBackend;
use crate::auth::storage::create_auth_storage;
use crate::auth::util::try_parse_error_message;
use crate::default_client::build_auth_reqwest_client_with_proxy_config;
use crate::default_client::create_client;
use crate::default_client::create_client_with_proxy_config;
use crate::default_client::build_default_reqwest_client_for_auth_route;
use crate::default_client::create_client_for_auth_route;
use crate::outbound_proxy::outbound_proxy_config_from_network_config;
use crate::token_data::TokenData;
use crate::token_data::parse_chatgpt_jwt_claims;
use crate::token_data::parse_jwt_expiration;
use codex_client::CodexHttpClient;
use codex_client::OutboundProxyConfig;
use codex_config::types::AuthCredentialsStoreMode;
use codex_config::types::NetworkConfigToml;
@@ -84,7 +83,6 @@ pub struct ChatgptAuthTokens {
#[derive(Debug, Clone)]
struct ChatgptAuthState {
auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
client: CodexHttpClient,
}
const TOKEN_REFRESH_INTERVAL: i64 = 8;
@@ -210,7 +208,6 @@ impl CodexAuth {
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> std::io::Result<Self> {
let auth_mode = auth_dot_json.resolved_mode();
let client = create_client_with_proxy_config(outbound_proxy_config);
if auth_mode == ApiAuthMode::ApiKey {
let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else {
return Err(std::io::Error::other("API key auth is missing a key."));
@@ -234,7 +231,6 @@ impl CodexAuth {
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
let state = ChatgptAuthState {
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
client,
};
match auth_mode {
@@ -467,10 +463,8 @@ impl CodexAuth {
agent_identity: None,
};
let client = create_client();
let state = ChatgptAuthState {
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
client,
};
let dummy_auth_id = NEXT_DUMMY_AUTH_ID.fetch_add(1, Ordering::Relaxed);
let storage = create_auth_storage(
@@ -500,10 +494,6 @@ impl ChatgptAuth {
fn storage(&self) -> &Arc<dyn AuthStorageBackend> {
&self.storage
}
fn client(&self) -> &CodexHttpClient {
&self.state.client
}
}
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
@@ -538,8 +528,8 @@ async fn verified_agent_identity_record(
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> std::io::Result<AgentIdentityAuthRecord> {
AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
let client =
build_auth_reqwest_client_with_proxy_config(chatgpt_base_url, outbound_proxy_config)?;
let jwks_url = agent_identity_jwks_url(chatgpt_base_url);
let client = build_default_reqwest_client_for_auth_route(&jwks_url, outbound_proxy_config)?;
let jwks = fetch_agent_identity_jwks(&client, chatgpt_base_url)
.await
.map_err(std::io::Error::other)?;
@@ -902,7 +892,7 @@ fn persist_tokens(
// The caller is responsible for persisting any returned tokens.
async fn request_chatgpt_token_refresh(
refresh_token: String,
client: &CodexHttpClient,
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> Result<RefreshResponse, RefreshTokenError> {
let refresh_request = RefreshRequest {
client_id: CLIENT_ID,
@@ -911,6 +901,8 @@ async fn request_chatgpt_token_refresh(
};
let endpoint = refresh_token_endpoint();
let client = create_client_for_auth_route(&endpoint, outbound_proxy_config)
.map_err(|err| RefreshTokenError::Transient(err.into()))?;
// Use shared client factory to include standard headers
let response = client
@@ -2020,7 +2012,9 @@ impl AuthManager {
auth: &ChatgptAuth,
refresh_token: String,
) -> Result<(), RefreshTokenError> {
let refresh_response = request_chatgpt_token_refresh(refresh_token, auth.client()).await?;
let refresh_response =
request_chatgpt_token_refresh(refresh_token, self.outbound_proxy_config.as_ref())
.await?;
persist_tokens(
auth.storage(),

View File

@@ -18,7 +18,7 @@ use super::manager::REVOKE_TOKEN_URL;
use super::manager::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
use super::storage::AuthDotJson;
use super::util::try_parse_error_message;
use crate::default_client::create_client_with_proxy_config;
use crate::default_client::create_client_for_auth_route;
use crate::token_data::TokenData;
const REVOKE_HTTP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -61,8 +61,8 @@ pub(crate) async fn revoke_auth_tokens_with_proxy_config(
return Ok(());
};
let client = create_client_with_proxy_config(outbound_proxy_config);
let endpoint = revoke_token_endpoint();
let client = create_client_for_auth_route(&endpoint, outbound_proxy_config)?;
revoke_oauth_token(&client, endpoint.as_str(), token, kind, REVOKE_HTTP_TIMEOUT).await
}