@@ -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