From 3bdfa1dc843edae7629e31ce46c1d37aaafecae3 Mon Sep 17 00:00:00 2001 From: Daniel Zabari Date: Wed, 29 Apr 2026 12:31:03 -0400 Subject: [PATCH] feat(auth): add RFC 8707 resource parameter to authorize and token requests Some authorization servers (e.g. Natoma, used for Atlassian's MCP server) require the OAuth 2.0 `resource` parameter on /authorize and /token to audience-bind the issued access token (RFC 8707, Resource Indicators for OAuth 2.0). Without it they reject the authorization request with `invalid_request` and the browser-based login flow never renders. Forward the AuthorizationManager's `base_url` (the MCP server URL) as the `resource` parameter on both: * the authorization URL produced by `get_authorization_url` * the token-exchange request issued by `exchange_code_for_token` This mirrors the upstream rust-sdk fix (modelcontextprotocol/rust-sdk #651, '11-25-2025 compliant Auth') which threads the same parameter through both call sites. Adds a unit test asserting that `resource` is present in the generated authorize URL and equals the manager's base URL. `cargo test -p rmcp --features=auth --lib transport::auth` => 23 passed. Co-Authored-By: Oz --- crates/rmcp/src/transport/auth.rs | 53 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 6a5567f48..f5113f0a5 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -612,9 +612,15 @@ impl AuthorizationManager { let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); // build authorization request + // + // RFC 8707 (Resource Indicators for OAuth 2.0): include the resource + // server's URL as the `resource` parameter so that the authorization + // server can audience-bind the issued access token. Some IdPs (e.g. + // Natoma) reject /authorize calls that omit this parameter. let mut auth_request = oauth_client .authorize_url(CsrfToken::new_random) - .set_pkce_challenge(pkce_challenge); + .set_pkce_challenge(pkce_challenge) + .add_extra_param("resource", self.base_url.to_string()); // add request scopes for scope in scopes { @@ -663,9 +669,15 @@ impl AuthorizationManager { debug!("client_id: {:?}", oauth_client.client_id()); // exchange token + // + // RFC 8707 (Resource Indicators for OAuth 2.0): forward the resource + // server URL to the token endpoint as well, mirroring what was sent + // on the /authorize request. Authorization servers that enforce + // resource indicators will reject token exchanges otherwise. let token_result = match oauth_client .exchange_code(AuthorizationCode::new(code.to_string())) .set_pkce_verifier(pkce_verifier) + .add_extra_param("resource", self.base_url.to_string()) .request_async(&http_client) .await { @@ -1455,8 +1467,8 @@ mod tests { use url::Url; use super::{ - AuthError, AuthorizationManager, InMemoryStateStore, StateStore, StoredAuthorizationState, - is_https_url, + AuthError, AuthorizationManager, AuthorizationMetadata, InMemoryStateStore, + OAuthClientConfig, StateStore, StoredAuthorizationState, is_https_url, }; // SEP-991: URL-based Client IDs @@ -1483,6 +1495,41 @@ mod tests { assert!(!is_https_url("data:text/html,")); } + /// RFC 8707: the authorization URL must include the `resource` parameter + /// pointing at the resource server (the manager's `base_url`). Without it, + /// authorization servers like Natoma reject the /authorize call with + /// `invalid_request`. + #[tokio::test] + async fn authorize_url_includes_rfc8707_resource_parameter() { + let base = "https://mcp.example.com/v1/sse"; + let mut manager = AuthorizationManager::new(base).await.unwrap(); + manager.set_metadata(AuthorizationMetadata { + authorization_endpoint: "https://auth.example.com/authorize".to_string(), + token_endpoint: "https://auth.example.com/token".to_string(), + ..Default::default() + }); + manager + .configure_client(OAuthClientConfig { + client_id: "test-client".to_string(), + client_secret: None, + scopes: vec![], + redirect_uri: "warp://mcp/oauth2callback".to_string(), + }) + .unwrap(); + + let auth_url = manager.get_authorization_url(&[]).await.unwrap(); + let parsed = Url::parse(&auth_url).unwrap(); + let resource = parsed + .query_pairs() + .find(|(k, _)| k == "resource") + .map(|(_, v)| v.into_owned()); + assert_eq!( + resource.as_deref(), + Some(base), + "authorize URL must include resource={base}, got: {auth_url}" + ); + } + #[test] fn parses_resource_metadata_parameter() { let header = r#"Bearer error="invalid_request", error_description="missing token", resource_metadata="https://example.com/.well-known/oauth-protected-resource/api""#;