Skip to content

Commit 25b261f

Browse files
committed
fix(oauth): drop duplicate RFC 8707 resource param
rmcp 0.17.0 already appends the resource parameter to both the authorize URL (get_authorization_url) and the token exchange request. The gateway's add_resource_parameter wrapper appended it a second time, producing ?resource=...&resource=..., which strict authorization servers (e.g. Supabase, and OAuth-protected MCP servers reached via Claude) reject with "resource: Expected string, received array" — breaking the login flow entirely. Remove the redundant wrapper and its call site; rmcp handles the param. Ported from D:/mcpmux feat/workspace-root-routing (commit f08e8ec); verified rmcp 0.17.0 appends resource at auth.rs:757 and :914, matching rmcp 1.5. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 2b29d62 commit 25b261f

1 file changed

Lines changed: 51 additions & 35 deletions

File tree

crates/mcpmux-gateway/src/pool/oauth.rs

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -255,36 +255,6 @@ impl OutboundOAuthManager {
255255
scopes.iter().map(|s| s.as_str()).collect()
256256
}
257257

258-
/// Add RFC 8707 'resource' parameter to authorization URL.
259-
///
260-
/// The resource parameter tells the Authorization Server which protected resource
261-
/// (MCP server) the client is requesting access to. This enables the AS to:
262-
/// - Issue tokens scoped to the specific resource
263-
/// - Apply resource-specific policies
264-
/// - Prevent token replay at other resources
265-
///
266-
/// Some servers (like Miro) require this parameter.
267-
fn add_resource_parameter(auth_url: &str, server_url: &str) -> String {
268-
use url::Url;
269-
270-
match Url::parse(auth_url) {
271-
Ok(mut url) => {
272-
// Add the resource parameter with the MCP server URL
273-
url.query_pairs_mut().append_pair("resource", server_url);
274-
info!("[OAuth] Added RFC 8707 resource parameter: {}", server_url);
275-
url.to_string()
276-
}
277-
Err(e) => {
278-
warn!(
279-
"[OAuth] Failed to parse auth URL to add resource parameter: {}",
280-
e
281-
);
282-
// Return original URL if parsing fails
283-
auth_url.to_string()
284-
}
285-
}
286-
}
287-
288258
/// Subscribe to OAuth completion events
289259
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<OAuthCompleteEvent> {
290260
self.completion_tx.subscribe()
@@ -1247,11 +1217,6 @@ impl OutboundOAuthManager {
12471217
}
12481218
};
12491219

1250-
// Add RFC 8707 'resource' parameter to the authorization URL.
1251-
// This tells the Authorization Server which protected resource (MCP server)
1252-
// the token is being requested for. Some servers (like Miro) require this.
1253-
let auth_url = Self::add_resource_parameter(&auth_url, server_url);
1254-
12551220
// Extract state parameter from auth_url
12561221
let state = match Self::extract_state_from_url(&auth_url) {
12571222
Some(s) => s,
@@ -1684,3 +1649,54 @@ impl Default for OutboundOAuthManager {
16841649
Self::new()
16851650
}
16861651
}
1652+
1653+
#[cfg(test)]
1654+
mod resource_param_tests {
1655+
use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadata};
1656+
1657+
/// The gateway delegates authorize-URL construction entirely to rmcp's
1658+
/// `AuthorizationManager::get_authorization_url` (via `create_auth_manager` /
1659+
/// `start_oauth_flow`). rmcp appends the RFC 8707 `resource` parameter itself, so the
1660+
/// gateway must NOT add a second one. This guards against re-introducing the removed
1661+
/// `add_resource_parameter` wrapper, which produced `?resource=...&resource=...` —
1662+
/// rejected by strict authorization servers (e.g. Supabase) and broke OAuth login.
1663+
#[tokio::test]
1664+
async fn authorize_url_has_exactly_one_resource_param() {
1665+
let base_url = "https://mcp.example.test/";
1666+
1667+
let mut manager = AuthorizationManager::new(base_url)
1668+
.await
1669+
.expect("construct AuthorizationManager");
1670+
1671+
manager.set_metadata(AuthorizationMetadata {
1672+
authorization_endpoint: "https://auth.example.test/authorize".to_string(),
1673+
token_endpoint: "https://auth.example.test/token".to_string(),
1674+
response_types_supported: Some(vec!["code".to_string()]),
1675+
code_challenge_methods_supported: Some(vec!["S256".to_string()]),
1676+
..Default::default()
1677+
});
1678+
manager
1679+
.configure_client_id("test-client")
1680+
.expect("configure client id");
1681+
1682+
let auth_url = manager
1683+
.get_authorization_url(&["openid"])
1684+
.await
1685+
.expect("generate authorization url");
1686+
1687+
let parsed = url::Url::parse(&auth_url).expect("authorize url should parse");
1688+
let resource_values: Vec<String> = parsed
1689+
.query_pairs()
1690+
.filter(|(k, _)| k == "resource")
1691+
.map(|(_, v)| v.into_owned())
1692+
.collect();
1693+
1694+
assert_eq!(
1695+
resource_values.len(),
1696+
1,
1697+
"authorize URL must carry exactly one RFC 8707 resource param, \
1698+
got {resource_values:?} in {auth_url}"
1699+
);
1700+
assert_eq!(resource_values[0], base_url);
1701+
}
1702+
}

0 commit comments

Comments
 (0)