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
commit f1daa9277e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 140 additions and 51 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) - Scope upgrade on 403 insufficient_scope (SEP-835)
- Automatic token refresh - Automatic token refresh
- Authorized HTTP Client implementation - Authorized HTTP Client implementation
- Injectable OAuth HTTP client for custom network environments
## Usage Guide ## Usage Guide
@ -26,86 +27,162 @@ Enable the auth feature in Cargo.toml:
rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-reqwest"] } 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 The `OAuthState` state machine manages the full authorization lifecycle. When no
scopes are provided, the SDK automatically selects scopes from the server's scopes are provided, the SDK automatically selects scopes from the server's
WWW-Authenticate header, Protected Resource Metadata, or AS metadata. WWW-Authenticate header, Protected Resource Metadata, or AS metadata.
```rust ignore ```rust ignore
// initialize oauth state machine // start authorization - pass empty scopes to let the SDK auto-select
let mut oauth_state = OAuthState::new(&server_url, None) oauth_state
.await .start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
.context("Failed to initialize oauth state machine")?; .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: If you know the scopes you need, you can still pass them explicitly:
```rust ignore ```rust ignore
oauth_state oauth_state
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client")) .start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
.await .await
.context("Failed to start authorization")?; .context("Failed to start authorization")?;
``` ```
### 3. Get authorization url and handle callback ### 4. Get authorization url and handle callback
```rust ignore ```rust ignore
// get authorization URL and guide user to open it // get authorization URL and guide user to open it
let auth_url = oauth_state.get_authorization_url().await?; let auth_url = oauth_state.get_authorization_url().await?;
println!("Please open the following URL in your browser for authorization:\n{}", auth_url); 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 // 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 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"; let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
oauth_state.handle_callback(auth_code, csrf_token).await?; 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 ```rust ignore
let am = oauth_state let am = oauth_state
.into_authorization_manager() .into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?; .ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
let client = AuthClient::new(reqwest::Client::default(), am); let client = AuthClient::new(reqwest::Client::default(), am);
let transport = StreamableHttpClientTransport::with_client( let transport = StreamableHttpClientTransport::with_client(
client, client,
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL), StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
); );
// create client and connect to MCP server // create client and connect to MCP server
let client_service = ClientInfo::default(); let client_service = ClientInfo::default();
let client = client_service.serve(transport).await?; 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 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 upgrade. The SDK computes the union of current and required scopes and
transitions back to the session state for re-authorization. transitions back to the session state for re-authorization.
```rust ignore ```rust ignore
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await { match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
Ok(auth_url) => { Ok(auth_url) => {
// open auth_url in browser, handle callback as before // open auth_url in browser, handle callback as before
println!("Re-authorize at: {}", auth_url); println!("Re-authorize at: {}", auth_url);
}
Err(e) => {
eprintln!("Scope upgrade failed: {}", e);
}
} }
Err(e) => {
eprintln!("Scope upgrade failed: {}", e);
}
}
``` ```
## Complete Examples ## Complete Examples
- **Client**: `examples/clients/src/auth/oauth_client.rs` - **Client**: [`examples/clients/src/auth/oauth_client.rs`](../examples/clients/src/auth/oauth_client.rs)
- **Server**: `examples/servers/src/complex_auth_streamhttp.rs` - **Server**: [`examples/servers/src/complex_auth_streamhttp.rs`](../examples/servers/src/complex_auth_streamhttp.rs)
### Running the Examples ### 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. - **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 - **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) - All tokens are securely stored in memory (custom credential stores supported)
- Automatic token refresh reduces user intervention - Automatic token refresh reduces user intervention
- Server metadata validation warns on non-compliant configurations but proceeds where relatively safe - 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 3. Check network connection and firewall settings
4. Verify server supports metadata discovery or dynamic client registration 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) 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 ## 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 - Starts a local HTTP server to handle OAuth callbacks
- Initializes the OAuth state machine and begins the authorization flow - 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 - Displays the authorization URL and waits for user authorization
- Establishes an authorized connection to the MCP server using the acquired access token - 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 - 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 anyhow::{Context, Result};
use axum::{ use axum::{
@ -115,8 +115,16 @@ async fn main() -> Result<()> {
client_metadata_url 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 // 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 .await
.context("Failed to initialize oauth state machine")?; .context("Failed to initialize oauth state machine")?;
// use CIMD (SEP-991) with client metadata URL. // use CIMD (SEP-991) with client metadata URL.