mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Harden MCP OAuth callback handling (#40691)
## Why MCP servers can share an OAuth callback URL. Without a validated issuer or a server-specific callback path, an authorization response could be associated with the wrong server. ## What changed - Use stable callbacks when authorization metadata advertises issuer-bound responses, and validate the returned issuer before exchanging the code. - Retain server-specific callback IDs for providers without issuer support, including fallback to the global or default callback for legacy registered clients. - Persist registered callback URLs for MCP servers and plugins, and insert the active listener port into portless loopback redirects. ## Testing Add coverage for issuer validation, callback-mode discovery, registered and legacy clients, plugin OAuth, CLI persistence, and loopback listener ports. GitOrigin-RevId: 2878c92e237fc17fd3def0bd2e1cce3e104a3db8
This commit is contained in:
1 parent
32ce703a80
commit
9be8d6e1c3
28 files changed
+1096
-127
No files matched your search
@@ -342,10 +342,13 @@ async fn add_streamable_http_with_custom_env_var() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn add_streamable_http_with_oauth_options() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let expected_callback = "http://127.0.0.1/callback/w9gKTtkB7gWy";
|
||||
|
||||
let mut add_cmd = codex_command(codex_home.path())?;
|
||||
add_cmd
|
||||
.args([
|
||||
"-c",
|
||||
"mcp_oauth_callback_port=43123",
|
||||
"mcp",
|
||||
"add",
|
||||
"oauth-server",
|
||||
@@ -357,7 +360,8 @@ async fn add_streamable_http_with_oauth_options() -> Result<()> {
|
||||
"https://resource.example.com",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
.success()
|
||||
.stdout(contains(format!("OAuth callback URL: {expected_callback}")));
|
||||
|
||||
let servers = load_global_mcp_servers(codex_home.path()).await?;
|
||||
let oauth_server = servers
|
||||
@@ -367,6 +371,13 @@ async fn add_streamable_http_with_oauth_options() -> Result<()> {
|
||||
oauth_server.oauth_client_id(),
|
||||
Some("eci-prd-pub-codex-123")
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_server
|
||||
.oauth
|
||||
.as_ref()
|
||||
.and_then(|oauth| oauth.callback_url.as_deref()),
|
||||
Some(expected_callback)
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_server.oauth_resource.as_deref(),
|
||||
Some("https://resource.example.com")
|
||||
@@ -375,6 +386,63 @@ async fn add_streamable_http_with_oauth_options() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_persists_issuer_bound_callback_before_starting_oauth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let oauth_server = MockServer::start().await;
|
||||
let issuer = format!("{}/mcp", oauth_server.uri());
|
||||
let metadata = serde_json::json!({
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": "not-a-valid-authorization-url",
|
||||
"token_endpoint": format!("{}/token", oauth_server.uri()),
|
||||
"authorization_response_iss_parameter_supported": true,
|
||||
});
|
||||
let concurrent_codex_home = codex_home.path().to_path_buf();
|
||||
let concurrent_add = std::sync::Once::new();
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/oauth-authorization-server/mcp"))
|
||||
.respond_with(move |_: &wiremock::Request| {
|
||||
concurrent_add.call_once(|| {
|
||||
codex_command(&concurrent_codex_home)
|
||||
.expect("create concurrent MCP add command")
|
||||
.args(["mcp", "add", "concurrent", "--", "echo", "concurrent"])
|
||||
.assert()
|
||||
.success();
|
||||
});
|
||||
ResponseTemplate::new(200).set_body_json(metadata.clone())
|
||||
})
|
||||
.mount(&oauth_server)
|
||||
.await;
|
||||
|
||||
let mut add_cmd = codex_command(codex_home.path())?;
|
||||
add_cmd.args([
|
||||
"mcp",
|
||||
"add",
|
||||
"issuer-bound",
|
||||
"--url",
|
||||
&format!("{}/mcp", oauth_server.uri()),
|
||||
"--oauth-client-id",
|
||||
"registered-client",
|
||||
]);
|
||||
let output = tokio::task::spawn_blocking(move || add_cmd.output()).await??;
|
||||
|
||||
assert!(!output.status.success());
|
||||
assert!(
|
||||
String::from_utf8(output.stdout)?.contains("OAuth callback URL: http://127.0.0.1/callback")
|
||||
);
|
||||
let servers = load_global_mcp_servers(codex_home.path()).await?;
|
||||
assert!(servers.contains_key("concurrent"));
|
||||
assert_eq!(
|
||||
servers["issuer-bound"]
|
||||
.oauth
|
||||
.as_ref()
|
||||
.and_then(|oauth| oauth.callback_url.as_deref()),
|
||||
Some("http://127.0.0.1/callback")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_streamable_http_rejects_removed_flag() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
Reference in new issue
Block a user