mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Add per-login MCP OAuth client registration selection (#38052)
## What changed - Add `--oauth-client-registration` with `auto` and `dcr` choices to `codex mcp add` and `codex mcp login`. - Add the equivalent optional `clientRegistration` field to `mcpServer/oauth/login` and the generated protocol schemas. - Keep automatic dynamic client registration as the default, prefer a configured OAuth client ID when present, and apply the selection only to the current login without persisting it in MCP configuration. ## Testing - Cover dynamic registration and configured-client OAuth flows through the app server. - Verify the CLI does not persist the per-login registration choice. GitOrigin-RevId: 653a4a1054ff43c2c5d52ae863b6a96d21692a53
This commit is contained in:
@@ -1930,8 +1930,26 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthClientRegistration": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"dcr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerOauthLoginParams": {
|
||||
"properties": {
|
||||
"clientRegistration": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerOauthClientRegistration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Registration strategy for this login only; omission preserves automatic DCR."
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+18
@@ -13160,6 +13160,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthClientRegistration": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"dcr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerOauthLoginCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13192,6 +13199,17 @@
|
||||
"McpServerOauthLoginParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"clientRegistration": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/McpServerOauthClientRegistration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Registration strategy for this login only; omission preserves automatic DCR."
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+18
@@ -9497,6 +9497,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthClientRegistration": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"dcr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerOauthLoginCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -9529,6 +9536,17 @@
|
||||
"McpServerOauthLoginParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"clientRegistration": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerOauthClientRegistration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Registration strategy for this login only; omission preserves automatic DCR."
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpServerOauthClientRegistration": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"dcr"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"clientRegistration": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerOauthClientRegistration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Registration strategy for this login only; omission preserves automatic DCR."
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type McpServerOauthClientRegistration = "auto" | "dcr";
|
||||
+6
-1
@@ -1,5 +1,10 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration";
|
||||
|
||||
export type McpServerOauthLoginParams = { name: string, threadId?: string | null, scopes?: Array<string> | null, timeoutSecs?: bigint | null, };
|
||||
export type McpServerOauthLoginParams = { name: string, threadId?: string | null,
|
||||
/**
|
||||
* Registration strategy for this login only; omission preserves automatic DCR.
|
||||
*/
|
||||
clientRegistration?: McpServerOauthClientRegistration | null, scopes?: Array<string> | null, timeoutSecs?: bigint | null, };
|
||||
|
||||
@@ -255,6 +255,7 @@ export type { McpServerElicitationAction } from "./McpServerElicitationAction";
|
||||
export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams";
|
||||
export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse";
|
||||
export type { McpServerMigration } from "./McpServerMigration";
|
||||
export type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration";
|
||||
export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification";
|
||||
export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams";
|
||||
export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse";
|
||||
|
||||
@@ -2195,6 +2195,7 @@ mod tests {
|
||||
params: v2::McpServerOauthLoginParams {
|
||||
name: "server-a".to_string(),
|
||||
thread_id: None,
|
||||
client_registration: None,
|
||||
scopes: None,
|
||||
timeout_secs: None,
|
||||
},
|
||||
|
||||
@@ -196,6 +196,9 @@ pub struct McpServerOauthLoginParams {
|
||||
pub name: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub thread_id: Option<String>,
|
||||
/// Registration strategy for this login only; omission preserves automatic DCR.
|
||||
#[ts(optional = nullable)]
|
||||
pub client_registration: Option<McpServerOauthClientRegistration>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
@@ -204,6 +207,15 @@ pub struct McpServerOauthLoginParams {
|
||||
pub timeout_secs: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum McpServerOauthClientRegistration {
|
||||
#[default]
|
||||
Auto,
|
||||
Dcr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -268,7 +268,7 @@ Example with notification opt-out:
|
||||
- `skills/config/write` — write user-level skill config by name or absolute path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth. For remote installs, clients may include an optional `installAttemptId`; app-server forwards it unchanged as `install_attempt_id` in the backend POST body, while omission preserves the legacy empty-body request (**under development; do not call from production clients yet**).
|
||||
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `<plugin>@<marketplace>` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**).
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, optionally pass `clientRegistration` (`auto` or `dcr`) to select registration for this login only, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. Omitting `clientRegistration` preserves automatic DCR; the override is never persisted in server configuration.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, owning `pluginId` (`null` for servers not contributed by a plugin), plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported.
|
||||
|
||||
@@ -113,6 +113,7 @@ use codex_app_server_protocol::MarketplaceUpgradeParams;
|
||||
use codex_app_server_protocol::MarketplaceUpgradeResponse;
|
||||
use codex_app_server_protocol::McpResourceReadParams;
|
||||
use codex_app_server_protocol::McpResourceReadResponse;
|
||||
use codex_app_server_protocol::McpServerOauthClientRegistration;
|
||||
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
|
||||
use codex_app_server_protocol::McpServerOauthLoginParams;
|
||||
use codex_app_server_protocol::McpServerOauthLoginResponse;
|
||||
@@ -455,6 +456,7 @@ use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::protocol::strip_user_message_prefix;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_protocol::user_input::UserInput as CoreInputItem;
|
||||
use codex_rmcp_client::McpOAuthClientRegistration;
|
||||
use codex_rmcp_client::StreamableHttpRedirectMode;
|
||||
use codex_rmcp_client::perform_oauth_login_return_url;
|
||||
use codex_rollout::InitialHistory;
|
||||
|
||||
@@ -118,9 +118,14 @@ impl McpRequestProcessor {
|
||||
let McpServerOauthLoginParams {
|
||||
name,
|
||||
thread_id,
|
||||
client_registration,
|
||||
scopes,
|
||||
timeout_secs,
|
||||
} = params;
|
||||
let client_registration = match client_registration.unwrap_or_default() {
|
||||
McpServerOauthClientRegistration::Auto => McpOAuthClientRegistration::Auto,
|
||||
McpServerOauthClientRegistration::Dcr => McpOAuthClientRegistration::Dcr,
|
||||
};
|
||||
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let (mcp_config, runtime_context) = match thread_id.as_deref() {
|
||||
@@ -201,6 +206,7 @@ impl McpRequestProcessor {
|
||||
env_http_headers,
|
||||
&resolved_scopes.scopes,
|
||||
server.oauth_client_id(),
|
||||
client_registration,
|
||||
server.oauth_resource.as_deref(),
|
||||
timeout_secs,
|
||||
mcp_config.mcp_oauth_callback_port,
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_mcp::should_retry_without_scopes;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::auth::AuthMode as DomainAuthMode;
|
||||
use codex_rmcp_client::McpOAuthClientRegistration;
|
||||
use codex_rmcp_client::OAuthDiscoveryTimeout;
|
||||
use codex_rmcp_client::StreamableHttpRedirectMode;
|
||||
use codex_rmcp_client::perform_oauth_login_silent;
|
||||
@@ -1998,6 +1999,7 @@ impl PluginRequestProcessor {
|
||||
oauth_config.env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
oauth_client_id,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
server.oauth_resource.as_deref(),
|
||||
callback_port,
|
||||
callback_url.as_deref(),
|
||||
@@ -2017,6 +2019,7 @@ impl PluginRequestProcessor {
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
oauth_client_id,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
server.oauth_resource.as_deref(),
|
||||
callback_port,
|
||||
callback_url.as_deref(),
|
||||
|
||||
@@ -54,6 +54,7 @@ const EXECUTOR_HTTP_MCP_URL: &str = "http://executor-only.invalid/mcp";
|
||||
const HTTP_MCP_SERVER_NAME: &str = "executor_http";
|
||||
const MCP_SERVER_NAME: &str = "executor_demo";
|
||||
const OAUTH_MCP_SERVER_NAME: &str = "executor_oauth";
|
||||
const PRE_REGISTERED_OAUTH_MCP_SERVER_NAME: &str = "executor_oauth_preregistered";
|
||||
const EXECUTOR_OAUTH_MCP_URL: &str = "http://oauth-only.invalid/oauth-mcp";
|
||||
const HOST_OAUTH_ACCESS_TOKEN: &str = "host-access-token";
|
||||
const EXECUTOR_OAUTH_ACCESS_TOKEN: &str = "executor-access-token";
|
||||
@@ -98,10 +99,12 @@ async fn selected_executor_plugin_exposes_its_mcps_only_to_that_thread() -> Resu
|
||||
}
|
||||
},
|
||||
));
|
||||
let (registration_request_tx, mut registration_request_rx) = mpsc::unbounded_channel();
|
||||
let (token_request_tx, mut token_request_rx) = mpsc::unbounded_channel();
|
||||
let oauth_metadata = json!({
|
||||
"authorization_endpoint": "https://oauth-only.invalid/authorize",
|
||||
"token_endpoint": "http://oauth-only.invalid/token",
|
||||
"registration_endpoint": "http://oauth-only.invalid/register",
|
||||
"scopes_supported": ["read", "write"],
|
||||
"response_types_supported": ["code"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
@@ -114,6 +117,19 @@ async fn selected_executor_plugin_exposes_its_mcps_only_to_that_thread() -> Resu
|
||||
async move { Json(metadata) }
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/register",
|
||||
post(move |Json(request): Json<serde_json::Value>| {
|
||||
let registration_request_tx = registration_request_tx.clone();
|
||||
async move {
|
||||
let _ = registration_request_tx.send(request.clone());
|
||||
Json(json!({
|
||||
"client_id": "executor-dcr-client",
|
||||
"redirect_uris": request["redirect_uris"],
|
||||
}))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/token",
|
||||
post(move |body: Bytes| {
|
||||
@@ -205,7 +221,12 @@ HTTP_PROXY = {http_proxy}
|
||||
(OAUTH_MCP_SERVER_NAME): {
|
||||
"url": EXECUTOR_OAUTH_MCP_URL,
|
||||
"environment_id": "local",
|
||||
"oauth": {"clientId": "executor-oauth-client"},
|
||||
"startup_timeout_sec": 10,
|
||||
},
|
||||
(PRE_REGISTERED_OAUTH_MCP_SERVER_NAME): {
|
||||
"url": EXECUTOR_OAUTH_MCP_URL,
|
||||
"environment_id": "local",
|
||||
"oauth": {"clientId": "configured-client"},
|
||||
"startup_timeout_sec": 10,
|
||||
}
|
||||
}
|
||||
@@ -251,12 +272,63 @@ startup_timeout_sec = 10
|
||||
)
|
||||
.await??;
|
||||
|
||||
let request_id = app_server
|
||||
.send_raw_request(
|
||||
"mcpServer/oauth/login",
|
||||
Some(json!({
|
||||
"name": PRE_REGISTERED_OAUTH_MCP_SERVER_NAME,
|
||||
"threadId": selected_thread.clone(),
|
||||
"clientRegistration": "dcr",
|
||||
"timeoutSecs": 10,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let response: McpServerOauthLoginResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
|
||||
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
|
||||
let parameters = authorization_url
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.collect::<BTreeMap<String, String>>();
|
||||
assert_eq!(
|
||||
parameters.get("client_id").map(String::as_str),
|
||||
Some("configured-client")
|
||||
);
|
||||
assert!(
|
||||
registration_request_rx.try_recv().is_err(),
|
||||
"configured OAuth client must skip dynamic registration"
|
||||
);
|
||||
let mut callback_url = reqwest::Url::parse(¶meters["redirect_uri"])?;
|
||||
callback_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("code", "configured-test-code")
|
||||
.append_pair("state", ¶meters["state"]);
|
||||
reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.build()?
|
||||
.get(callback_url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let token_request = timeout(DEFAULT_READ_TIMEOUT, token_request_rx.recv())
|
||||
.await?
|
||||
.expect("configured client should exchange its authorization code");
|
||||
assert!(token_request.contains("client_id=configured-client"));
|
||||
let completed: McpServerOauthLoginCompletedNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
app_server.read_notification("mcpServer/oauthLogin/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(completed.name, PRE_REGISTERED_OAUTH_MCP_SERVER_NAME);
|
||||
assert!(completed.success);
|
||||
|
||||
let request_id = app_server
|
||||
.send_raw_request(
|
||||
"mcpServer/oauth/login",
|
||||
Some(json!({
|
||||
"name": OAUTH_MCP_SERVER_NAME,
|
||||
"threadId": selected_thread.clone(),
|
||||
"clientRegistration": "dcr",
|
||||
"timeoutSecs": 10,
|
||||
})),
|
||||
)
|
||||
@@ -268,12 +340,11 @@ startup_timeout_sec = 10
|
||||
.authorization_url
|
||||
.starts_with("https://oauth-only.invalid/authorize?")
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.authorization_url
|
||||
.contains("client_id=executor-oauth-client")
|
||||
);
|
||||
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
|
||||
let client_id = authorization_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "client_id").then(|| value.into_owned()));
|
||||
assert_eq!(client_id.as_deref(), Some("executor-dcr-client"));
|
||||
let state = authorization_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "state").then(|| value.into_owned()))
|
||||
@@ -282,6 +353,14 @@ startup_timeout_sec = 10
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "redirect_uri").then(|| value.into_owned()))
|
||||
.expect("authorization URL should include redirect_uri");
|
||||
let registration_request = timeout(DEFAULT_READ_TIMEOUT, registration_request_rx.recv())
|
||||
.await?
|
||||
.expect("executor registration endpoint should receive a request");
|
||||
assert_eq!(registration_request["client_name"], json!("Codex"));
|
||||
assert_eq!(
|
||||
registration_request["redirect_uris"],
|
||||
json!([redirect_uri.clone()])
|
||||
);
|
||||
let mut callback_url = reqwest::Url::parse(&redirect_uri)?;
|
||||
callback_url
|
||||
.query_pairs_mut()
|
||||
@@ -300,6 +379,7 @@ startup_timeout_sec = 10
|
||||
assert!(token_request.contains("grant_type=authorization_code"));
|
||||
assert!(token_request.contains("code=executor-test-code"));
|
||||
assert!(token_request.contains("code_verifier="));
|
||||
assert!(token_request.contains("client_id=executor-dcr-client"));
|
||||
let completed: McpServerOauthLoginCompletedNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
app_server.read_notification("mcpServer/oauthLogin/completed"),
|
||||
@@ -469,6 +549,10 @@ startup_timeout_sec = 10
|
||||
OAUTH_MCP_SERVER_NAME.to_string(),
|
||||
Some("executor-demo@1".to_string()),
|
||||
),
|
||||
(
|
||||
PRE_REGISTERED_OAUTH_MCP_SERVER_NAME.to_string(),
|
||||
Some("executor-demo@1".to_string()),
|
||||
),
|
||||
(REFRESH_PROBE_SERVER_NAME.to_string(), None),
|
||||
])
|
||||
);
|
||||
@@ -481,7 +565,10 @@ startup_timeout_sec = 10
|
||||
.map(|server| server.name)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(unselected_server_names.iter().all(|name| {
|
||||
name != MCP_SERVER_NAME && name != HTTP_MCP_SERVER_NAME && name != OAUTH_MCP_SERVER_NAME
|
||||
name != MCP_SERVER_NAME
|
||||
&& name != HTTP_MCP_SERVER_NAME
|
||||
&& name != OAUTH_MCP_SERVER_NAME
|
||||
&& name != PRE_REGISTERED_OAUTH_MCP_SERVER_NAME
|
||||
}));
|
||||
|
||||
http_server_handle.abort();
|
||||
|
||||
@@ -31,6 +31,7 @@ use codex_mcp::oauth_login_support;
|
||||
use codex_mcp::resolve_oauth_scopes;
|
||||
use codex_mcp::should_retry_without_scopes;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::McpOAuthClientRegistration;
|
||||
use codex_rmcp_client::OAuthDiscoveryTimeout;
|
||||
use codex_rmcp_client::StreamableHttpRedirectMode;
|
||||
use codex_rmcp_client::delete_oauth_tokens;
|
||||
@@ -151,11 +152,35 @@ pub struct AddMcpStreamableHttpArgs {
|
||||
#[arg(long = "oauth-client-id", value_name = "CLIENT_ID", requires = "url")]
|
||||
pub oauth_client_id: Option<String>,
|
||||
|
||||
/// OAuth client-registration strategy for the immediate login only.
|
||||
#[arg(
|
||||
long = "oauth-client-registration",
|
||||
value_enum,
|
||||
value_name = "AUTO|DCR",
|
||||
requires = "url"
|
||||
)]
|
||||
pub oauth_client_registration: Option<McpOAuthClientRegistrationArg>,
|
||||
|
||||
/// Optional OAuth resource parameter to include during MCP login.
|
||||
#[arg(long = "oauth-resource", value_name = "RESOURCE", requires = "url")]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum McpOAuthClientRegistrationArg {
|
||||
Auto,
|
||||
Dcr,
|
||||
}
|
||||
|
||||
impl From<McpOAuthClientRegistrationArg> for McpOAuthClientRegistration {
|
||||
fn from(value: McpOAuthClientRegistrationArg) -> Self {
|
||||
match value {
|
||||
McpOAuthClientRegistrationArg::Auto => Self::Auto,
|
||||
McpOAuthClientRegistrationArg::Dcr => Self::Dcr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub struct RemoveArgs {
|
||||
/// Name of the MCP server configuration to remove.
|
||||
@@ -170,6 +195,14 @@ pub struct LoginArgs {
|
||||
/// Comma-separated list of OAuth scopes to request.
|
||||
#[arg(long, value_delimiter = ',', value_name = "SCOPE,SCOPE")]
|
||||
pub scopes: Vec<String>,
|
||||
|
||||
/// OAuth client-registration strategy for this login only.
|
||||
#[arg(
|
||||
long = "oauth-client-registration",
|
||||
value_enum,
|
||||
value_name = "AUTO|DCR"
|
||||
)]
|
||||
pub oauth_client_registration: Option<McpOAuthClientRegistrationArg>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
@@ -235,6 +268,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
resolved_scopes: &ResolvedMcpOAuthScopes,
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
@@ -249,6 +283,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
callback_port,
|
||||
callback_url,
|
||||
@@ -268,6 +303,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
env_http_headers,
|
||||
&[],
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
callback_port,
|
||||
callback_url,
|
||||
@@ -316,7 +352,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
.await
|
||||
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
|
||||
|
||||
let (transport, oauth_client_id, oauth_resource) = match transport_args {
|
||||
let (transport, oauth_client_id, client_registration, oauth_resource) = match transport_args {
|
||||
AddMcpTransportArgs {
|
||||
stdio: Some(stdio), ..
|
||||
} => {
|
||||
@@ -340,6 +376,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
cwd: None,
|
||||
},
|
||||
None,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
None,
|
||||
)
|
||||
}
|
||||
@@ -349,6 +386,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
oauth_client_id,
|
||||
oauth_client_registration,
|
||||
oauth_resource,
|
||||
}),
|
||||
..
|
||||
@@ -360,6 +398,9 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
env_http_headers: None,
|
||||
},
|
||||
oauth_client_id,
|
||||
oauth_client_registration
|
||||
.map(McpOAuthClientRegistration::from)
|
||||
.unwrap_or_default(),
|
||||
oauth_resource,
|
||||
),
|
||||
AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"),
|
||||
@@ -427,6 +468,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
oauth_config.env_http_headers,
|
||||
&resolved_scopes,
|
||||
oauth_client_id.as_deref(),
|
||||
client_registration,
|
||||
oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
@@ -487,7 +529,14 @@ async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let LoginArgs { name, scopes } = login_args;
|
||||
let LoginArgs {
|
||||
name,
|
||||
scopes,
|
||||
oauth_client_registration,
|
||||
} = login_args;
|
||||
let client_registration = oauth_client_registration
|
||||
.map(McpOAuthClientRegistration::from)
|
||||
.unwrap_or_default();
|
||||
|
||||
let Some(server) = mcp_servers.get(&name) else {
|
||||
bail!("No MCP server named '{name}' found.");
|
||||
@@ -532,6 +581,7 @@ async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
env_http_headers,
|
||||
&resolved_scopes,
|
||||
server.oauth_client_id(),
|
||||
client_registration,
|
||||
server.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
|
||||
@@ -239,7 +239,15 @@ async fn add_streamable_http_without_manual_token() -> Result<()> {
|
||||
|
||||
let mut add_cmd = codex_command(codex_home.path())?;
|
||||
add_cmd
|
||||
.args(["mcp", "add", "github", "--url", "https://example.com/mcp"])
|
||||
.args([
|
||||
"mcp",
|
||||
"add",
|
||||
"github",
|
||||
"--url",
|
||||
"https://example.com/mcp",
|
||||
"--oauth-client-registration",
|
||||
"dcr",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
@@ -260,9 +268,13 @@ async fn add_streamable_http_without_manual_token() -> Result<()> {
|
||||
other => panic!("unexpected transport: {other:?}"),
|
||||
}
|
||||
assert!(github.enabled);
|
||||
assert_eq!(github.oauth, None);
|
||||
|
||||
assert!(!codex_home.path().join(".credentials.json").exists());
|
||||
assert!(!codex_home.path().join(".env").exists());
|
||||
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
assert!(!config.contains("client_registration"));
|
||||
assert!(!config.contains("[mcp_servers.github.oauth]"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::McpOAuthClientRegistration;
|
||||
use codex_rmcp_client::OAuthDiscoveryTimeout;
|
||||
use codex_rmcp_client::StreamableHttpRedirectMode;
|
||||
use codex_rmcp_client::perform_oauth_login;
|
||||
@@ -181,6 +182,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
oauth_config.env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
oauth_client_id,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
server_config.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
@@ -199,6 +201,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
oauth_client_id,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
server_config.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
|
||||
@@ -8,6 +8,7 @@ mod incoming_jsonrpc;
|
||||
mod local_stdio_transport;
|
||||
mod logging_client_handler;
|
||||
mod oauth;
|
||||
mod oauth_client_registration;
|
||||
mod oauth_http_client;
|
||||
mod perform_oauth_login;
|
||||
mod program_resolver;
|
||||
@@ -35,6 +36,7 @@ pub use oauth::delete_oauth_tokens;
|
||||
pub use oauth::save_oauth_tokens;
|
||||
pub use oauth::stored_oauth_credential_snapshot;
|
||||
pub use oauth::stored_oauth_credentials;
|
||||
pub use oauth_client_registration::McpOAuthClientRegistration;
|
||||
pub use perform_oauth_login::OAuthProviderError;
|
||||
pub use perform_oauth_login::OauthLoginHandle;
|
||||
pub use perform_oauth_login::perform_oauth_login;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rmcp::transport::AuthorizationManager;
|
||||
use rmcp::transport::AuthorizationRequest;
|
||||
use rmcp::transport::auth::OAuthHttpClient;
|
||||
use rmcp::transport::auth::OAuthState;
|
||||
|
||||
/// OAuth client-registration strategy for one interactive HTTP MCP login.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum McpOAuthClientRegistration {
|
||||
/// Preserve the current automatic Dynamic Client Registration flow.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Require the authorization server's Dynamic Client Registration endpoint.
|
||||
Dcr,
|
||||
}
|
||||
|
||||
pub(crate) async fn start_authorization(
|
||||
server_url: &str,
|
||||
http_client: Arc<dyn OAuthHttpClient>,
|
||||
scopes: &[&str],
|
||||
redirect_uri: &str,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
) -> Result<OAuthState> {
|
||||
let request = match client_registration {
|
||||
McpOAuthClientRegistration::Auto | McpOAuthClientRegistration::Dcr => {
|
||||
AuthorizationRequest::new(redirect_uri)
|
||||
.with_scopes(scopes.iter().copied())
|
||||
.with_client_name("Codex")
|
||||
}
|
||||
};
|
||||
|
||||
let mut auth_manager =
|
||||
AuthorizationManager::new_with_oauth_http_client(server_url, http_client).await?;
|
||||
auth_manager.set_allow_missing_issuer(true);
|
||||
let mut oauth_state = OAuthState::Unauthorized(auth_manager);
|
||||
oauth_state.start_authorization(request).await?;
|
||||
Ok(oauth_state)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_exec_server::HttpClient;
|
||||
use rmcp::transport::AuthorizationManager;
|
||||
use rmcp::transport::AuthorizationRequest;
|
||||
use rmcp::transport::AuthorizationSession;
|
||||
use rmcp::transport::auth::OAuthClientConfig;
|
||||
use rmcp::transport::auth::OAuthHttpClient;
|
||||
@@ -29,6 +28,8 @@ use crate::StoredOAuthTokens;
|
||||
use crate::WrappedOAuthTokenResponse;
|
||||
use crate::http_client_adapter::StreamableHttpRedirectMode;
|
||||
use crate::oauth::compute_expires_at_millis;
|
||||
use crate::oauth_client_registration::McpOAuthClientRegistration;
|
||||
use crate::oauth_client_registration::start_authorization as start_client_registration;
|
||||
use crate::oauth_http_client::OAuthHttpClientAdapter;
|
||||
use crate::save_oauth_tokens;
|
||||
use crate::utils::build_default_headers;
|
||||
@@ -92,6 +93,7 @@ pub async fn perform_oauth_login(
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
@@ -106,6 +108,7 @@ pub async fn perform_oauth_login(
|
||||
env_http_headers,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
callback_port,
|
||||
callback_url,
|
||||
@@ -126,6 +129,7 @@ pub async fn perform_oauth_login_silent(
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
@@ -141,6 +145,7 @@ pub async fn perform_oauth_login_silent(
|
||||
env_http_headers,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
callback_port,
|
||||
callback_url,
|
||||
@@ -161,6 +166,7 @@ async fn perform_oauth_login_with_browser_output(
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
@@ -182,6 +188,7 @@ async fn perform_oauth_login_with_browser_output(
|
||||
http_context,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
/*launch_browser*/ true,
|
||||
callback_port,
|
||||
@@ -203,6 +210,7 @@ pub async fn perform_oauth_login_return_url(
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
timeout_secs: Option<i64>,
|
||||
callback_port: Option<u16>,
|
||||
@@ -224,6 +232,7 @@ pub async fn perform_oauth_login_return_url(
|
||||
http_context,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
client_registration,
|
||||
oauth_resource,
|
||||
/*launch_browser*/ false,
|
||||
callback_port,
|
||||
@@ -492,6 +501,7 @@ impl OauthLoginFlow {
|
||||
http_context: OAuthHttpContext,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
client_registration: McpOAuthClientRegistration,
|
||||
oauth_resource: Option<&str>,
|
||||
launch_browser: bool,
|
||||
callback_port: Option<u16>,
|
||||
@@ -541,14 +551,27 @@ impl OauthLoginFlow {
|
||||
));
|
||||
|
||||
let scope_refs: Vec<&str> = scopes.iter().map(String::as_str).collect();
|
||||
let oauth_state = start_authorization(
|
||||
server_url,
|
||||
oauth_http_client,
|
||||
&scope_refs,
|
||||
&redirect_uri,
|
||||
oauth_client_id,
|
||||
)
|
||||
.await?;
|
||||
let oauth_state = if let Some(oauth_client_id) =
|
||||
oauth_client_id.filter(|client_id| !client_id.trim().is_empty())
|
||||
{
|
||||
start_authorization(
|
||||
server_url,
|
||||
oauth_http_client,
|
||||
&scope_refs,
|
||||
&redirect_uri,
|
||||
oauth_client_id,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
start_client_registration(
|
||||
server_url,
|
||||
oauth_http_client,
|
||||
&scope_refs,
|
||||
&redirect_uri,
|
||||
client_registration,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let auth_url = append_query_param(
|
||||
&oauth_state.get_authorization_url().await?,
|
||||
"resource",
|
||||
@@ -670,25 +693,12 @@ async fn start_authorization(
|
||||
http_client: Arc<dyn OAuthHttpClient>,
|
||||
scopes: &[&str],
|
||||
redirect_uri: &str,
|
||||
oauth_client_id: Option<&str>,
|
||||
oauth_client_id: &str,
|
||||
) -> Result<OAuthState> {
|
||||
let mut auth_manager =
|
||||
AuthorizationManager::new_with_oauth_http_client(server_url, http_client).await?;
|
||||
auth_manager.set_allow_missing_issuer(true);
|
||||
|
||||
let Some(oauth_client_id) = oauth_client_id.filter(|client_id| !client_id.trim().is_empty())
|
||||
else {
|
||||
let mut oauth_state = OAuthState::Unauthorized(auth_manager);
|
||||
oauth_state
|
||||
.start_authorization(
|
||||
AuthorizationRequest::new(redirect_uri)
|
||||
.with_scopes(scopes.iter().copied())
|
||||
.with_client_name("Codex"),
|
||||
)
|
||||
.await?;
|
||||
return Ok(oauth_state);
|
||||
};
|
||||
|
||||
let metadata = auth_manager.resolve_metadata().await?.metadata;
|
||||
auth_manager.set_metadata(metadata);
|
||||
auth_manager.configure_client(
|
||||
@@ -721,6 +731,7 @@ fn append_query_param(url: &str, key: &str, value: Option<&str>) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -747,6 +758,7 @@ mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::CallbackOutcome;
|
||||
use super::McpOAuthClientRegistration;
|
||||
use super::OAuthHttpClientAdapter;
|
||||
use super::OAuthProviderError;
|
||||
use super::StreamableHttpRedirectMode;
|
||||
@@ -790,7 +802,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_oauth_metadata_server() -> String {
|
||||
async fn spawn_oauth_metadata_server() -> (String, Arc<AtomicUsize>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind metadata listener");
|
||||
@@ -800,8 +812,11 @@ mod tests {
|
||||
"issuer": format!("{base_url}/mcp"),
|
||||
"authorization_endpoint": format!("{base_url}/oauth/authorize"),
|
||||
"token_endpoint": format!("{base_url}/oauth/token"),
|
||||
"scopes_supported": [""],
|
||||
"registration_endpoint": format!("{base_url}/oauth/register"),
|
||||
"scopes_supported": ["read", "offline_access"],
|
||||
});
|
||||
let registration_requests = Arc::new(AtomicUsize::new(0));
|
||||
let captured_registration_requests = Arc::clone(®istration_requests);
|
||||
let path_scoped_metadata = metadata.clone();
|
||||
let app = Router::new()
|
||||
.route(
|
||||
@@ -817,6 +832,16 @@ mod tests {
|
||||
let metadata = metadata.clone();
|
||||
async move { Json(metadata) }
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/oauth/register",
|
||||
post(move || {
|
||||
let registration_requests = Arc::clone(&captured_registration_requests);
|
||||
async move {
|
||||
registration_requests.fetch_add(1, Ordering::SeqCst);
|
||||
Json(json!({"client_id": "unexpected-dynamic-client"}))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -825,38 +850,50 @@ mod tests {
|
||||
.expect("serve oauth metadata");
|
||||
});
|
||||
|
||||
base_url
|
||||
(base_url, registration_requests)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_authorization_uses_configured_client_id() {
|
||||
let base_url = spawn_oauth_metadata_server().await;
|
||||
let oauth_state = start_authorization(
|
||||
&format!("{base_url}/mcp"),
|
||||
Arc::new(OAuthHttpClientAdapter::new(
|
||||
Arc::new(RouteAwareHttpClient::new(HttpClientFactory::new(
|
||||
OutboundProxyPolicy::ReqwestDefault,
|
||||
))),
|
||||
HeaderMap::new(),
|
||||
)),
|
||||
&[],
|
||||
"http://127.0.0.1/callback",
|
||||
Some("eci-prd-pub-codex-123"),
|
||||
)
|
||||
.await
|
||||
.expect("start oauth authorization");
|
||||
|
||||
let authorization_url = oauth_state
|
||||
.get_authorization_url()
|
||||
async fn configured_client_preserves_exact_scopes_and_redirect_without_registration() {
|
||||
for (scopes, expected_scope) in [(&[][..], None), (&["read"][..], Some("read"))] {
|
||||
let (base_url, registration_requests) = spawn_oauth_metadata_server().await;
|
||||
let redirect_uri = "http://127.0.0.1:43123/callback/configured-client";
|
||||
let oauth_state = start_authorization(
|
||||
&format!("{base_url}/mcp"),
|
||||
Arc::new(OAuthHttpClientAdapter::new(
|
||||
Arc::new(RouteAwareHttpClient::new(HttpClientFactory::new(
|
||||
OutboundProxyPolicy::ReqwestDefault,
|
||||
))),
|
||||
HeaderMap::new(),
|
||||
)),
|
||||
scopes,
|
||||
redirect_uri,
|
||||
"eci-prd-pub-codex-123",
|
||||
)
|
||||
.await
|
||||
.expect("read authorization url");
|
||||
let auth_url = Url::parse(&authorization_url).expect("authorization url should parse");
|
||||
let client_id = auth_url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "client_id")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
.expect("start pre-registered OAuth authorization");
|
||||
|
||||
assert_eq!(client_id.as_deref(), Some("eci-prd-pub-codex-123"));
|
||||
let authorization_url = oauth_state
|
||||
.get_authorization_url()
|
||||
.await
|
||||
.expect("read authorization URL");
|
||||
let query = Url::parse(&authorization_url)
|
||||
.expect("authorization URL should parse")
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
assert_eq!(
|
||||
query.get("client_id").map(String::as_str),
|
||||
Some("eci-prd-pub-codex-123")
|
||||
);
|
||||
assert_eq!(
|
||||
query.get("redirect_uri").map(String::as_str),
|
||||
Some(redirect_uri)
|
||||
);
|
||||
assert_eq!(query.get("scope").map(String::as_str), expected_scope);
|
||||
assert_eq!(registration_requests.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -925,7 +962,7 @@ mod tests {
|
||||
)),
|
||||
&[],
|
||||
"http://127.0.0.1/callback",
|
||||
Some("test-client"),
|
||||
"test-client",
|
||||
)
|
||||
.await
|
||||
.expect("start issuer-aware authorization");
|
||||
@@ -970,6 +1007,7 @@ mod tests {
|
||||
/*env_http_headers*/ None,
|
||||
&[],
|
||||
/*oauth_client_id*/ None,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
/*oauth_resource*/ None,
|
||||
/*callback_port*/ None,
|
||||
/*callback_url*/ None,
|
||||
@@ -993,6 +1031,7 @@ mod tests {
|
||||
/*env_http_headers*/ None,
|
||||
&[],
|
||||
/*oauth_client_id*/ None,
|
||||
McpOAuthClientRegistration::Auto,
|
||||
/*oauth_resource*/ None,
|
||||
/*callback_port*/ None,
|
||||
/*callback_url*/ None,
|
||||
|
||||
Reference in New Issue
Block a user