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