docs: explain OAuth HTTP client setup (#918)
This commit is contained in:
parent
77932141e3
commit
f1daa9277e
3 changed files with 138 additions and 49 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue