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