Skip to content

Commit f59dd43

Browse files
committed
feat: reject auth servers lacking S256 PKCE support
1 parent bdf0c32 commit f59dd43

1 file changed

Lines changed: 86 additions & 16 deletions

File tree

crates/rmcp/src/transport/auth.rs

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ pub enum AuthError {
477477
#[error("Metadata error: {0}")]
478478
MetadataError(String),
479479

480+
#[error("Authorization server does not support the required PKCE code challenge method (S256)")]
481+
PkceUnsupported,
482+
480483
#[error("URL parse error: {0}")]
481484
UrlError(#[from] url::ParseError),
482485

@@ -780,6 +783,10 @@ pub struct AuthorizationManager {
780783
resource_scopes: RwLock<Vec<String>>,
781784
/// OIDC Dynamic Client Registration `application_type` (SEP-837)
782785
application_type: Option<String>,
786+
/// Refuse servers that omit `code_challenge_methods_supported` entirely.
787+
/// Off by default; a server that advertises methods without S256 is always
788+
/// refused regardless of this flag.
789+
require_pkce_support: bool,
783790
}
784791

785792
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -993,6 +1000,7 @@ impl AuthorizationManager {
9931000
www_auth_scopes: RwLock::new(Vec::new()),
9941001
resource_scopes: RwLock::new(Vec::new()),
9951002
application_type: Some(DEFAULT_APPLICATION_TYPE.to_string()),
1003+
require_pkce_support: false,
9961004
};
9971005

9981006
Ok(manager)
@@ -1003,6 +1011,15 @@ impl AuthorizationManager {
10031011
self.scope_upgrade_config = config;
10041012
}
10051013

1014+
/// Refuse servers that omit `code_challenge_methods_supported` (off by default).
1015+
///
1016+
/// A server that advertises methods without `S256` is always refused;
1017+
/// enabling this rejects the omitted-field case too, for strict
1018+
/// OAuth 2.1 / MCP compliance.
1019+
pub fn set_require_pkce_support(&mut self, require: bool) {
1020+
self.require_pkce_support = require;
1021+
}
1022+
10061023
/// Set a custom credential store
10071024
///
10081025
/// This allows you to provide your own implementation of credential storage,
@@ -1159,18 +1176,24 @@ impl AuthorizationManager {
11591176
}
11601177
}
11611178

1162-
// for PKCE, we always send s256 since oauth 2.1 requires servers to support it,
1163-
// but warn if the server metadata suggests otherwise
1179+
// The client always sends an S256 challenge. A server that advertises
1180+
// methods without S256 can't do the flow we require, so refuse it. A
1181+
// server that omits the field is tolerated by default, and only refused
1182+
// when `require_pkce_support` is opted in.
11641183
match &metadata.code_challenge_methods_supported {
11651184
Some(methods) if !methods.iter().any(|m| m == "S256") => {
1185+
return Err(AuthError::PkceUnsupported);
1186+
}
1187+
None if self.require_pkce_support => {
1188+
return Err(AuthError::PkceUnsupported);
1189+
}
1190+
None => {
11661191
warn!(
1167-
?methods,
1168-
"server does not advertise S256 in code_challenge_methods_supported, \
1169-
proceeding with S256 anyway as oauth 2.1 requires it. \
1170-
The server is not compliant with the specification!"
1192+
"authorization server metadata omits code_challenge_methods_supported; \
1193+
proceeding with an S256 challenge anyway"
11711194
);
11721195
}
1173-
_ => {}
1196+
Some(_) => {}
11741197
}
11751198

11761199
Ok(())
@@ -2849,6 +2872,16 @@ impl OAuthState {
28492872
Ok(OAuthState::Unauthorized(manager))
28502873
}
28512874

2875+
/// Strictly require the authorization server to advertise PKCE support.
2876+
///
2877+
/// Must be called before authorization begins, while the state is still
2878+
/// unauthorized. See [`AuthorizationManager::set_require_pkce_support`].
2879+
pub fn set_require_pkce_support(&mut self, require: bool) {
2880+
if let OAuthState::Unauthorized(manager) = self {
2881+
manager.set_require_pkce_support(require);
2882+
}
2883+
}
2884+
28522885
/// Get client_id and OAuth credentials
28532886
pub async fn get_credentials(&self) -> Result<Credentials, AuthError> {
28542887
// return client_id and credentials
@@ -4307,22 +4340,59 @@ mod tests {
43074340
assert!(manager.validate_server_metadata("code").is_err());
43084341
}
43094342

4310-
#[tokio::test]
4311-
async fn test_validate_as_metadata_passes_without_pkce_s256() {
4312-
let mut manager = AuthorizationManager::new("https://example.com")
4313-
.await
4314-
.unwrap();
4315-
let metadata = AuthorizationMetadata {
4343+
fn as_metadata_with_pkce(methods: Option<Vec<String>>) -> AuthorizationMetadata {
4344+
AuthorizationMetadata {
43164345
authorization_endpoint: "https://auth.example.com/authorize".to_string(),
43174346
token_endpoint: "https://auth.example.com/token".to_string(),
43184347
response_types_supported: Some(vec!["code".to_string()]),
4319-
code_challenge_methods_supported: Some(vec!["plain".to_string()]),
4348+
code_challenge_methods_supported: methods,
43204349
..Default::default()
4321-
};
4322-
manager.set_metadata(metadata);
4350+
}
4351+
}
4352+
4353+
#[tokio::test]
4354+
async fn test_validate_as_metadata_rejects_without_pkce_s256() {
4355+
let mut manager = AuthorizationManager::new("https://example.com")
4356+
.await
4357+
.unwrap();
4358+
manager.set_metadata(as_metadata_with_pkce(Some(vec!["plain".to_string()])));
4359+
assert!(matches!(
4360+
manager.validate_server_metadata("code"),
4361+
Err(AuthError::PkceUnsupported)
4362+
));
4363+
}
4364+
4365+
#[tokio::test]
4366+
async fn test_validate_as_metadata_allows_absent_pkce_methods_by_default() {
4367+
let mut manager = AuthorizationManager::new("https://example.com")
4368+
.await
4369+
.unwrap();
4370+
manager.set_metadata(as_metadata_with_pkce(None));
4371+
assert!(manager.validate_server_metadata("code").is_ok());
4372+
}
4373+
4374+
#[tokio::test]
4375+
async fn test_validate_as_metadata_passes_with_pkce_s256() {
4376+
let mut manager = AuthorizationManager::new("https://example.com")
4377+
.await
4378+
.unwrap();
4379+
manager.set_metadata(as_metadata_with_pkce(Some(vec!["S256".to_string()])));
43234380
assert!(manager.validate_server_metadata("code").is_ok());
43244381
}
43254382

4383+
#[tokio::test]
4384+
async fn test_validate_as_metadata_rejects_absent_pkce_methods_when_strict() {
4385+
let mut manager = AuthorizationManager::new("https://example.com")
4386+
.await
4387+
.unwrap();
4388+
manager.set_require_pkce_support(true);
4389+
manager.set_metadata(as_metadata_with_pkce(None));
4390+
assert!(matches!(
4391+
manager.validate_server_metadata("code"),
4392+
Err(AuthError::PkceUnsupported)
4393+
));
4394+
}
4395+
43264396
#[tokio::test]
43274397
async fn test_validate_as_metadata_passes_without_metadata() {
43284398
let manager = AuthorizationManager::new("https://example.com")

0 commit comments

Comments
 (0)