Files
codex/codex-rs/core/tests/suite/gateway_auth.rs
T
smurching-oai 9526f8d0d0 Default gateway OAuth delivery to Authorization bearer tokens (#57313)
## 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
2026-10-27 00:08:07 +00:00

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(())
}