docs: explain OAuth HTTP client setup (#918)

This commit is contained in:
Dale Seo 2026-06-25 19:59:47 -04:00 committed by GitHub
parent 77932141e3
commit f1daa9277e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 138 additions and 49 deletions

View file

@ -14,6 +14,7 @@ This document describes the OAuth 2.1 authorization implementation for Model Con
- Scope upgrade on 403 insufficient_scope (SEP-835)
- Automatic token refresh
- Authorized HTTP Client implementation
- Injectable OAuth HTTP client for custom network environments
## Usage Guide
@ -26,86 +27,162 @@ Enable the auth feature in Cargo.toml:
rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-reqwest"] }
```
### 2. Use OAuthState
### 2. Configure OAuth network requests
OAuth makes several HTTP requests before the MCP transport is connected:
protected-resource discovery, authorization-server discovery, dynamic client
registration, authorization-code exchange, token refresh, and client credentials
exchange. When no OAuth HTTP client is provided, the SDK sends those requests
with an internally-created `reqwest::Client`.
If you only need to customize reqwest behavior, pass a configured
`reqwest::Client` to `OAuthState::new`. This preserves the caller-provided
reqwest configuration across OAuth operations, including token requests.
```rust ignore
let default_headers = reqwest::header::HeaderMap::new();
let oauth_http_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.default_headers(default_headers)
.build()?;
let mut oauth_state = OAuthState::new(&server_url, Some(oauth_http_client))
.await
.context("Failed to initialize oauth state machine")?;
```
This is useful for proxy, TLS root, connector, timeout, and default-header
configuration while staying within reqwest. The redirect behavior is the
behavior of the provided reqwest client, so configure that client accordingly.
This OAuth HTTP client is separate from the `reqwest::Client` later passed to
`AuthClient::new`, which is used for the authorized MCP transport after tokens
have been obtained.
If OAuth requests must run outside reqwest, implement `OAuthHttpClient` and use
`OAuthState::new_with_oauth_http_client`. The SDK passes each OAuth request to
your implementation with the raw HTTP request, a suggested timeout, and an
`OAuthHttpRedirectPolicy`.
```rust ignore
use std::sync::Arc;
use rmcp::transport::{
OAuthHttpClient, OAuthHttpClientFuture, OAuthHttpRedirectPolicy,
OAuthHttpRequest, OAuthState,
};
struct MyOAuthHttpClient;
impl OAuthHttpClient for MyOAuthHttpClient {
fn execute(&self, request: OAuthHttpRequest) -> OAuthHttpClientFuture<'_> {
Box::pin(async move {
match request.redirect_policy {
OAuthHttpRedirectPolicy::Follow => {
// Follow redirects according to your HTTP environment.
}
OAuthHttpRedirectPolicy::Stop => {
// Return redirect responses without following them.
}
_ => {
// Future redirect policies may be added.
}
}
// Convert `request.request` into your HTTP stack's request type,
// execute it, then convert the response back into the expected
// OAuth HTTP response type.
let response = todo!("send OAuth request");
Ok(response)
})
}
}
let mut oauth_state = OAuthState::new_with_oauth_http_client(
&server_url,
Arc::new(MyOAuthHttpClient),
)
.await?;
```
Use this path when OAuth traffic must go through a browser fetch API, a remote
execution environment, a company gateway, a test fake, or any other non-reqwest
transport.
### 3. Start authorization with OAuthState
The `OAuthState` state machine manages the full authorization lifecycle. When no
scopes are provided, the SDK automatically selects scopes from the server's
WWW-Authenticate header, Protected Resource Metadata, or AS metadata.
```rust ignore
// initialize oauth state machine
let mut oauth_state = OAuthState::new(&server_url, None)
.await
.context("Failed to initialize oauth state machine")?;
// start authorization - pass empty scopes to let the SDK auto-select
oauth_state
.start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
.await
.context("Failed to start authorization")?;
// start authorization - pass empty scopes to let the SDK auto-select
oauth_state
.start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
.await
.context("Failed to start authorization")?;
```
If you know the scopes you need, you can still pass them explicitly:
```rust ignore
oauth_state
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
.await
.context("Failed to start authorization")?;
oauth_state
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
.await
.context("Failed to start authorization")?;
```
### 3. Get authorization url and handle callback
### 4. Get authorization url and handle callback
```rust ignore
// get authorization URL and guide user to open it
let auth_url = oauth_state.get_authorization_url().await?;
println!("Please open the following URL in your browser for authorization:\n{}", auth_url);
// get authorization URL and guide user to open it
let auth_url = oauth_state.get_authorization_url().await?;
println!("Please open the following URL in your browser for authorization:\n{}", auth_url);
// handle callback - in real applications, this is typically done in a callback server
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
oauth_state.handle_callback(auth_code, csrf_token).await?;
// handle callback - in real applications, this is typically done in a callback server
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
oauth_state.handle_callback(auth_code, csrf_token).await?;
```
### 4. Use Authorized Streamable HTTP Transport and create client
### 5. Use Authorized Streamable HTTP Transport and create client
```rust ignore
let am = oauth_state
.into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
let client = AuthClient::new(reqwest::Client::default(), am);
let transport = StreamableHttpClientTransport::with_client(
client,
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
);
let am = oauth_state
.into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
let client = AuthClient::new(reqwest::Client::default(), am);
let transport = StreamableHttpClientTransport::with_client(
client,
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
);
// create client and connect to MCP server
let client_service = ClientInfo::default();
let client = client_service.serve(transport).await?;
// create client and connect to MCP server
let client_service = ClientInfo::default();
let client = client_service.serve(transport).await?;
```
### 5. Handle scope upgrades
### 6. Handle scope upgrades
If a server returns 403 with `insufficient_scope`, you can request a scope
upgrade. The SDK computes the union of current and required scopes and
transitions back to the session state for re-authorization.
```rust ignore
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
Ok(auth_url) => {
// open auth_url in browser, handle callback as before
println!("Re-authorize at: {}", auth_url);
}
Err(e) => {
eprintln!("Scope upgrade failed: {}", e);
}
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
Ok(auth_url) => {
// open auth_url in browser, handle callback as before
println!("Re-authorize at: {}", auth_url);
}
Err(e) => {
eprintln!("Scope upgrade failed: {}", e);
}
}
```
## Complete Examples
- **Client**: `examples/clients/src/auth/oauth_client.rs`
- **Server**: `examples/servers/src/complex_auth_streamhttp.rs`
- **Client**: [`examples/clients/src/auth/oauth_client.rs`](../examples/clients/src/auth/oauth_client.rs)
- **Server**: [`examples/servers/src/complex_auth_streamhttp.rs`](../examples/servers/src/complex_auth_streamhttp.rs)
### Running the Examples
@ -134,6 +211,7 @@ cargo run -p mcp-client-examples --example clients_oauth_client
- **PKCE S256 always enforced**: never falls back to `plain` or no challenge. OAuth 2.1 mandates S256 as Mandatory To Implement for servers.
- **RFC 8707 resource binding**: authorization and token requests include the `resource` parameter to bind tokens to the protected resource
- **Redirect policy is explicit for custom OAuth clients**: discovery and registration requests use `OAuthHttpRedirectPolicy::Follow`, while token requests use `OAuthHttpRedirectPolicy::Stop` so custom implementations can avoid forwarding credentials to redirected endpoints
- All tokens are securely stored in memory (custom credential stores supported)
- Automatic token refresh reduces user intervention
- Server metadata validation warns on non-compliant configurations but proceeds where relatively safe
@ -147,7 +225,9 @@ If you encounter authorization issues, check the following:
3. Check network connection and firewall settings
4. Verify server supports metadata discovery or dynamic client registration
5. If PKCE fails, the server may not support S256 (non-compliant with OAuth 2.1)
6. Check `tracing` logs at debug level for detailed discovery and validation info
6. If OAuth requests need custom proxy, TLS, or connector settings, pass a configured reqwest client to `OAuthState::new`
7. If OAuth requests must run through a non-reqwest environment, implement `OAuthHttpClient` and use `OAuthState::new_with_oauth_http_client`
8. Check `tracing` logs at debug level for detailed discovery and validation info
## References

View file

@ -45,6 +45,7 @@ A client demonstrating how to authenticate with an MCP server using OAuth.
- Starts a local HTTP server to handle OAuth callbacks
- Initializes the OAuth state machine and begins the authorization flow
- Shows how to pass a configured reqwest client for OAuth discovery, registration, token exchange, and refresh requests
- Displays the authorization URL and waits for user authorization
- Establishes an authorized connection to the MCP server using the acquired access token
- Demonstrates how to use the authorized connection to retrieve available tools and prompts

View file

@ -1,4 +1,4 @@
use std::{env, net::SocketAddr, sync::Arc};
use std::{env, net::SocketAddr, sync::Arc, time::Duration};
use anyhow::{Context, Result};
use axum::{
@ -115,8 +115,16 @@ async fn main() -> Result<()> {
client_metadata_url
);
// Configure the HTTP client used for OAuth discovery, registration, token
// exchange, and refresh. Customize this builder for proxies, TLS roots,
// default headers, or other reqwest settings required by your environment.
let oauth_http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.context("Failed to build OAuth HTTP client")?;
// initialize oauth state machine
let mut oauth_state = OAuthState::new(&server_url, None)
let mut oauth_state = OAuthState::new(&server_url, Some(oauth_http_client))
.await
.context("Failed to initialize oauth state machine")?;
// use CIMD (SEP-991) with client metadata URL.