@@ -11,8 +11,11 @@ namespace ModelContextProtocol.AspNetCore.Authentication;
1111/// Represents an authentication handler for MCP protocol that adds resource metadata to challenge responses
1212/// and handles resource metadata endpoint requests.
1313/// </summary>
14- public class McpAuthenticationHandler : AuthenticationHandler < McpAuthenticationOptions > , IAuthenticationRequestHandler
14+ public partial class McpAuthenticationHandler : AuthenticationHandler < McpAuthenticationOptions > , IAuthenticationRequestHandler
1515{
16+ private const string DefaultResourceMetadataPath = "/.well-known/oauth-protected-resource" ;
17+ private static readonly PathString DefaultResourceMetadataPrefix = new ( DefaultResourceMetadataPath ) ;
18+
1619 /// <summary>
1720 /// Initializes a new instance of the <see cref="McpAuthenticationHandler"/> class.
1821 /// </summary>
@@ -25,67 +28,114 @@ public McpAuthenticationHandler(
2528 }
2629
2730 /// <inheritdoc />
28- public async Task < bool > HandleRequestAsync ( )
31+ public Task < bool > HandleRequestAsync ( )
2932 {
30- // Check if the request is for the resource metadata endpoint
31- string requestPath = Request . Path . Value ?? string . Empty ;
32-
33- string expectedMetadataPath = Options . ResourceMetadataUri ? . ToString ( ) ?? string . Empty ;
34- if ( Options . ResourceMetadataUri != null && ! Options . ResourceMetadataUri . IsAbsoluteUri )
33+ if ( Options . ResourceMetadataUri is Uri configuredUri )
3534 {
36- // For relative URIs, it's just the path component.
37- expectedMetadataPath = Options . ResourceMetadataUri . OriginalString ;
35+ return HandleConfiguredResourceMetadataRequestAsync ( configuredUri ) ;
3836 }
3937
40- // If the path doesn't match, let the request continue through the pipeline
41- if ( ! string . Equals ( requestPath , expectedMetadataPath , StringComparison . OrdinalIgnoreCase ) )
38+ return HandleDefaultResourceMetadataRequestAsync ( ) ;
39+ }
40+
41+ private async Task < bool > HandleConfiguredResourceMetadataRequestAsync ( Uri resourceMetadataUri )
42+ {
43+ if ( ! IsConfiguredEndpointRequest ( resourceMetadataUri ) )
4244 {
4345 return false ;
4446 }
4547
4648 return await HandleResourceMetadataRequestAsync ( ) ;
4749 }
4850
49- /// <summary>
50- /// Gets the base URL from the current request, including scheme, host, and path base.
51- /// </summary>
52- private string GetBaseUrl ( ) => $ "{ Request . Scheme } ://{ Request . Host } { Request . PathBase } ";
51+ private async Task < bool > HandleDefaultResourceMetadataRequestAsync ( )
52+ {
53+ if ( ! Request . Path . StartsWithSegments ( DefaultResourceMetadataPrefix , out var resourceSuffix ) )
54+ {
55+ return false ;
56+ }
57+
58+ var deriveResourceUriBuilder = new UriBuilder ( Request . Scheme , Request . Host . Host )
59+ {
60+ Path = $ "{ Request . PathBase } { resourceSuffix } ",
61+ } ;
62+
63+ if ( Request . Host . Port is not null )
64+ {
65+ deriveResourceUriBuilder . Port = Request . Host . Port . Value ;
66+ }
67+
68+ return await HandleResourceMetadataRequestAsync ( deriveResourceUriBuilder . Uri ) ;
69+ }
5370
5471 /// <summary>
5572 /// Gets the absolute URI for the resource metadata endpoint.
5673 /// </summary>
5774 private string GetAbsoluteResourceMetadataUri ( )
5875 {
59- var resourceMetadataUri = Options . ResourceMetadataUri ;
76+ if ( Options . ResourceMetadataUri is Uri resourceMetadataUri )
77+ {
78+ if ( resourceMetadataUri . IsAbsoluteUri )
79+ {
80+ return resourceMetadataUri . ToString ( ) ;
81+ }
82+
83+ var separator = resourceMetadataUri . OriginalString . StartsWith ( '/' ) ? "" : "/" ;
84+ return $ "{ Request . Scheme } ://{ Request . Host . ToUriComponent ( ) } { Request . PathBase } { separator } { resourceMetadataUri . OriginalString } ";
85+ }
86+
87+ return $ "{ Request . Scheme } ://{ Request . Host . ToUriComponent ( ) } { Request . PathBase } { DefaultResourceMetadataPath } { Request . Path } ";
88+ }
89+
90+ private bool IsConfiguredEndpointRequest ( Uri resourceMetadataUri )
91+ {
92+ var expectedPath = GetConfiguredResourceMetadataPath ( resourceMetadataUri ) ;
93+
94+ if ( ! string . Equals ( Request . Path . Value , expectedPath , StringComparison . OrdinalIgnoreCase ) )
95+ {
96+ return false ;
97+ }
98+
99+ if ( ! resourceMetadataUri . IsAbsoluteUri )
100+ {
101+ return true ;
102+ }
60103
61- string currentPath = resourceMetadataUri ? . ToString ( ) ?? string . Empty ;
104+ if ( ! string . Equals ( Request . Host . Host , resourceMetadataUri . Host , StringComparison . OrdinalIgnoreCase ) )
105+ {
106+ LogResourceMetadataHostMismatch ( Logger , resourceMetadataUri . Host ) ;
107+ return false ;
108+ }
62109
63- if ( resourceMetadataUri != null && resourceMetadataUri . IsAbsoluteUri )
110+ if ( ! string . Equals ( Request . Scheme , resourceMetadataUri . Scheme , StringComparison . OrdinalIgnoreCase ) )
64111 {
65- return currentPath ;
112+ LogResourceMetadataSchemeMismatch ( Logger , resourceMetadataUri . Scheme ) ;
113+ return false ;
66114 }
67115
68- // For relative URIs, combine with the base URL
69- string baseUrl = GetBaseUrl ( ) ;
70- string relativePath = resourceMetadataUri ? . OriginalString . TrimStart ( '/' ) ?? string . Empty ;
116+ return true ;
117+ }
71118
72- if ( ! Uri . TryCreate ( $ "{ baseUrl . TrimEnd ( '/' ) } /{ relativePath } ", UriKind . Absolute , out var absoluteUri ) )
119+ private static string GetConfiguredResourceMetadataPath ( Uri resourceMetadataUri )
120+ {
121+ if ( resourceMetadataUri . IsAbsoluteUri )
73122 {
74- throw new InvalidOperationException ( $ "Could not create absolute URI for resource metadata. Base URL: { baseUrl } , Relative Path: { relativePath } " ) ;
123+ return resourceMetadataUri . AbsolutePath ;
75124 }
76125
77- return absoluteUri . ToString ( ) ;
126+ var path = resourceMetadataUri . OriginalString ;
127+ return path . StartsWith ( '/' ) ? path : $ "/{ path } ";
78128 }
79129
80- private async Task < bool > HandleResourceMetadataRequestAsync ( )
130+ private async Task < bool > HandleResourceMetadataRequestAsync ( Uri ? derivedResourceUri = null )
81131 {
82- var resourceMetadata = Options . ResourceMetadata ;
132+ var resourceMetadata = CloneResourceMetadata ( Options . ResourceMetadata , derivedResourceUri ) ;
83133
84134 if ( Options . Events . OnResourceMetadataRequest is not null )
85135 {
86136 var context = new ResourceMetadataRequestContext ( Request . HttpContext , Scheme , Options )
87137 {
88- ResourceMetadata = CloneResourceMetadata ( resourceMetadata ) ,
138+ ResourceMetadata = resourceMetadata ,
89139 } ;
90140
91141 await Options . Events . OnResourceMetadataRequest ( context ) ;
@@ -109,11 +159,16 @@ private async Task<bool> HandleResourceMetadataRequestAsync()
109159 resourceMetadata = context . ResourceMetadata ;
110160 }
111161
112- if ( resourceMetadata == null )
162+ if ( resourceMetadata is null )
163+ {
164+ throw new InvalidOperationException ( "ResourceMetadata has not been configured. Please set McpAuthenticationOptions.ResourceMetadata or ensure context.ResourceMetadata is set inside McpAuthenticationOptions.Events.OnResourceMetadataRequest." ) ;
165+ }
166+
167+ resourceMetadata . Resource ??= derivedResourceUri ;
168+
169+ if ( resourceMetadata . Resource is null )
113170 {
114- throw new InvalidOperationException (
115- "ResourceMetadata has not been configured. Please set McpAuthenticationOptions.ResourceMetadata or ensure context.ResourceMetadata is set inside McpAuthenticationOptions.Events.OnResourceMetadataRequest."
116- ) ;
171+ throw new InvalidOperationException ( "ResourceMetadata.Resource could not be determined. Please set McpAuthenticationOptions.ResourceMetadata.Resource or avoid setting a custom McpAuthenticationOptions.ResourceMetadataUri." ) ;
117172 }
118173
119174 await Results . Json ( resourceMetadata , McpJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( ProtectedResourceMetadata ) ) ) . ExecuteAsync ( Context ) ;
@@ -142,7 +197,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
142197 return base . HandleChallengeAsync ( properties ) ;
143198 }
144199
145- internal static ProtectedResourceMetadata ? CloneResourceMetadata ( ProtectedResourceMetadata ? resourceMetadata )
200+ internal static ProtectedResourceMetadata ? CloneResourceMetadata ( ProtectedResourceMetadata ? resourceMetadata , Uri ? derivedResourceUri = null )
146201 {
147202 if ( resourceMetadata is null )
148203 {
@@ -151,7 +206,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
151206
152207 return new ProtectedResourceMetadata
153208 {
154- Resource = resourceMetadata . Resource ,
209+ Resource = resourceMetadata . Resource ?? derivedResourceUri ,
155210 AuthorizationServers = [ .. resourceMetadata . AuthorizationServers ] ,
156211 BearerMethodsSupported = [ .. resourceMetadata . BearerMethodsSupported ] ,
157212 ScopesSupported = [ .. resourceMetadata . ScopesSupported ] ,
@@ -168,4 +223,9 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
168223 } ;
169224 }
170225
226+ [ LoggerMessage ( Level = LogLevel . Warning , Message = "Resource metadata request host did not match configured host '{ConfiguredHost}'." ) ]
227+ private static partial void LogResourceMetadataHostMismatch ( ILogger logger , string configuredHost ) ;
228+
229+ [ LoggerMessage ( Level = LogLevel . Warning , Message = "Resource metadata request scheme did not match configured scheme '{ConfiguredScheme}'." ) ]
230+ private static partial void LogResourceMetadataSchemeMismatch ( ILogger logger , string configuredScheme ) ;
171231}
0 commit comments