Skip to content

Commit 9029a36

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

1 file changed

Lines changed: 91 additions & 16 deletions

File tree

crates/rmcp/src/transport/auth.rs

Lines changed: 91 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 (matching the reference SDKs); a server that advertises
788+
/// methods without S256 is always 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,18 @@ impl AuthorizationManager {
10031011
self.scope_upgrade_config = config;
10041012
}
10051013

1014+
/// Strictly require the authorization server to advertise PKCE support.
1015+
///
1016+
/// The client always sends an `S256` challenge. When the server's metadata
1017+
/// lists `code_challenge_methods_supported` without `S256`, authorization is
1018+
/// always refused with [`AuthError::PkceUnsupported`]. When the field is
1019+
/// omitted entirely, the default is to proceed anyway (matching the reference
1020+
/// SDKs); enabling this makes that case a hard error too, for full
1021+
/// OAuth 2.1 / MCP `MUST` compliance.
1022+
pub fn set_require_pkce_support(&mut self, require: bool) {
1023+
self.require_pkce_support = require;
1024+
}
1025+
10061026
/// Set a custom credential store
10071027
///
10081028
/// This allows you to provide your own implementation of credential storage,
@@ -1159,18 +1179,26 @@ impl AuthorizationManager {
11591179
}
11601180
}
11611181

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
1182+
// The client always sends an S256 challenge, so validate the server's
1183+
// advertised PKCE methods. This mirrors the TypeScript reference SDK:
1184+
// a server that lists methods without S256 genuinely can't do the flow we
1185+
// require, so it is always refused; a server that omits the field entirely
1186+
// is tolerated by default (the spec says to refuse, but the reference SDKs
1187+
// proceed), and only refused when `require_pkce_support` is opted in.
11641188
match &metadata.code_challenge_methods_supported {
11651189
Some(methods) if !methods.iter().any(|m| m == "S256") => {
1190+
return Err(AuthError::PkceUnsupported);
1191+
}
1192+
None if self.require_pkce_support => {
1193+
return Err(AuthError::PkceUnsupported);
1194+
}
1195+
None => {
11661196
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!"
1197+
"authorization server metadata omits code_challenge_methods_supported; \
1198+
proceeding with an S256 challenge anyway"
11711199
);
11721200
}
1173-
_ => {}
1201+
Some(_) => {}
11741202
}
11751203

11761204
Ok(())
@@ -2849,6 +2877,16 @@ impl OAuthState {
28492877
Ok(OAuthState::Unauthorized(manager))
28502878
}
28512879

2880+
/// Strictly require the authorization server to advertise PKCE support.
2881+
///
2882+
/// Must be called before authorization begins, while the state is still
2883+
/// unauthorized. See [`AuthorizationManager::set_require_pkce_support`].
2884+
pub fn set_require_pkce_support(&mut self, require: bool) {
2885+
if let OAuthState::Unauthorized(manager) = self {
2886+
manager.set_require_pkce_support(require);
2887+
}
2888+
}
2889+
28522890
/// Get client_id and OAuth credentials
28532891
pub async fn get_credentials(&self) -> Result<Credentials, AuthError> {
28542892
// return client_id and credentials
@@ -4307,22 +4345,59 @@ mod tests {
43074345
assert!(manager.validate_server_metadata("code").is_err());
43084346
}
43094347

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 {
4348+
fn as_metadata_with_pkce(methods: Option<Vec<String>>) -> AuthorizationMetadata {
4349+
AuthorizationMetadata {
43164350
authorization_endpoint: "https://auth.example.com/authorize".to_string(),
43174351
token_endpoint: "https://auth.example.com/token".to_string(),
43184352
response_types_supported: Some(vec!["code".to_string()]),
4319-
code_challenge_methods_supported: Some(vec!["plain".to_string()]),
4353+
code_challenge_methods_supported: methods,
43204354
..Default::default()
4321-
};
4322-
manager.set_metadata(metadata);
4355+
}
4356+
}
4357+
4358+
#[tokio::test]
4359+
async fn test_validate_as_metadata_rejects_without_pkce_s256() {
4360+
let mut manager = AuthorizationManager::new("https://example.com")
4361+
.await
4362+
.unwrap();
4363+
manager.set_metadata(as_metadata_with_pkce(Some(vec!["plain".to_string()])));
4364+
assert!(matches!(
4365+
manager.validate_server_metadata("code"),
4366+
Err(AuthError::PkceUnsupported)
4367+
));
4368+
}
4369+
4370+
#[tokio::test]
4371+
async fn test_validate_as_metadata_allows_absent_pkce_methods_by_default() {
4372+
let mut manager = AuthorizationManager::new("https://example.com")
4373+
.await
4374+
.unwrap();
4375+
manager.set_metadata(as_metadata_with_pkce(None));
4376+
assert!(manager.validate_server_metadata("code").is_ok());
4377+
}
4378+
4379+
#[tokio::test]
4380+
async fn test_validate_as_metadata_passes_with_pkce_s256() {
4381+
let mut manager = AuthorizationManager::new("https://example.com")
4382+
.await
4383+
.unwrap();
4384+
manager.set_metadata(as_metadata_with_pkce(Some(vec!["S256".to_string()])));
43234385
assert!(manager.validate_server_metadata("code").is_ok());
43244386
}
43254387

4388+
#[tokio::test]
4389+
async fn test_validate_as_metadata_rejects_absent_pkce_methods_when_strict() {
4390+
let mut manager = AuthorizationManager::new("https://example.com")
4391+
.await
4392+
.unwrap();
4393+
manager.set_require_pkce_support(true);
4394+
manager.set_metadata(as_metadata_with_pkce(None));
4395+
assert!(matches!(
4396+
manager.validate_server_metadata("code"),
4397+
Err(AuthError::PkceUnsupported)
4398+
));
4399+
}
4400+
43264401
#[tokio::test]
43274402
async fn test_validate_as_metadata_passes_without_metadata() {
43284403
let manager = AuthorizationManager::new("https://example.com")

0 commit comments

Comments
 (0)