@@ -50,6 +50,32 @@ public async Task CanAuthenticate()
5050 transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
5151 }
5252
53+ [ Theory ]
54+ [ InlineData ( "client_secret_basic" ) ]
55+ [ InlineData ( "client_secret_post" ) ]
56+ public async Task CanAuthenticate_WithExplicitTokenEndpointAuthMethod ( string tokenEndpointAuthMethod )
57+ {
58+ await using var app = await StartMcpServerAsync ( ) ;
59+
60+ await using var transport = new HttpClientTransport ( new ( )
61+ {
62+ Endpoint = new ( McpServerUrl ) ,
63+ OAuth = new ( )
64+ {
65+ ClientId = "demo-client" ,
66+ ClientSecret = "demo-secret" ,
67+ TokenEndpointAuthMethod = tokenEndpointAuthMethod ,
68+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
69+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
70+ } ,
71+ } , HttpClient , LoggerFactory ) ;
72+
73+ await using var client = await McpClient . CreateAsync (
74+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
75+
76+ Assert . Equal ( tokenEndpointAuthMethod , TestOAuthServer . LastTokenEndpointAuthMethod ) ;
77+ }
78+
5379 [ Fact ]
5480 public async Task AuthorizationCallbackHandler_ReceivesConfiguredRedirectUri ( )
5581 {
@@ -176,6 +202,28 @@ public void HttpClientTransport_RejectsBothAuthorizationCallbacks()
176202#pragma warning restore MCP9007
177203 }
178204
205+ [ Theory ]
206+ [ InlineData ( "" ) ]
207+ [ InlineData ( "None" ) ]
208+ [ InlineData ( "private_key_jwt" ) ]
209+ public void HttpClientTransport_RejectsUnsupportedTokenEndpointAuthMethod ( string tokenEndpointAuthMethod )
210+ {
211+ var ex = Assert . Throws < ArgumentException > ( ( ) => new HttpClientTransport (
212+ new ( )
213+ {
214+ Endpoint = new ( McpServerUrl ) ,
215+ OAuth = new ( )
216+ {
217+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
218+ TokenEndpointAuthMethod = tokenEndpointAuthMethod ,
219+ } ,
220+ } ,
221+ HttpClient ,
222+ LoggerFactory ) ) ;
223+
224+ Assert . Equal ( "options.TokenEndpointAuthMethod" , ex . ParamName ) ;
225+ }
226+
179227 [ Fact ]
180228 public async Task CanAuthenticate_WhenAuthorizationResponseStateMatches ( )
181229 {
@@ -309,6 +357,80 @@ public async Task CanAuthenticate_WithDynamicClientRegistration()
309357 transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
310358
311359 Assert . Equal ( "native" , TestOAuthServer . LastApplicationType ) ;
360+ Assert . Equal ( "client_secret_post" , TestOAuthServer . LastRegistrationTokenEndpointAuthMethod ) ;
361+ }
362+
363+ [ Fact ]
364+ public async Task DynamicClientRegistration_ResponseTokenEndpointAuthMethodIsAuthoritative ( )
365+ {
366+ TestOAuthServer . DynamicRegistrationTokenEndpointAuthMethod = "client_secret_post" ;
367+ await using var app = await StartMcpServerAsync ( ) ;
368+
369+ await using var transport = new HttpClientTransport ( new ( )
370+ {
371+ Endpoint = new ( McpServerUrl ) ,
372+ OAuth = new ( )
373+ {
374+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
375+ TokenEndpointAuthMethod = "client_secret_basic" ,
376+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
377+ DynamicClientRegistration = new ( ) ,
378+ } ,
379+ } , HttpClient , LoggerFactory ) ;
380+
381+ await using var client = await McpClient . CreateAsync (
382+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
383+
384+ Assert . Equal ( "client_secret_basic" , TestOAuthServer . LastRegistrationTokenEndpointAuthMethod ) ;
385+ Assert . Equal ( "client_secret_post" , TestOAuthServer . LastTokenEndpointAuthMethod ) ;
386+ }
387+
388+ [ Fact ]
389+ public async Task DynamicClientRegistration_UsesRequestedMethodWhenResponseOmitsIt ( )
390+ {
391+ TestOAuthServer . DynamicRegistrationTokenEndpointAuthMethod = null ;
392+ await using var app = await StartMcpServerAsync ( ) ;
393+
394+ await using var transport = new HttpClientTransport ( new ( )
395+ {
396+ Endpoint = new ( McpServerUrl ) ,
397+ OAuth = new ( )
398+ {
399+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
400+ TokenEndpointAuthMethod = "client_secret_basic" ,
401+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
402+ DynamicClientRegistration = new ( ) ,
403+ } ,
404+ } , HttpClient , LoggerFactory ) ;
405+
406+ await using var client = await McpClient . CreateAsync (
407+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
408+
409+ Assert . Equal ( "client_secret_basic" , TestOAuthServer . LastRegistrationTokenEndpointAuthMethod ) ;
410+ Assert . Equal ( "client_secret_basic" , TestOAuthServer . LastTokenEndpointAuthMethod ) ;
411+ }
412+
413+ [ Fact ]
414+ public async Task DynamicClientRegistration_RejectsUnsupportedResponseTokenEndpointAuthMethod ( )
415+ {
416+ TestOAuthServer . DynamicRegistrationTokenEndpointAuthMethod = "private_key_jwt" ;
417+ await using var app = await StartMcpServerAsync ( ) ;
418+
419+ await using var transport = new HttpClientTransport ( new ( )
420+ {
421+ Endpoint = new ( McpServerUrl ) ,
422+ OAuth = new ( )
423+ {
424+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
425+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
426+ DynamicClientRegistration = new ( ) ,
427+ } ,
428+ } , HttpClient , LoggerFactory ) ;
429+
430+ var ex = await Assert . ThrowsAsync < McpException > ( ( ) => McpClient . CreateAsync (
431+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ) ;
432+
433+ Assert . Contains ( "private_key_jwt" , ex . Message ) ;
312434 }
313435
314436 [ Fact ]
@@ -457,7 +579,7 @@ public async Task CannotAuthenticate_WithClientMetadataDocument_WhenServerAdvert
457579 OAuth = new ClientOAuthOptions ( )
458580 {
459581 RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
460- AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync ,
582+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
461583 ClientMetadataDocumentUri = new Uri ( ClientMetadataDocumentUrl ) ,
462584 } ,
463585 } , HttpClient , LoggerFactory ) ;
@@ -482,7 +604,7 @@ public async Task CanAuthenticate_WithClientMetadataDocument_AndExplicitNoneAuth
482604 OAuth = new ClientOAuthOptions ( )
483605 {
484606 RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
485- AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync ,
607+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
486608 ClientMetadataDocumentUri = new Uri ( ClientMetadataDocumentUrl ) ,
487609 TokenEndpointAuthMethod = "none" ,
488610 } ,
@@ -492,6 +614,77 @@ public async Task CanAuthenticate_WithClientMetadataDocument_AndExplicitNoneAuth
492614 transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
493615 }
494616
617+ [ Fact ]
618+ public async Task ClientMetadataDocument_PreservesConfiguredMethodAfterAuthorizationServerMigration ( )
619+ {
620+ TestOAuthServer . SupportedTokenEndpointAuthMethods = [ "client_secret_basic" , "none" ] ;
621+ var selectedAuthorizationServer = OAuthServerUrl ;
622+ var migrationChallengeSent = 0 ;
623+
624+ Builder . Services . Configure < McpAuthenticationOptions > ( McpAuthenticationDefaults . AuthenticationScheme , options =>
625+ {
626+ options . Events . OnResourceMetadataRequest = async context =>
627+ {
628+ context . HandleResponse ( ) ;
629+ var metadata = new ProtectedResourceMetadata
630+ {
631+ Resource = McpServerUrl ,
632+ AuthorizationServers = { selectedAuthorizationServer } ,
633+ ScopesSupported = [ "mcp:tools" ] ,
634+ } ;
635+ await Results . Json ( metadata , McpJsonUtilities . DefaultOptions ) . ExecuteAsync ( context . HttpContext ) ;
636+ } ;
637+ } ) ;
638+
639+ await using var app = await StartMcpServerAsync ( configureMiddleware : app =>
640+ {
641+ app . Use ( async ( context , next ) =>
642+ {
643+ if ( context . Request . Method == HttpMethods . Post && context . Request . Path == "/" )
644+ {
645+ context . Request . EnableBuffering ( ) ;
646+ var message = await JsonSerializer . DeserializeAsync (
647+ context . Request . Body ,
648+ McpJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( JsonRpcMessage ) ) ,
649+ context . RequestAborted ) as JsonRpcMessage ;
650+ context . Request . Body . Position = 0 ;
651+
652+ if ( message is JsonRpcRequest { Method : "ping" } &&
653+ Interlocked . CompareExchange ( ref migrationChallengeSent , 1 , 0 ) == 0 )
654+ {
655+ selectedAuthorizationServer = $ "{ OAuthServerUrl } /tenant";
656+ context . Response . StatusCode = StatusCodes . Status401Unauthorized ;
657+ context . Response . Headers . WWWAuthenticate =
658+ $ "{ JwtBearerDefaults . AuthenticationScheme } resource_metadata=\" { McpServerUrl } /.well-known/oauth-protected-resource\" ";
659+ return ;
660+ }
661+ }
662+
663+ await next ( context ) ;
664+ } ) ;
665+ } ) ;
666+
667+ await using var transport = new HttpClientTransport ( new ( )
668+ {
669+ Endpoint = new ( McpServerUrl ) ,
670+ OAuth = new ( )
671+ {
672+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
673+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync ,
674+ ClientMetadataDocumentUri = new Uri ( ClientMetadataDocumentUrl ) ,
675+ TokenEndpointAuthMethod = "none" ,
676+ } ,
677+ } , HttpClient , LoggerFactory ) ;
678+
679+ await using var client = await McpClient . CreateAsync (
680+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
681+
682+ await client . PingAsync ( cancellationToken : TestContext . Current . CancellationToken ) ;
683+
684+ Assert . Equal ( "none" , TestOAuthServer . LastTokenEndpointAuthMethod ) ;
685+ Assert . Contains ( "/.well-known/oauth-authorization-server/tenant" , TestOAuthServer . MetadataRequests ) ;
686+ }
687+
495688 [ Fact ]
496689 public async Task UsesDynamicClientRegistration_WhenCimdNotSupported ( )
497690 {
0 commit comments