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:
@@ -17,7 +17,7 @@ use super::ResolvedMcpCatalog;
|
||||
|
||||
fn server(url: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -43,6 +43,7 @@ use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
@@ -872,7 +873,7 @@ fn chatgpt_auth_provider_for_server(
|
||||
) -> Option<SharedAuthProvider> {
|
||||
if !server
|
||||
.configured_config()
|
||||
.is_some_and(|config| config.use_chatgpt_auth)
|
||||
.is_some_and(|config| matches!(&config.auth, McpServerAuth::ChatGpt))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1272,7 +1272,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
(
|
||||
"stdio".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1299,7 +1299,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
(
|
||||
"http".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1407,7 +1407,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
|
||||
let server_name = "github";
|
||||
let entry = McpAuthStatusEntry {
|
||||
config: Some(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://api.githubcopilot.com/mcp/".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1461,7 +1461,7 @@ fn mcp_init_error_display_reports_generic_errors() {
|
||||
let server_name = "custom";
|
||||
let entry = McpAuthStatusEntry {
|
||||
config: Some(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()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
@@ -141,7 +142,7 @@ where
|
||||
let config = server.configured_config().cloned();
|
||||
let has_runtime_auth = config
|
||||
.as_ref()
|
||||
.is_some_and(|config| config.use_chatgpt_auth)
|
||||
.is_some_and(|config| matches!(&config.auth, McpServerAuth::ChatGpt))
|
||||
&& auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
&& config.as_ref().is_some_and(|config| {
|
||||
matches!(
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::time::Duration;
|
||||
|
||||
use async_channel::unbounded;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AppToolApproval;
|
||||
@@ -26,6 +27,7 @@ use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_connectors::ConnectorSnapshot;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::CHATGPT_CODEX_BASE_URL;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
@@ -257,23 +259,28 @@ pub fn effective_mcp_servers_from_configured(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
let chatgpt_origin = url::Url::parse(&config.chatgpt_base_url)
|
||||
let chatgpt_origin = url::Url::parse(CHATGPT_CODEX_BASE_URL)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin());
|
||||
let mut servers = configured_servers
|
||||
.into_iter()
|
||||
.map(|(name, mut server)| {
|
||||
if server.use_chatgpt_auth {
|
||||
let server_origin = match &server.transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => url::Url::parse(url)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin()),
|
||||
McpServerTransportConfig::Stdio { .. } => None,
|
||||
};
|
||||
server.use_chatgpt_auth =
|
||||
server_origin.is_some() && server_origin.as_ref() == chatgpt_origin.as_ref();
|
||||
match server.auth.clone() {
|
||||
McpServerAuth::ChatGpt => {
|
||||
let server_origin = match &server.transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
||||
url::Url::parse(url)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin())
|
||||
}
|
||||
McpServerTransportConfig::Stdio { .. } => None,
|
||||
};
|
||||
if server_origin.as_ref() != chatgpt_origin.as_ref() {
|
||||
server.auth = McpServerAuth::OAuth;
|
||||
}
|
||||
}
|
||||
McpServerAuth::OAuth => {}
|
||||
}
|
||||
(name, EffectiveMcpServer::configured(server))
|
||||
})
|
||||
@@ -474,7 +481,7 @@ pub fn codex_apps_mcp_server_config(
|
||||
mcp_server_config_for_url(
|
||||
codex_apps_mcp_url_for_base_url(chatgpt_base_url),
|
||||
apps_mcp_product_sku,
|
||||
/*use_chatgpt_auth*/ true,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -492,14 +499,14 @@ pub fn hosted_plugin_runtime_mcp_server_config(
|
||||
mcp_server_config_for_url(
|
||||
format!("{base_url}/ps/mcp"),
|
||||
apps_mcp_product_sku,
|
||||
/*use_chatgpt_auth*/ true,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
|
||||
fn mcp_server_config_for_url(
|
||||
url: String,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
use_chatgpt_auth: bool,
|
||||
auth_mode: McpServerAuth,
|
||||
) -> McpServerConfig {
|
||||
let http_headers = apps_mcp_product_sku.map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())])
|
||||
@@ -512,7 +519,7 @@ fn mcp_server_config_for_url(
|
||||
http_headers,
|
||||
env_http_headers: None,
|
||||
},
|
||||
use_chatgpt_auth,
|
||||
auth: auth_mode,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
|
||||
@@ -296,7 +296,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
catalog.register(McpServerRegistration::from_config(
|
||||
"sample".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://user.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -322,7 +322,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
catalog.register(McpServerRegistration::from_config(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://docs.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -32,7 +32,7 @@ fn stdio_server(
|
||||
env_vars: Vec<McpServerEnvVar>,
|
||||
) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -67,7 +67,7 @@ fn declared_placement_preserves_local_plugin_normalization() {
|
||||
Vec::new(),
|
||||
);
|
||||
let expected_http = McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
|
||||
fn stdio_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -131,7 +131,7 @@ mod tests {
|
||||
|
||||
fn http_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
Reference in New Issue
Block a user