Model Context Protocol OAuth Authorization

June 25, 2026 · View on GitHub

This document describes the OAuth 2.1 authorization implementation for Model Context Protocol (MCP), following the MCP 2025-11-25 Authorization Specification.

Features

  • Full support for OAuth 2.1 authorization flow with PKCE (S256)
  • RFC 8707 resource parameter binding
  • Protected Resource Metadata discovery (RFC 9728)
  • Authorization Server Metadata discovery (RFC 8414 + OpenID Connect)
  • Dynamic client registration (RFC 7591)
  • Client ID Metadata Documents (CIMD) (SEP-991 / Client ID Metadata Documents )
  • Scope selection from WWW-Authenticate, Protected Resource Metadata, and AS metadata
  • 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

1. Enable Features

Enable the auth feature in Cargo.toml:

[dependencies]
rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-reqwest"] }

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.

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.

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.

// 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:

oauth_state
    .start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
    .await
    .context("Failed to start authorization")?;

4. Get authorization url and handle callback

// 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?;

5. Use Authorized Streamable HTTP Transport and create client

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?;

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.

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

Running the Examples

# Run the OAuth server
cargo run -p mcp-server-examples --example servers_complex_auth_streamhttp

# Run the OAuth client (in another terminal)
cargo run -p mcp-client-examples --example clients_oauth_client

Authorization Flow Description

  1. Resource Metadata Discovery: Client probes the server and extracts WWW-Authenticate parameters including resource_metadata URL and scope
  2. Protected Resource Metadata: Client fetches resource server metadata (RFC 9728) to find authorization server(s) and supported scopes
  3. AS Metadata Discovery: Client discovers authorization server metadata via RFC 8414 and OpenID Connect well-known endpoints
  4. Client Registration: If supported, client dynamically registers itself (or uses URL-based Client ID via SEP-991)
  5. Scope Selection: SDK picks scopes from WWW-Authenticate > PRM > AS metadata > caller defaults
  6. Authorization Request: Build authorization URL with PKCE (S256) and RFC 8707 resource parameter
  7. Authorization Code Exchange: After user authorization, exchange code for access token (with resource parameter)
  8. Token Usage: Use access token for API calls via AuthClient or AuthorizedHttpClient
  9. Token Refresh: Automatically use refresh token to get new access token when current one expires; previously granted scopes are forwarded in the refresh request so providers that require them (e.g. Azure AD v2) work correctly
  10. Scope Upgrade: On 403 insufficient_scope, compute scope union and re-authorize with upgraded scopes

Security Considerations

  • 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

Troubleshooting

If you encounter authorization issues, check the following:

  1. Ensure server supports OAuth 2.1 authorization
  2. Verify callback URI matches server's allowed redirect URIs
  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. 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