Skip to content

Commit f1daa92

Browse files
authored
docs: explain OAuth HTTP client setup (#918)
1 parent 7793214 commit f1daa92

3 files changed

Lines changed: 140 additions & 51 deletions

File tree

docs/OAUTH_SUPPORT.md

Lines changed: 129 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This document describes the OAuth 2.1 authorization implementation for Model Con
1414
- Scope upgrade on 403 insufficient_scope (SEP-835)
1515
- Automatic token refresh
1616
- Authorized HTTP Client implementation
17+
- Injectable OAuth HTTP client for custom network environments
1718

1819
## Usage Guide
1920

@@ -26,86 +27,162 @@ Enable the auth feature in Cargo.toml:
2627
rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-reqwest"] }
2728
```
2829

29-
### 2. Use OAuthState
30+
### 2. Configure OAuth network requests
31+
32+
OAuth makes several HTTP requests before the MCP transport is connected:
33+
protected-resource discovery, authorization-server discovery, dynamic client
34+
registration, authorization-code exchange, token refresh, and client credentials
35+
exchange. When no OAuth HTTP client is provided, the SDK sends those requests
36+
with an internally-created `reqwest::Client`.
37+
38+
If you only need to customize reqwest behavior, pass a configured
39+
`reqwest::Client` to `OAuthState::new`. This preserves the caller-provided
40+
reqwest configuration across OAuth operations, including token requests.
41+
42+
```rust ignore
43+
let default_headers = reqwest::header::HeaderMap::new();
44+
let oauth_http_client = reqwest::Client::builder()
45+
.timeout(std::time::Duration::from_secs(60))
46+
.default_headers(default_headers)
47+
.build()?;
48+
49+
let mut oauth_state = OAuthState::new(&server_url, Some(oauth_http_client))
50+
.await
51+
.context("Failed to initialize oauth state machine")?;
52+
```
53+
54+
This is useful for proxy, TLS root, connector, timeout, and default-header
55+
configuration while staying within reqwest. The redirect behavior is the
56+
behavior of the provided reqwest client, so configure that client accordingly.
57+
This OAuth HTTP client is separate from the `reqwest::Client` later passed to
58+
`AuthClient::new`, which is used for the authorized MCP transport after tokens
59+
have been obtained.
60+
61+
If OAuth requests must run outside reqwest, implement `OAuthHttpClient` and use
62+
`OAuthState::new_with_oauth_http_client`. The SDK passes each OAuth request to
63+
your implementation with the raw HTTP request, a suggested timeout, and an
64+
`OAuthHttpRedirectPolicy`.
65+
66+
```rust ignore
67+
use std::sync::Arc;
68+
69+
use rmcp::transport::{
70+
OAuthHttpClient, OAuthHttpClientFuture, OAuthHttpRedirectPolicy,
71+
OAuthHttpRequest, OAuthState,
72+
};
73+
74+
struct MyOAuthHttpClient;
75+
76+
impl OAuthHttpClient for MyOAuthHttpClient {
77+
fn execute(&self, request: OAuthHttpRequest) -> OAuthHttpClientFuture<'_> {
78+
Box::pin(async move {
79+
match request.redirect_policy {
80+
OAuthHttpRedirectPolicy::Follow => {
81+
// Follow redirects according to your HTTP environment.
82+
}
83+
OAuthHttpRedirectPolicy::Stop => {
84+
// Return redirect responses without following them.
85+
}
86+
_ => {
87+
// Future redirect policies may be added.
88+
}
89+
}
90+
91+
// Convert `request.request` into your HTTP stack's request type,
92+
// execute it, then convert the response back into the expected
93+
// OAuth HTTP response type.
94+
let response = todo!("send OAuth request");
95+
Ok(response)
96+
})
97+
}
98+
}
99+
100+
let mut oauth_state = OAuthState::new_with_oauth_http_client(
101+
&server_url,
102+
Arc::new(MyOAuthHttpClient),
103+
)
104+
.await?;
105+
```
106+
107+
Use this path when OAuth traffic must go through a browser fetch API, a remote
108+
execution environment, a company gateway, a test fake, or any other non-reqwest
109+
transport.
110+
111+
### 3. Start authorization with OAuthState
30112

31113
The `OAuthState` state machine manages the full authorization lifecycle. When no
32114
scopes are provided, the SDK automatically selects scopes from the server's
33115
WWW-Authenticate header, Protected Resource Metadata, or AS metadata.
34116

35117
```rust ignore
36-
// initialize oauth state machine
37-
let mut oauth_state = OAuthState::new(&server_url, None)
38-
.await
39-
.context("Failed to initialize oauth state machine")?;
40-
41-
// start authorization - pass empty scopes to let the SDK auto-select
42-
oauth_state
43-
.start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
44-
.await
45-
.context("Failed to start authorization")?;
118+
// start authorization - pass empty scopes to let the SDK auto-select
119+
oauth_state
120+
.start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
121+
.await
122+
.context("Failed to start authorization")?;
46123
```
47124

48125
If you know the scopes you need, you can still pass them explicitly:
49126

50127
```rust ignore
51-
oauth_state
52-
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
53-
.await
54-
.context("Failed to start authorization")?;
128+
oauth_state
129+
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
130+
.await
131+
.context("Failed to start authorization")?;
55132
```
56133

57-
### 3. Get authorization url and handle callback
134+
### 4. Get authorization url and handle callback
58135

59136
```rust ignore
60-
// get authorization URL and guide user to open it
61-
let auth_url = oauth_state.get_authorization_url().await?;
62-
println!("Please open the following URL in your browser for authorization:\n{}", auth_url);
63-
64-
// handle callback - in real applications, this is typically done in a callback server
65-
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
66-
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
67-
oauth_state.handle_callback(auth_code, csrf_token).await?;
137+
// get authorization URL and guide user to open it
138+
let auth_url = oauth_state.get_authorization_url().await?;
139+
println!("Please open the following URL in your browser for authorization:\n{}", auth_url);
140+
141+
// handle callback - in real applications, this is typically done in a callback server
142+
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
143+
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
144+
oauth_state.handle_callback(auth_code, csrf_token).await?;
68145
```
69146

70-
### 4. Use Authorized Streamable HTTP Transport and create client
147+
### 5. Use Authorized Streamable HTTP Transport and create client
71148

72149
```rust ignore
73-
let am = oauth_state
74-
.into_authorization_manager()
75-
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
76-
let client = AuthClient::new(reqwest::Client::default(), am);
77-
let transport = StreamableHttpClientTransport::with_client(
78-
client,
79-
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
80-
);
81-
82-
// create client and connect to MCP server
83-
let client_service = ClientInfo::default();
84-
let client = client_service.serve(transport).await?;
150+
let am = oauth_state
151+
.into_authorization_manager()
152+
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
153+
let client = AuthClient::new(reqwest::Client::default(), am);
154+
let transport = StreamableHttpClientTransport::with_client(
155+
client,
156+
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
157+
);
158+
159+
// create client and connect to MCP server
160+
let client_service = ClientInfo::default();
161+
let client = client_service.serve(transport).await?;
85162
```
86163

87-
### 5. Handle scope upgrades
164+
### 6. Handle scope upgrades
88165

89166
If a server returns 403 with `insufficient_scope`, you can request a scope
90167
upgrade. The SDK computes the union of current and required scopes and
91168
transitions back to the session state for re-authorization.
92169

93170
```rust ignore
94-
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
95-
Ok(auth_url) => {
96-
// open auth_url in browser, handle callback as before
97-
println!("Re-authorize at: {}", auth_url);
98-
}
99-
Err(e) => {
100-
eprintln!("Scope upgrade failed: {}", e);
101-
}
171+
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
172+
Ok(auth_url) => {
173+
// open auth_url in browser, handle callback as before
174+
println!("Re-authorize at: {}", auth_url);
175+
}
176+
Err(e) => {
177+
eprintln!("Scope upgrade failed: {}", e);
102178
}
179+
}
103180
```
104181

105182
## Complete Examples
106183

107-
- **Client**: `examples/clients/src/auth/oauth_client.rs`
108-
- **Server**: `examples/servers/src/complex_auth_streamhttp.rs`
184+
- **Client**: [`examples/clients/src/auth/oauth_client.rs`](../examples/clients/src/auth/oauth_client.rs)
185+
- **Server**: [`examples/servers/src/complex_auth_streamhttp.rs`](../examples/servers/src/complex_auth_streamhttp.rs)
109186

110187
### Running the Examples
111188

@@ -134,6 +211,7 @@ cargo run -p mcp-client-examples --example clients_oauth_client
134211

135212
- **PKCE S256 always enforced**: never falls back to `plain` or no challenge. OAuth 2.1 mandates S256 as Mandatory To Implement for servers.
136213
- **RFC 8707 resource binding**: authorization and token requests include the `resource` parameter to bind tokens to the protected resource
214+
- **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
137215
- All tokens are securely stored in memory (custom credential stores supported)
138216
- Automatic token refresh reduces user intervention
139217
- 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:
147225
3. Check network connection and firewall settings
148226
4. Verify server supports metadata discovery or dynamic client registration
149227
5. If PKCE fails, the server may not support S256 (non-compliant with OAuth 2.1)
150-
6. Check `tracing` logs at debug level for detailed discovery and validation info
228+
6. If OAuth requests need custom proxy, TLS, or connector settings, pass a configured reqwest client to `OAuthState::new`
229+
7. If OAuth requests must run through a non-reqwest environment, implement `OAuthHttpClient` and use `OAuthState::new_with_oauth_http_client`
230+
8. Check `tracing` logs at debug level for detailed discovery and validation info
151231

152232
## References
153233

examples/clients/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ A client demonstrating how to authenticate with an MCP server using OAuth.
4545

4646
- Starts a local HTTP server to handle OAuth callbacks
4747
- Initializes the OAuth state machine and begins the authorization flow
48+
- Shows how to pass a configured reqwest client for OAuth discovery, registration, token exchange, and refresh requests
4849
- Displays the authorization URL and waits for user authorization
4950
- Establishes an authorized connection to the MCP server using the acquired access token
5051
- Demonstrates how to use the authorized connection to retrieve available tools and prompts

examples/clients/src/auth/oauth_client.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{env, net::SocketAddr, sync::Arc};
1+
use std::{env, net::SocketAddr, sync::Arc, time::Duration};
22

33
use anyhow::{Context, Result};
44
use axum::{
@@ -115,8 +115,16 @@ async fn main() -> Result<()> {
115115
client_metadata_url
116116
);
117117

118+
// Configure the HTTP client used for OAuth discovery, registration, token
119+
// exchange, and refresh. Customize this builder for proxies, TLS roots,
120+
// default headers, or other reqwest settings required by your environment.
121+
let oauth_http_client = reqwest::Client::builder()
122+
.timeout(Duration::from_secs(30))
123+
.build()
124+
.context("Failed to build OAuth HTTP client")?;
125+
118126
// initialize oauth state machine
119-
let mut oauth_state = OAuthState::new(&server_url, None)
127+
let mut oauth_state = OAuthState::new(&server_url, Some(oauth_http_client))
120128
.await
121129
.context("Failed to initialize oauth state machine")?;
122130
// use CIMD (SEP-991) with client metadata URL.

0 commit comments

Comments
 (0)