Files
codex/codex-rs/rmcp-client/src/oauth_http_client.rs
T
jif b215961a56 Support OAuth for HTTP MCP servers from selected executor plugins (#28529)
## Why

#28522 routes selected-plugin HTTP MCP traffic through the owning
executor, but OAuth bootstrap and refresh still used host-local clients.
Executor-only servers therefore cannot complete discovery or login
through the same network boundary as the MCP connection.

## What changed

- adapt `codex_exec_server::HttpClient` to RMCP 1.8's `OAuthHttpClient`
contract
- let RMCP own discovery, dynamic registration, PKCE, token exchange,
and refresh
- route auth status, persisted-token startup, and app-server login
through the server runtime while preserving the existing local discovery
path
- add optional `threadId` to `mcpServer/oauth/login` and echo it in the
completion notification
- implement RMCP's redirect policy and 1 MiB OAuth response limit over
executor HTTP
- cover selected-thread OAuth discovery and login through an
executor-only route

Depends on #28522.
2026-06-25 10:31:17 +01:00

117 lines
4.2 KiB
Rust

use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_exec_server::HttpClient;
use codex_exec_server::HttpHeader;
use codex_exec_server::HttpRedirectPolicy;
use codex_exec_server::HttpRequestParams;
use oauth2::HttpRequest;
use oauth2::HttpResponse;
use reqwest::header::HeaderMap;
use rmcp::transport::auth::OAuthHttpClient;
use rmcp::transport::auth::OAuthHttpClientError;
use rmcp::transport::auth::OAuthHttpClientFuture;
use rmcp::transport::auth::OAuthHttpRedirectPolicy;
use rmcp::transport::auth::OAuthHttpRequest;
const MAX_OAUTH_HTTP_RESPONSE_BODY_BYTES: usize = 1024 * 1024;
static NEXT_OAUTH_REQUEST_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Clone)]
pub(crate) struct OAuthHttpClientAdapter {
http_client: Arc<dyn HttpClient>,
default_headers: HeaderMap,
}
impl OAuthHttpClientAdapter {
pub(crate) fn new(http_client: Arc<dyn HttpClient>, default_headers: HeaderMap) -> Self {
Self {
http_client,
default_headers,
}
}
async fn execute_request(
&self,
request: HttpRequest,
redirect_policy: OAuthHttpRedirectPolicy,
timeout: Option<Duration>,
) -> Result<HttpResponse, OAuthHttpClientError> {
let redirect_policy = match redirect_policy {
OAuthHttpRedirectPolicy::Follow => HttpRedirectPolicy::Follow,
OAuthHttpRedirectPolicy::Stop => HttpRedirectPolicy::Stop,
_ => {
return Err(OAuthHttpClientError::new(
"unsupported OAuth HTTP redirect policy",
));
}
};
let (parts, body) = request.into_parts();
let mut headers = self.default_headers.clone();
for name in parts.headers.keys() {
headers.remove(name);
}
headers.extend(parts.headers);
let headers = headers
.iter()
.map(|(name, value)| {
Ok(HttpHeader {
name: name.as_str().to_string(),
value: value
.to_str()
.map_err(|error| OAuthHttpClientError::new(error.to_string()))?
.to_string(),
})
})
.collect::<Result<Vec<_>, OAuthHttpClientError>>()?;
let timeout_ms = timeout.map(|timeout| {
u64::try_from(timeout.as_millis())
.unwrap_or(u64::MAX)
.max(1)
});
let request_id = NEXT_OAUTH_REQUEST_ID.fetch_add(1, Ordering::Relaxed);
let (response, mut body_stream) = self
.http_client
.http_request_stream(HttpRequestParams {
method: parts.method.to_string(),
url: parts.uri.to_string(),
headers,
body: (!body.is_empty()).then_some(body.into()),
timeout_ms,
redirect_policy,
request_id: format!("oauth-request-{request_id}"),
stream_response: true,
})
.await
.map_err(|error| OAuthHttpClientError::new(error.to_string()))?;
let mut body = Vec::new();
while let Some(chunk) = body_stream
.recv()
.await
.map_err(|error| OAuthHttpClientError::new(error.to_string()))?
{
if chunk.len() > MAX_OAUTH_HTTP_RESPONSE_BODY_BYTES - body.len() {
return Err(OAuthHttpClientError::new(format!(
"OAuth HTTP response body exceeds {MAX_OAUTH_HTTP_RESPONSE_BODY_BYTES} bytes"
)));
}
body.extend_from_slice(&chunk);
}
let mut builder = oauth2::http::Response::builder().status(response.status);
for header in response.headers {
builder = builder.header(header.name, header.value);
}
builder
.body(body)
.map_err(|error| OAuthHttpClientError::new(error.to_string()))
}
}
impl OAuthHttpClient for OAuthHttpClientAdapter {
fn execute(&self, request: OAuthHttpRequest) -> OAuthHttpClientFuture<'_> {
Box::pin(self.execute_request(request.request, request.redirect_policy, request.timeout))
}
}