@@ -13,6 +13,9 @@ namespace ModelContextProtocol.AspNetCore.Authentication;
1313/// </summary>
1414public 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>
@@ -27,65 +30,119 @@ public McpAuthenticationHandler(
2730 /// <inheritdoc />
2831 public async 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 await 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 await 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+ Port = Request . Host . Port ?? ( Request . Scheme == "https" ? 443 : 80 ) ,
62+ } ;
63+
64+ return await HandleResourceMetadataRequestAsync ( deriveResourceUriBuilder . Uri ) ;
65+ }
5366
5467 /// <summary>
5568 /// Gets the absolute URI for the resource metadata endpoint.
5669 /// </summary>
5770 private string GetAbsoluteResourceMetadataUri ( )
5871 {
59- var resourceMetadataUri = Options . ResourceMetadataUri ;
72+ if ( Options . ResourceMetadataUri is Uri resourceMetadataUri )
73+ {
74+ if ( resourceMetadataUri . IsAbsoluteUri )
75+ {
76+ return resourceMetadataUri . ToString ( ) ;
77+ }
78+
79+ var seperator = resourceMetadataUri . OriginalString . StartsWith ( "/" ) ? "" : "/" ;
80+ return $ "{ Request . Scheme } ://{ Request . Host . ToUriComponent ( ) } { Request . PathBase } { seperator } { resourceMetadataUri . OriginalString } ";
81+ }
82+
83+ return $ "{ Request . Scheme } ://{ Request . Host . ToUriComponent ( ) } { Request . PathBase } { DefaultResourceMetadataPath } { Request . Path } ";
84+ }
85+
86+ private bool IsConfiguredEndpointRequest ( Uri resourceMetadataUri )
87+ {
88+ var expectedPath = GetConfiguredResourceMetadataPath ( resourceMetadataUri ) ;
6089
61- string currentPath = resourceMetadataUri ? . ToString ( ) ?? string . Empty ;
90+ if ( ! string . Equals ( Request . Path . Value , expectedPath , StringComparison . OrdinalIgnoreCase ) )
91+ {
92+ return false ;
93+ }
6294
63- if ( resourceMetadataUri != null && resourceMetadataUri . IsAbsoluteUri )
95+ if ( ! resourceMetadataUri . IsAbsoluteUri )
6496 {
65- return currentPath ;
97+ return true ;
6698 }
6799
68- // For relative URIs, combine with the base URL
69- string baseUrl = GetBaseUrl ( ) ;
70- string relativePath = resourceMetadataUri ? . OriginalString . TrimStart ( '/' ) ?? string . Empty ;
100+ if ( ! Request . Host . HasValue )
101+ {
102+ return false ;
103+ }
71104
72- if ( ! Uri . TryCreate ( $ " { baseUrl . TrimEnd ( '/' ) } / { relativePath } " , UriKind . Absolute , out var absoluteUri ) )
105+ if ( ! string . Equals ( Request . Host . Host , resourceMetadataUri . Host , StringComparison . OrdinalIgnoreCase ) )
73106 {
74- throw new InvalidOperationException ( $ "Could not create absolute URI for resource metadata. Base URL: { baseUrl } , Relative Path: { relativePath } ") ;
107+ Logger . LogWarning (
108+ "Resource metadata request host '{RequestHost}' did not match configured host '{ConfiguredHost}'." ,
109+ Request . Host . Value ,
110+ resourceMetadataUri . Host ) ;
111+ return false ;
75112 }
76113
77- return absoluteUri . ToString ( ) ;
114+ if ( ! string . Equals ( Request . Scheme , resourceMetadataUri . Scheme , StringComparison . OrdinalIgnoreCase ) )
115+ {
116+ Logger . LogWarning (
117+ "Resource metadata request scheme '{RequestScheme}' did not match configured scheme '{ConfiguredScheme}'." ,
118+ Request . Scheme ,
119+ resourceMetadataUri . Scheme ) ;
120+ return false ;
121+ }
122+
123+ return true ;
78124 }
79125
80- private async Task < bool > HandleResourceMetadataRequestAsync ( )
126+ private static string GetConfiguredResourceMetadataPath ( Uri resourceMetadataUri )
81127 {
82- var resourceMetadata = Options . ResourceMetadata ;
128+ if ( resourceMetadataUri . IsAbsoluteUri )
129+ {
130+ return resourceMetadataUri . AbsolutePath ;
131+ }
132+
133+ var path = resourceMetadataUri . OriginalString ;
134+ return path . StartsWith ( "/" ) ? path : $ "/{ path } ";
135+ }
136+
137+ private async Task < bool > HandleResourceMetadataRequestAsync ( Uri ? derivedResourceUri = null )
138+ {
139+ var resourceMetadata = CloneResourceMetadata ( Options . ResourceMetadata , derivedResourceUri ) ;
83140
84141 if ( Options . Events . OnResourceMetadataRequest is not null )
85142 {
86143 var context = new ResourceMetadataRequestContext ( Request . HttpContext , Scheme , Options )
87144 {
88- ResourceMetadata = CloneResourceMetadata ( resourceMetadata ) ,
145+ ResourceMetadata = resourceMetadata ,
89146 } ;
90147
91148 await Options . Events . OnResourceMetadataRequest ( context ) ;
@@ -109,13 +166,13 @@ private async Task<bool> HandleResourceMetadataRequestAsync()
109166 resourceMetadata = context . ResourceMetadata ;
110167 }
111168
112- if ( resourceMetadata == null )
169+ if ( resourceMetadata 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 has not been configured. Please set McpAuthenticationOptions.ResourceMetadata or ensure context.ResourceMetadata is set inside McpAuthenticationOptions.Events.OnResourceMetadataRequest." ) ;
117172 }
118173
174+ resourceMetadata . Resource ??= derivedResourceUri ;
175+
119176 await Results . Json ( resourceMetadata , McpJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( ProtectedResourceMetadata ) ) ) . ExecuteAsync ( Context ) ;
120177 return true ;
121178 }
@@ -142,7 +199,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
142199 return base . HandleChallengeAsync ( properties ) ;
143200 }
144201
145- internal static ProtectedResourceMetadata ? CloneResourceMetadata ( ProtectedResourceMetadata ? resourceMetadata )
202+ internal static ProtectedResourceMetadata ? CloneResourceMetadata ( ProtectedResourceMetadata ? resourceMetadata , Uri ? derivedResourceUri = null )
146203 {
147204 if ( resourceMetadata is null )
148205 {
@@ -151,7 +208,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
151208
152209 return new ProtectedResourceMetadata
153210 {
154- Resource = resourceMetadata . Resource ,
211+ Resource = resourceMetadata . Resource ?? derivedResourceUri ,
155212 AuthorizationServers = [ .. resourceMetadata . AuthorizationServers ] ,
156213 BearerMethodsSupported = [ .. resourceMetadata . BearerMethodsSupported ] ,
157214 ScopesSupported = [ .. resourceMetadata . ScopesSupported ] ,
@@ -167,5 +224,4 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
167224 DpopBoundAccessTokensRequired = resourceMetadata . DpopBoundAccessTokensRequired
168225 } ;
169226 }
170-
171227}
0 commit comments