1+ using System . Security . Claims ;
2+ using Microsoft . AspNetCore . Authorization ;
3+ using Microsoft . Extensions . DependencyInjection ;
4+ using Microsoft . Extensions . Options ;
5+ using ModelContextProtocol . Protocol ;
6+ using ModelContextProtocol . Server ;
7+
8+ namespace ModelContextProtocol . AspNetCore ;
9+
10+ /// <summary>
11+ /// Evaluates authorization policies from endpoint metadata.
12+ /// </summary>
13+ internal sealed class AuthorizationFilterSetup ( IAuthorizationPolicyProvider ? policyProvider = null ) : IConfigureOptions < McpServerOptions >
14+ {
15+ public void Configure ( McpServerOptions options )
16+ {
17+ ConfigureListToolsFilter ( options ) ;
18+ ConfigureCallToolFilter ( options ) ;
19+
20+ ConfigureListResourcesFilter ( options ) ;
21+ ConfigureListResourceTemplatesFilter ( options ) ;
22+ ConfigureReadResourceFilter ( options ) ;
23+
24+ ConfigureListPromptsFilter ( options ) ;
25+ ConfigureGetPromptFilter ( options ) ;
26+ }
27+
28+ private void ConfigureListToolsFilter ( McpServerOptions options )
29+ {
30+ options . Filters . ListToolsFilters . Add ( next => async ( context , cancellationToken ) =>
31+ {
32+ var result = await next ( context , cancellationToken ) ;
33+ await FilterAuthorizedItemsAsync (
34+ result . Tools , static tool => tool . McpServerTool ,
35+ context . User , context . Services , context ) ;
36+ return result ;
37+ } ) ;
38+ }
39+
40+ private void ConfigureCallToolFilter ( McpServerOptions options )
41+ {
42+ options . Filters . CallToolFilters . Add ( next => async ( context , cancellationToken ) =>
43+ {
44+ var authResult = await GetAuthorizationResultAsync ( context . User , context . MatchedPrimitive , context . Services , context ) ;
45+ if ( ! authResult . Succeeded )
46+ {
47+ return new CallToolResult
48+ {
49+ Content = [ new TextContentBlock { Text = "Access forbidden: This tool requires authorization." } ] ,
50+ IsError = true
51+ } ;
52+ }
53+
54+ return await next ( context , cancellationToken ) ;
55+ } ) ;
56+ }
57+
58+ private void ConfigureListResourcesFilter ( McpServerOptions options )
59+ {
60+ options . Filters . ListResourcesFilters . Add ( next => async ( context , cancellationToken ) =>
61+ {
62+ var result = await next ( context , cancellationToken ) ;
63+ await FilterAuthorizedItemsAsync (
64+ result . Resources , static resource => resource . McpServerResource ,
65+ context . User , context . Services , context ) ;
66+ return result ;
67+ } ) ;
68+ }
69+
70+ private void ConfigureListResourceTemplatesFilter ( McpServerOptions options )
71+ {
72+ options . Filters . ListResourceTemplatesFilters . Add ( next => async ( context , cancellationToken ) =>
73+ {
74+ var result = await next ( context , cancellationToken ) ;
75+ await FilterAuthorizedItemsAsync (
76+ result . ResourceTemplates , static resourceTemplate => resourceTemplate . McpServerResource ,
77+ context . User , context . Services , context ) ;
78+ return result ;
79+ } ) ;
80+ }
81+
82+ private void ConfigureReadResourceFilter ( McpServerOptions options )
83+ {
84+ options . Filters . ReadResourceFilters . Add ( next => async ( context , cancellationToken ) =>
85+ {
86+ var authResult = await GetAuthorizationResultAsync ( context . User , context . MatchedPrimitive , context . Services , context ) ;
87+ if ( ! authResult . Succeeded )
88+ {
89+ throw new McpException ( "Access forbidden: This resource requires authorization." , McpErrorCode . InvalidRequest ) ;
90+ }
91+
92+ return await next ( context , cancellationToken ) ;
93+ } ) ;
94+ }
95+
96+ private void ConfigureListPromptsFilter ( McpServerOptions options )
97+ {
98+ options . Filters . ListPromptsFilters . Add ( next => async ( context , cancellationToken ) =>
99+ {
100+ var result = await next ( context , cancellationToken ) ;
101+ await FilterAuthorizedItemsAsync (
102+ result . Prompts , static prompt => prompt . McpServerPrompt ,
103+ context . User , context . Services , context ) ;
104+ return result ;
105+ } ) ;
106+ }
107+
108+ private void ConfigureGetPromptFilter ( McpServerOptions options )
109+ {
110+ options . Filters . GetPromptFilters . Add ( next => async ( context , cancellationToken ) =>
111+ {
112+ var authResult = await GetAuthorizationResultAsync ( context . User , context . MatchedPrimitive , context . Services , context ) ;
113+ if ( ! authResult . Succeeded )
114+ {
115+ throw new McpException ( "Access forbidden: This prompt requires authorization." , McpErrorCode . InvalidRequest ) ;
116+ }
117+
118+ return await next ( context , cancellationToken ) ;
119+ } ) ;
120+ }
121+
122+ /// <summary>
123+ /// Filters a collection of items based on authorization policies in their metadata.
124+ /// For list operations where we need to filter results by authorization.
125+ /// </summary>
126+ private async ValueTask FilterAuthorizedItemsAsync < T > ( IList < T > items , Func < T , IMcpServerPrimitive ? > primitiveSelector ,
127+ ClaimsPrincipal ? user , IServiceProvider ? requestServices , object context )
128+ {
129+ for ( int i = items . Count - 1 ; i >= 0 ; i -- )
130+ {
131+ var authorizationResult = await GetAuthorizationResultAsync (
132+ user , primitiveSelector ( items [ i ] ) , requestServices , context ) ;
133+
134+ if ( ! authorizationResult . Succeeded )
135+ {
136+ items . RemoveAt ( i ) ;
137+ }
138+ }
139+ }
140+
141+ private async ValueTask < AuthorizationResult > GetAuthorizationResultAsync (
142+ ClaimsPrincipal ? user , IMcpServerPrimitive ? primitive , IServiceProvider ? requestServices , object context )
143+ {
144+ // If no primitive was found for this request or there is IAllowAnonymous metadata anywhere on the class or method,
145+ // the request should go through as normal.
146+ if ( primitive is null || primitive . Metadata . Any ( static m => m is IAllowAnonymous ) )
147+ {
148+ return AuthorizationResult . Success ( ) ;
149+ }
150+
151+ // There are no [Authorize] style attributes applied to the method or containing class. Any fallback policies
152+ // have already been enforced at the HTTP request level by the ASP.NET Core authorization middleware.
153+ if ( ! primitive . Metadata . Any ( static m => m is IAuthorizeData or AuthorizationPolicy or IAuthorizationRequirementData ) )
154+ {
155+ return AuthorizationResult . Success ( ) ;
156+ }
157+
158+ if ( policyProvider is null )
159+ {
160+ throw new InvalidOperationException ( $ "You must call AddAuthorization() because an authorization related attribute was found on { primitive . Id } ") ;
161+ }
162+
163+ // TODO: Cache policy lookup. We would probably use a singleton (not-static) ConditionalWeakTable<IMcpServerPrimitive, AuthorizationPolicy?>.
164+ var policy = await CombineAsync ( policyProvider , primitive . Metadata ) ;
165+ if ( policy is null )
166+ {
167+ return AuthorizationResult . Success ( ) ;
168+ }
169+
170+ if ( requestServices is null )
171+ {
172+ // The IAuthorizationPolicyProvider service must be non-null to get to this line, so it's very unexpected for RequestContext.Services to not be set.
173+ throw new InvalidOperationException ( "RequestContext.Services is not set! The IMcpServer must be initialized with a non-null IServiceProvider." ) ;
174+ }
175+
176+ // ASP.NET Core's AuthorizationMiddleware resolves the IAuthorizationService from scoped request services, so we do the same.
177+ var authService = requestServices . GetRequiredService < IAuthorizationService > ( ) ;
178+ return await authService . AuthorizeAsync ( user ?? new ClaimsPrincipal ( new ClaimsIdentity ( ) ) , context , policy ) ;
179+ }
180+
181+ /// <summary>
182+ /// Combines authorization policies and requirements from endpoint metadata without considering <see cref="IAllowAnonymous"/>.
183+ /// </summary>
184+ /// <param name="policyProvider">The authorization policy provider.</param>
185+ /// <param name="endpointMetadata">The endpoint metadata collection.</param>
186+ /// <returns>The combined authorization policy, or null if no authorization is required.</returns>
187+ private static async ValueTask < AuthorizationPolicy ? > CombineAsync ( IAuthorizationPolicyProvider policyProvider , IReadOnlyList < object > endpointMetadata )
188+ {
189+ // https://github.com/dotnet/aspnetcore/issues/63365 tracks adding this as public API to AuthorizationPolicy itself.
190+ // Copied from https://github.com/dotnet/aspnetcore/blob/9f2977bf9cfb539820983bda3bedf81c8cda9f20/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs#L116-L138
191+ var authorizeData = endpointMetadata . OfType < IAuthorizeData > ( ) ;
192+ var policies = endpointMetadata . OfType < AuthorizationPolicy > ( ) ;
193+
194+ var policy = await AuthorizationPolicy . CombineAsync ( policyProvider , authorizeData , policies ) ;
195+
196+ AuthorizationPolicyBuilder ? reqPolicyBuilder = null ;
197+
198+ foreach ( var m in endpointMetadata )
199+ {
200+ if ( m is not IAuthorizationRequirementData requirementData )
201+ {
202+ continue ;
203+ }
204+
205+ reqPolicyBuilder ??= new AuthorizationPolicyBuilder ( ) ;
206+ foreach ( var requirement in requirementData . GetRequirements ( ) )
207+ {
208+ reqPolicyBuilder . AddRequirements ( requirement ) ;
209+ }
210+ }
211+
212+ if ( reqPolicyBuilder is null )
213+ {
214+ return policy ;
215+ }
216+
217+ // Combine policy with requirements or just use requirements if no policy
218+ return ( policy is null )
219+ ? reqPolicyBuilder . Build ( )
220+ : AuthorizationPolicy . Combine ( policy , reqPolicyBuilder . Build ( ) ) ;
221+ }
222+ }
0 commit comments