@@ -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:
2627rmcp = { 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
31113The ` OAuthState ` state machine manages the full authorization lifecycle. When no
32114scopes are provided, the SDK automatically selects scopes from the server's
33115WWW-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
48125If 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
89166If a server returns 403 with ` insufficient_scope ` , you can request a scope
90167upgrade. The SDK computes the union of current and required scopes and
91168transitions 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:
1472253 . Check network connection and firewall settings
1482264 . Verify server supports metadata discovery or dynamic client registration
1492275 . 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
0 commit comments