mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Represent MCP authentication with an enum (#29924)
## Why MCP authentication has distinct OAuth and ChatGPT-session flows. Representing that choice as `use_chatgpt_auth` makes one flow implicit and allows the configuration model to express the distinction only through a boolean. ChatGPT credential forwarding also needs a first-party trust boundary. A configurable `chatgpt_base_url` controls routing, but must not grant an MCP server permission to receive session credentials. This change builds on #29733, where the boolean was introduced. ## What changed - Replace `use_chatgpt_auth` with an `auth` field backed by the exhaustive `McpServerAuth` enum. - Support `auth = "oauth"` and `auth = "chatgpt"`, with OAuth remaining the default. - Trust only the origin derived from the existing hardcoded `CHATGPT_CODEX_BASE_URL` when granting ChatGPT auth to an MCP server. - Keep configured bearer tokens and authorization headers ahead of the selected authentication flow. - Update config writers, schema output, fixtures, and integration-test setup to use the enum. ## Verification Integration coverage exercises the complete streamable HTTP startup path in two independent configurations: - A directly constructed MCP configuration verifies that matching an overridden `chatgpt_base_url` does not grant ChatGPT auth. - A persisted `config.toml` containing an attacker-controlled `chatgpt_base_url` and `auth = "chatgpt"` verifies the same boundary through normal config parsing. Both tests complete MCP initialization and tool listing and assert that the full captured request sequence contains no authorization headers. Separate integration coverage verifies that configured authorization takes precedence over ChatGPT auth.
This commit is contained in:
@@ -1384,6 +1384,25 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerAuth": {
|
||||
"description": "Authentication flow Codex attempts after resolving an HTTP MCP server's configured bearer token and authorization headers, which always take precedence. ChatGPT authentication falls back to stored OAuth credentials when its session provider is unavailable; both modes ultimately fall back to an unauthenticated connection.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Use stored MCP OAuth credentials when available. Starting an OAuth login is a separate operation.",
|
||||
"enum": [
|
||||
"oauth"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Use the current ChatGPT session for servers on the trusted first-party ChatGPT origin. If no ChatGPT session provider is available, startup can still fall back to stored OAuth credentials.",
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpServerEnvVar": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2427,6 +2446,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"auth": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerAuth"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"bearer_token_env_var": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2555,10 +2582,6 @@
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_chatgpt_auth": {
|
||||
"default": null,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -114,7 +114,7 @@ use tempfile::TempDir;
|
||||
|
||||
fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -141,7 +141,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
|
||||
fn http_mcp(url: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -5568,7 +5568,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: vec!["hello".to_string()],
|
||||
@@ -5926,7 +5926,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: vec!["--verbose".to_string()],
|
||||
@@ -6006,7 +6006,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6062,7 +6062,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6121,7 +6121,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6177,7 +6177,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6245,7 +6245,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6328,7 +6328,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
let mut servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6367,7 +6367,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -6435,7 +6435,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6464,7 +6464,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
(
|
||||
"logs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "logs-server".to_string(),
|
||||
args: vec!["--follow".to_string()],
|
||||
@@ -6553,7 +6553,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6604,7 +6604,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6655,7 +6655,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6711,7 +6711,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
@@ -95,8 +96,8 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
||||
}
|
||||
}
|
||||
|
||||
if config.use_chatgpt_auth {
|
||||
entry["use_chatgpt_auth"] = value(true);
|
||||
if matches!(&config.auth, McpServerAuth::ChatGpt) {
|
||||
entry["auth"] = value("chatgpt");
|
||||
}
|
||||
if !config.enabled {
|
||||
entry["enabled"] = value(false);
|
||||
|
||||
@@ -873,7 +873,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
servers.insert(
|
||||
"stdio".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: vec!["--flag".to_string()],
|
||||
@@ -908,7 +908,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
servers.insert(
|
||||
"http".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com".to_string(),
|
||||
bearer_token_env_var: Some("TOKEN".to_string()),
|
||||
@@ -983,7 +983,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1044,7 +1044,7 @@ foo = { command = "cmd" }
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1095,7 +1095,7 @@ foo = { command = "cmd" } # keep me
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1145,7 +1145,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1196,7 +1196,7 @@ foo = { command = "cmd" }
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -357,7 +357,7 @@ fn mcp_dependency_to_server_config(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing url for streamable_http dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.clone(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -387,7 +387,7 @@ fn mcp_dependency_to_server_config(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing command for stdio dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.clone(),
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -395,7 +395,7 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -184,7 +184,7 @@ fn insert_rmcp_test_server(config: &mut Config, command: String, approval_mode:
|
||||
servers.insert(
|
||||
RMCP_SERVER.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn refresh_shuts_down_superseded_mcp_stdio_server() -> anyhow::Result<()>
|
||||
servers.insert(
|
||||
"refresh_cleanup".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
@@ -268,7 +269,7 @@ fn copy_binary_to_remote_env(
|
||||
|
||||
struct TestMcpServerOptions {
|
||||
environment_id: String,
|
||||
use_chatgpt_auth: bool,
|
||||
auth: McpServerAuth,
|
||||
supports_parallel_tool_calls: bool,
|
||||
tool_timeout_sec: Option<Duration>,
|
||||
}
|
||||
@@ -277,7 +278,7 @@ impl Default for TestMcpServerOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
use_chatgpt_auth: false,
|
||||
auth: McpServerAuth::default(),
|
||||
supports_parallel_tool_calls: false,
|
||||
tool_timeout_sec: None,
|
||||
}
|
||||
@@ -318,7 +319,7 @@ fn insert_mcp_server(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
transport,
|
||||
use_chatgpt_auth: options.use_chatgpt_auth,
|
||||
auth: options.auth,
|
||||
environment_id: options.environment_id,
|
||||
enabled: true,
|
||||
required: false,
|
||||
@@ -1231,7 +1232,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
supports_parallel_tool_calls: true,
|
||||
tool_timeout_sec: Some(Duration::from_secs(2)),
|
||||
},
|
||||
@@ -2304,62 +2305,20 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> anyhow::Result<()> {
|
||||
async fn streamable_http_configured_auth_precedes_chatgpt_auth() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let Some(chatgpt_auth_server) =
|
||||
start_streamable_http_test_server("chatgpt-auth", Some("Access Token")).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let chatgpt_auth_url = chatgpt_auth_server.url().to_string();
|
||||
let chatgpt_base_url = chatgpt_auth_url
|
||||
.strip_suffix("/mcp")
|
||||
.expect("test MCP URL should end in /mcp")
|
||||
.to_string();
|
||||
|
||||
let chatgpt_auth_fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = chatgpt_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"chatgpt_auth",
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url: chatgpt_auth_url,
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
.build_with_remote_env(&server)
|
||||
.await?;
|
||||
wait_for_mcp_server(&chatgpt_auth_fixture.codex, "chatgpt_auth").await?;
|
||||
drop(chatgpt_auth_fixture);
|
||||
chatgpt_auth_server.shutdown().await;
|
||||
|
||||
let Some(configured_auth_server) =
|
||||
start_streamable_http_test_server("configured-auth", Some("configured-token")).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let configured_auth_url = configured_auth_server.url().to_string();
|
||||
let configured_auth_base_url = configured_auth_url
|
||||
.strip_suffix("/mcp")
|
||||
.expect("test MCP URL should end in /mcp")
|
||||
.to_string();
|
||||
|
||||
let configured_auth_fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = configured_auth_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"configured_auth",
|
||||
@@ -2374,12 +2333,12 @@ async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> any
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: true,
|
||||
auth: McpServerAuth::ChatGpt,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
.build_with_remote_env(&server)
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
|
||||
wait_for_mcp_server(&configured_auth_fixture.codex, "configured_auth").await?;
|
||||
@@ -2390,19 +2349,19 @@ async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> any
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow::Result<()> {
|
||||
async fn streamable_http_chatgpt_auth_is_not_sent_to_configured_origin() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let untrusted_server = MockServer::start().await;
|
||||
let untrusted_apps = AppsTestServer::mount(&untrusted_server).await?;
|
||||
let untrusted_mcp_url = format!("{}/api/codex/apps", untrusted_apps.chatgpt_base_url);
|
||||
let trusted_chatgpt_base_url = server.uri();
|
||||
let untrusted_chatgpt_base_url = untrusted_apps.chatgpt_base_url;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = trusted_chatgpt_base_url;
|
||||
config.chatgpt_base_url = untrusted_chatgpt_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"untrusted_origin",
|
||||
@@ -2413,7 +2372,7 @@ async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow:
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
use_chatgpt_auth: true,
|
||||
auth: McpServerAuth::ChatGpt,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -2452,6 +2411,67 @@ async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn configured_chatgpt_base_url_does_not_grant_mcp_chatgpt_auth() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let untrusted_server = MockServer::start().await;
|
||||
let untrusted_apps = AppsTestServer::mount(&untrusted_server).await?;
|
||||
let untrusted_mcp_url = format!("{}/api/codex/apps", untrusted_apps.chatgpt_base_url);
|
||||
let untrusted_chatgpt_base_url = untrusted_apps.chatgpt_base_url;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_pre_build_hook(move |codex_home| {
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{untrusted_chatgpt_base_url}"
|
||||
|
||||
[mcp_servers.untrusted_origin]
|
||||
url = "{untrusted_mcp_url}"
|
||||
auth = "chatgpt"
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.expect("write attacker-controlled MCP config");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
wait_for_mcp_server(&fixture.codex, "untrusted_origin").await?;
|
||||
let observed_requests = untrusted_server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should capture MCP startup requests")
|
||||
.into_iter()
|
||||
.filter(|request| request.url.path() == "/api/codex/apps")
|
||||
.filter_map(|request| {
|
||||
let body: Value = serde_json::from_slice(&request.body).ok()?;
|
||||
let method = body.get("method")?.as_str()?.to_string();
|
||||
let authorization = request
|
||||
.headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_string);
|
||||
Some((method, authorization))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
observed_requests,
|
||||
vec![
|
||||
("initialize".to_string(), None),
|
||||
("notifications/initialized".to_string(), None),
|
||||
("tools/list".to_string(), None),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This test writes to a fallback credentials file in CODEX_HOME.
|
||||
/// Ideally, we wouldn't need to serialize the test but it's much more cumbersome to wire CODEX_HOME through the code.
|
||||
#[test]
|
||||
|
||||
@@ -1090,7 +1090,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -1217,7 +1217,7 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result<
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -1366,7 +1366,7 @@ async fn tool_search_uses_non_app_mcp_server_instructions_as_namespace_descripti
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -657,7 +657,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -276,7 +276,7 @@ async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> {
|
||||
servers.insert(
|
||||
"notes".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -383,7 +383,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
codex_config::types::McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: codex_config::types::McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -482,7 +482,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -775,7 +775,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
codex_config::types::McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: codex_config::types::McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user