mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## What changed Allow omitting `gateway_oauth.delivery` to send the gateway token as `Authorization: Bearer …`. Custom headers and cookies remain available when a provider also needs its own credentials. Reject conflicting provider authentication and duplicate delivery headers with actionable errors. For gateway bearer delivery, require `model_catalog_url` to share the scheme, host, and port of `base_url`. Consistently reject request header collisions across auth application methods. ## Testing Add coverage for omitted delivery, provider credential conflicts, and model catalog origin validation. Update inference tests to verify gateway-only authentication while signed in and combined ChatGPT and gateway credentials in separate headers. GitOrigin-RevId: 8564d3998511b3f6c55f3b65a01ed9056a90417e
301 lines
9.3 KiB
Rust
301 lines
9.3 KiB
Rust
//! Exercises combined credentials and fail-closed gateway setup on the inference path.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Result;
|
|
use codex_core::TurnInputRequest;
|
|
use codex_core::config::ConfigBuilder;
|
|
use codex_login::AuthManager;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::auth::AgentIdentityAuthPolicy;
|
|
use codex_model_provider::AgentIdentitySessionFallback;
|
|
use codex_model_provider::ProviderAuthScope;
|
|
use codex_model_provider::create_model_provider;
|
|
use codex_model_provider::test_support::seed_gateway_auth;
|
|
use codex_model_provider_info::ModelProviderInfo;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::user_input::UserInput;
|
|
use core_test_support::responses;
|
|
use core_test_support::test_codex::TestCodex;
|
|
use core_test_support::test_codex::test_codex;
|
|
use core_test_support::wait_for_event;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
#[tokio::test]
|
|
async fn inference_sends_chatgpt_and_gateway_credentials_to_the_configured_headers() -> Result<()> {
|
|
sample_with_gateway(
|
|
"requires_openai_auth = true",
|
|
"delivery = { kind = 'header', name = 'x-gateway-auth' }",
|
|
CodexAuth::from_external_chatgpt_tokens(
|
|
"header.e30.sig",
|
|
"test-account",
|
|
/*chatgpt_plan_type*/ None,
|
|
)?,
|
|
&[
|
|
("authorization", "Bearer header.e30.sig"),
|
|
("x-gateway-auth", "Bearer gateway-token"),
|
|
("chatgpt-account-id", "test-account"),
|
|
],
|
|
/*expected_error*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn gateway_token_failure_blocks_sampling() -> Result<()> {
|
|
sample_with_gateway(
|
|
"requires_openai_auth = true",
|
|
"delivery = { kind = 'header', name = 'x-gateway-auth' }",
|
|
CodexAuth::from_api_key("provider-token"),
|
|
&[],
|
|
Some("Gateway OAuth authentication failed; check the gateway configuration and credential store."),
|
|
).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn omitted_delivery_sends_only_gateway_auth_even_when_signed_in() -> Result<()> {
|
|
sample_with_gateway(
|
|
"requires_openai_auth = false",
|
|
"",
|
|
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
|
&[("authorization", "Bearer gateway-token")],
|
|
/*expected_error*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn sample_with_gateway(
|
|
provider_settings: &str,
|
|
gateway_settings: &str,
|
|
primary_auth: CodexAuth,
|
|
expected_headers: &[(&str, &str)],
|
|
expected_error: Option<&str>,
|
|
) -> Result<()> {
|
|
let server = MockServer::start().await;
|
|
let response = if expected_error.is_none() {
|
|
Some(
|
|
responses::mount_sse_once(
|
|
&server,
|
|
responses::sse(vec![
|
|
responses::ev_response_created("gateway-response"),
|
|
responses::ev_completed("gateway-response"),
|
|
]),
|
|
)
|
|
.await,
|
|
)
|
|
} else {
|
|
Mock::given(method("POST"))
|
|
.and(path("/token"))
|
|
.respond_with(
|
|
ResponseTemplate::new(500).set_body_json(json!({"error": "server_error"})),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
None
|
|
};
|
|
let expires_at = if expected_error.is_some() {
|
|
0
|
|
} else {
|
|
i64::MAX
|
|
};
|
|
let test = gateway_codex(
|
|
&server,
|
|
provider_settings,
|
|
gateway_settings,
|
|
primary_auth,
|
|
expires_at,
|
|
)
|
|
.await?;
|
|
assert_eq!(run_gateway_turn(&test).await?.as_deref(), expected_error);
|
|
if let Some(response) = response {
|
|
let request = response.single_request();
|
|
for name in [
|
|
"authorization",
|
|
"x-gateway-auth",
|
|
"cookie",
|
|
"chatgpt-account-id",
|
|
] {
|
|
let expected = expected_headers
|
|
.iter()
|
|
.find(|(key, _)| *key == name)
|
|
.map(|(_, value)| *value);
|
|
assert_eq!(
|
|
request.header(name).as_deref(),
|
|
expected,
|
|
"{gateway_settings}: {name}"
|
|
);
|
|
}
|
|
} else {
|
|
let requests = server
|
|
.received_requests()
|
|
.await
|
|
.expect("recorded gateway requests");
|
|
assert!(!requests.is_empty());
|
|
assert!(
|
|
requests
|
|
.iter()
|
|
.all(|request| request.url.path() == "/token")
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn gateway_codex(
|
|
server: &MockServer,
|
|
provider_settings: &str,
|
|
gateway_settings: &str,
|
|
primary_auth: CodexAuth,
|
|
expires_at: i64,
|
|
) -> Result<TestCodex> {
|
|
let home = Arc::new(TempDir::new()?);
|
|
let provider: ModelProviderInfo = toml::from_str(&format!(
|
|
r#"
|
|
name = "gateway"
|
|
base_url = "{base}/v1"
|
|
{provider_settings}
|
|
[gateway_oauth]
|
|
authorization_url = "{base}/authorize"
|
|
token_url = "{base}/token"
|
|
client_id = "client"
|
|
{gateway_settings}
|
|
"#,
|
|
base = server.uri()
|
|
))?;
|
|
let primary = AuthManager::from_auth_for_testing_with_home(
|
|
primary_auth.clone(),
|
|
home.path().to_path_buf(),
|
|
);
|
|
let _gateway = seed_gateway_auth(
|
|
&provider,
|
|
&primary,
|
|
json!({"access_token": "gateway-token", "refresh_token": "refresh-token", "expires_at": expires_at}),
|
|
);
|
|
test_codex()
|
|
.with_home(home)
|
|
.with_auth(primary_auth)
|
|
.with_config(move |config| {
|
|
config.model_provider = provider;
|
|
})
|
|
.build_with_auto_env(server)
|
|
.await
|
|
}
|
|
|
|
async fn run_gateway_turn(test: &TestCodex) -> Result<Option<String>> {
|
|
test.codex
|
|
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
|
text: "hello".into(),
|
|
text_elements: vec![],
|
|
}]))
|
|
.await?;
|
|
let mut error_message = None;
|
|
wait_for_event(&test.codex, |event| {
|
|
if let EventMsg::Error(error) = event {
|
|
error_message = Some(error.message.clone());
|
|
}
|
|
matches!(event, EventMsg::TurnComplete(_))
|
|
})
|
|
.await;
|
|
Ok(error_message)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn configured_gateway_http_initialization_fails_closed() -> Result<()> {
|
|
const CHILD_ENV: &str = "CODEX_TEST_GATEWAY_INVALID_CA_CHILD";
|
|
if std::env::var_os(CHILD_ENV).is_none() {
|
|
let home = TempDir::new()?;
|
|
let invalid_ca = home.path().join("invalid-ca.pem");
|
|
std::fs::write(&invalid_ca, "not a PEM certificate")?;
|
|
let output = std::process::Command::new(std::env::current_exe()?)
|
|
.arg("--exact")
|
|
.arg("suite::gateway_auth::configured_gateway_http_initialization_fails_closed")
|
|
.arg("--nocapture")
|
|
.env(CHILD_ENV, "1")
|
|
.env("CODEX_CA_CERTIFICATE", invalid_ca)
|
|
.output()?;
|
|
assert!(
|
|
output.status.success(),
|
|
"gateway setup subprocess failed: {}\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let home = TempDir::new()?;
|
|
// An invalid custom CA must not introduce a gateway dependency when none is configured.
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.build()
|
|
.await?;
|
|
let primary =
|
|
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
|
create_model_provider(config.model_provider, Some(primary))
|
|
.api_auth()
|
|
.await?;
|
|
std::fs::write(
|
|
home.path().join("config.toml"),
|
|
r#"
|
|
model_provider = "gateway"
|
|
[features]
|
|
respect_system_proxy = true
|
|
[model_providers.gateway]
|
|
name = "Gateway"
|
|
base_url = "http://127.0.0.1:9/v1"
|
|
[model_providers.gateway.gateway_oauth]
|
|
authorization_url = "http://127.0.0.1:9/authorize"
|
|
token_url = "http://127.0.0.1:9/token"
|
|
client_id = "client"
|
|
delivery = { kind = "header", name = "x-gateway-auth" }
|
|
"#,
|
|
)?;
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.build()
|
|
.await?;
|
|
let primary =
|
|
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
|
let provider = create_model_provider(config.model_provider.clone(), Some(primary));
|
|
let error = provider.api_auth().await.err().unwrap();
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"failed to create provider OAuth HTTP client"
|
|
);
|
|
let error = provider
|
|
.api_auth_for_scope(ProviderAuthScope {
|
|
agent_identity_policy: AgentIdentityAuthPolicy::JwtOnly,
|
|
session_source: SessionSource::Cli,
|
|
agent_identity_session_fallback: AgentIdentitySessionFallback::default(),
|
|
})
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"failed to create provider OAuth HTTP client"
|
|
);
|
|
let contents = std::fs::read_to_string(home.path().join("config.toml"))?;
|
|
std::fs::write(
|
|
home.path().join("config.toml"),
|
|
contents.replace("client_id = \"client\"", "client_id = \"\""),
|
|
)?;
|
|
let error = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.build()
|
|
.await
|
|
.unwrap_err();
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("gateway_oauth requires a nonempty client_id")
|
|
);
|
|
Ok(())
|
|
}
|