feat: reject auth servers lacking S256 PKCE support (#955)

This commit is contained in:
Dale Seo 2026-07-05 09:05:09 -04:00 committed by GitHub
parent bdf0c32e8c
commit 95490facd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -477,6 +477,9 @@ pub enum AuthError {
#[error("Metadata error: {0}")] #[error("Metadata error: {0}")]
MetadataError(String), MetadataError(String),
#[error("Authorization server does not support the required PKCE code challenge method (S256)")]
PkceUnsupported,
#[error("URL parse error: {0}")] #[error("URL parse error: {0}")]
UrlError(#[from] url::ParseError), UrlError(#[from] url::ParseError),
@ -1159,18 +1162,21 @@ impl AuthorizationManager {
} }
} }
// for PKCE, we always send s256 since oauth 2.1 requires servers to support it, // The client always sends an S256 challenge. A server that advertises
// but warn if the server metadata suggests otherwise // methods without S256 can't do the flow we require, so refuse it. A
// server that omits the field is tolerated: it usually means the server
// didn't advertise PKCE, not that it lacks S256.
match &metadata.code_challenge_methods_supported { match &metadata.code_challenge_methods_supported {
Some(methods) if !methods.iter().any(|m| m == "S256") => { Some(methods) if !methods.iter().any(|m| m == "S256") => {
return Err(AuthError::PkceUnsupported);
}
None => {
warn!( warn!(
?methods, "authorization server metadata omits code_challenge_methods_supported; \
"server does not advertise S256 in code_challenge_methods_supported, \ proceeding with an S256 challenge anyway"
proceeding with S256 anyway as oauth 2.1 requires it. \
The server is not compliant with the specification!"
); );
} }
_ => {} Some(_) => {}
} }
Ok(()) Ok(())
@ -4307,19 +4313,43 @@ mod tests {
assert!(manager.validate_server_metadata("code").is_err()); assert!(manager.validate_server_metadata("code").is_err());
} }
#[tokio::test] fn as_metadata_with_pkce(methods: Option<Vec<String>>) -> AuthorizationMetadata {
async fn test_validate_as_metadata_passes_without_pkce_s256() { AuthorizationMetadata {
let mut manager = AuthorizationManager::new("https://example.com")
.await
.unwrap();
let metadata = AuthorizationMetadata {
authorization_endpoint: "https://auth.example.com/authorize".to_string(), authorization_endpoint: "https://auth.example.com/authorize".to_string(),
token_endpoint: "https://auth.example.com/token".to_string(), token_endpoint: "https://auth.example.com/token".to_string(),
response_types_supported: Some(vec!["code".to_string()]), response_types_supported: Some(vec!["code".to_string()]),
code_challenge_methods_supported: Some(vec!["plain".to_string()]), code_challenge_methods_supported: methods,
..Default::default() ..Default::default()
}; }
manager.set_metadata(metadata); }
#[tokio::test]
async fn test_validate_as_metadata_rejects_without_pkce_s256() {
let mut manager = AuthorizationManager::new("https://example.com")
.await
.unwrap();
manager.set_metadata(as_metadata_with_pkce(Some(vec!["plain".to_string()])));
assert!(matches!(
manager.validate_server_metadata("code"),
Err(AuthError::PkceUnsupported)
));
}
#[tokio::test]
async fn test_validate_as_metadata_allows_absent_pkce_methods_by_default() {
let mut manager = AuthorizationManager::new("https://example.com")
.await
.unwrap();
manager.set_metadata(as_metadata_with_pkce(None));
assert!(manager.validate_server_metadata("code").is_ok());
}
#[tokio::test]
async fn test_validate_as_metadata_passes_with_pkce_s256() {
let mut manager = AuthorizationManager::new("https://example.com")
.await
.unwrap();
manager.set_metadata(as_metadata_with_pkce(Some(vec!["S256".to_string()])));
assert!(manager.validate_server_metadata("code").is_ok()); assert!(manager.validate_server_metadata("code").is_ok());
} }