Support confidential OAuth clients for enterprise-managed MCP auth (#56001)

## Why

Enterprise-managed MCP authentication previously required public OAuth clients at both the identity provider and the MCP authorization server, preventing use of registrations that require client secrets.

## What changed

- Accept separate `client_secret` and `token_endpoint_auth_method` settings for the enterprise IdP and MCP server OAuth registration. Support `client_secret_basic` and `client_secret_post`, defaulting to Basic when a secret is configured.
- Validate the registered method against server metadata and use it for enterprise login and both ID-JAG exchanges.
- Require a configured `oauth.authorization_server_issuer` for confidential MCP registrations and reject empty secrets or authentication methods without secrets.
- Require the account OAuth client to match the configured IdP client before reusing an account grant with a provisioned secret; otherwise use dedicated enterprise login.

## Testing

Extend configuration, login, exchange, and app-server lifecycle tests to cover confidential registrations, separate IdP and resource credentials, authentication method validation, and exclusion of client secrets from authorization URLs, stored login grants, and logs.

GitOrigin-RevId: b3be378c2b1bb67e6a3652c648e72fd2e21e956d
This commit is contained in:
jif
2026-10-21 22:30:35 +00:00
committed by copyberry
parent f496b09bd1
commit 6203846fb3
25 changed files with 627 additions and 90 deletions
@@ -169,6 +169,9 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> anyhow::Result<TomlTa
if let Some(client_secret) = &oauth.client_secret {
oauth_table["client_secret"] = value(client_secret.as_str());
}
if let Some(method) = oauth.token_endpoint_auth_method {
oauth_table["token_endpoint_auth_method"] = value(method.as_str());
}
if let Some(callback_url) = &oauth.callback_url {
oauth_table["callback_url"] = value(callback_url.clone());
}
+16 -5
View File
@@ -1054,11 +1054,20 @@ fn blocking_replace_mcp_servers_round_trips() {
let codex_home = tmp.path();
let mut servers = BTreeMap::new();
servers.insert("ema".to_string(), serde_json::from_value(serde_json::json!({
"url": "https://ema.example/mcp", "auth": "ema_auth", "scopes": ["tools"],
"oauth_resource": "https://ema.example",
"oauth": {"client_id": "resource-client", "authorization_server_issuer": "https://as.example"}
})).expect("EMA server"));
servers.insert(
"ema".to_string(),
serde_json::from_value(serde_json::json!({
"url": "https://ema.example/mcp", "auth": "ema_auth", "scopes": ["tools"],
"oauth_resource": "https://ema.example",
"oauth": {
"client_id": "resource-client",
"client_secret": "test-ema-secret",
"token_endpoint_auth_method": "client_secret_post",
"authorization_server_issuer": "https://as.example"
}
}))
.expect("EMA server"),
);
servers.insert(
"stdio".to_string(),
McpServerConfig {
@@ -1154,6 +1163,8 @@ oauth_resource = \"https://ema.example\"
[mcp_servers.ema.oauth]
client_id = \"resource-client\"
client_secret = \"test-ema-secret\"
token_endpoint_auth_method = \"client_secret_post\"
authorization_server_issuer = \"https://as.example\"
[mcp_servers.http]