mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Route auth proxy resolution by actual request URL
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user