Skip to content

Commit 434ccb7

Browse files
authored
fix(auth): pass WWW-Authenticate scopes to DCR registration request (#705)
* fix(auth): pass WWW-Authenticate scopes to DCR registration request When an MCP server returns a 401 with `WWW-Authenticate: Bearer scope="..."`, the scopes are parsed but never included in the Dynamic Client Registration (DCR) request. Per RFC 7591, the DCR request should include a `scope` field so the authorization server knows what scopes the client intends to use. Servers that enforce scope-matching between registration and authorization will reject the flow without this. Changes: - Add optional `scope` field to `ClientRegistrationRequest` with `skip_serializing_if` for backward compatibility - Update `register_client()` to accept scopes parameter and include them in the DCR request body and returned `OAuthClientConfig` - Thread scopes from `AuthorizationSession::new()` into both `register_client()` call sites - Re-export `oauth2::TokenResponse` trait so consumers can extract scopes from token responses - Add serialization tests for the new `scope` field * refactor(auth): change register_client to accept &[&str] instead of &[String] Avoids unnecessary Vec<String> allocation in callers that already have &[&str]. * fix(auth): make ClientRegistrationRequest crate-private * refactor(auth): stop re-exporting oauth2 TokenResponse trait * style(auth): merge TokenResponse into grouped oauth2 import Fix nightly rustfmt check by consolidating the separate `use oauth2::TokenResponse` into the existing `use oauth2::{...}` block.
1 parent 2d90b76 commit 434ccb7

File tree

1 file changed

+42
-4
lines changed

1 file changed

+42
-4
lines changed

crates/rmcp/src/transport/auth.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,12 +438,14 @@ pub struct AuthorizationManager {
438438
}
439439

440440
#[derive(Debug, Clone, Serialize, Deserialize)]
441-
pub struct ClientRegistrationRequest {
441+
pub(crate) struct ClientRegistrationRequest {
442442
pub client_name: String,
443443
pub redirect_uris: Vec<String>,
444444
pub grant_types: Vec<String>,
445445
pub token_endpoint_auth_method: String,
446446
pub response_types: Vec<String>,
447+
#[serde(skip_serializing_if = "Option::is_none")]
448+
pub scope: Option<String>,
447449
}
448450

449451
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -683,6 +685,7 @@ impl AuthorizationManager {
683685
&mut self,
684686
name: &str,
685687
redirect_uri: &str,
688+
scopes: &[&str],
686689
) -> Result<OAuthClientConfig, AuthError> {
687690
if self.metadata.is_none() {
688691
return Err(AuthError::NoAuthorizationSupport);
@@ -705,6 +708,11 @@ impl AuthorizationManager {
705708
],
706709
token_endpoint_auth_method: "none".to_string(), // public client
707710
response_types: vec!["code".to_string()],
711+
scope: if scopes.is_empty() {
712+
None
713+
} else {
714+
Some(scopes.join(" "))
715+
},
708716
};
709717

710718
let response = match self
@@ -758,7 +766,7 @@ impl AuthorizationManager {
758766
// as a password, which is not a goal of the client secret.
759767
client_secret: reg_response.client_secret.filter(|s| !s.is_empty()),
760768
redirect_uri: redirect_uri.to_string(),
761-
scopes: vec![],
769+
scopes: scopes.iter().map(|s| s.to_string()).collect(),
762770
};
763771

764772
self.configure_client(config.clone())?;
@@ -1526,7 +1534,7 @@ impl AuthorizationSession {
15261534
} else {
15271535
// Fallback to dynamic registration
15281536
auth_manager
1529-
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri)
1537+
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri, scopes)
15301538
.await
15311539
.map_err(|e| {
15321540
AuthError::RegistrationFailed(format!("Dynamic registration failed: {}", e))
@@ -1535,7 +1543,7 @@ impl AuthorizationSession {
15351543
} else {
15361544
// Fallback to dynamic registration
15371545
match auth_manager
1538-
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri)
1546+
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri, scopes)
15391547
.await
15401548
{
15411549
Ok(config) => config,
@@ -2831,4 +2839,34 @@ mod tests {
28312839
"expected InternalError when OAuth client is not configured, got: {err:?}"
28322840
);
28332841
}
2842+
2843+
// -- ClientRegistrationRequest serialization --
2844+
2845+
#[test]
2846+
fn client_registration_request_includes_scope_when_present() {
2847+
let req = super::ClientRegistrationRequest {
2848+
client_name: "test".to_string(),
2849+
redirect_uris: vec!["http://localhost/callback".to_string()],
2850+
grant_types: vec!["authorization_code".to_string()],
2851+
token_endpoint_auth_method: "none".to_string(),
2852+
response_types: vec!["code".to_string()],
2853+
scope: Some("read write".to_string()),
2854+
};
2855+
let json = serde_json::to_value(&req).unwrap();
2856+
assert_eq!(json["scope"], "read write");
2857+
}
2858+
2859+
#[test]
2860+
fn client_registration_request_omits_scope_when_none() {
2861+
let req = super::ClientRegistrationRequest {
2862+
client_name: "test".to_string(),
2863+
redirect_uris: vec!["http://localhost/callback".to_string()],
2864+
grant_types: vec!["authorization_code".to_string()],
2865+
token_endpoint_auth_method: "none".to_string(),
2866+
response_types: vec!["code".to_string()],
2867+
scope: None,
2868+
};
2869+
let json = serde_json::to_value(&req).unwrap();
2870+
assert!(!json.as_object().unwrap().contains_key("scope"));
2871+
}
28342872
}

0 commit comments

Comments
 (0)