Skip to content

Commit d29040f

Browse files
committed
Add AddAuthorizationFilters
1 parent 9b5786d commit d29040f

5 files changed

Lines changed: 410 additions & 17 deletions

File tree

docs/concepts/filters.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,20 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
121121

122122
## Built-in Authorization Filters
123123

124-
When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), authorization filters are automatically configured to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources.
124+
When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), you can add authorization filters to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources by calling `AddAuthorizationFilters()` on your MCP server builder.
125+
126+
### Enabling Authorization Filters
127+
128+
To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server:
129+
130+
```csharp
131+
services.AddMcpServer()
132+
.WithHttpTransport()
133+
.AddAuthorizationFilters() // Enable authorization filter support
134+
.WithTools<WeatherTools>();
135+
```
136+
137+
**Important**: You should always call `AddAuthorizationFilters()` when using ASP.NET Core integration if you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources.
125138

126139
### Authorization Attributes Support
127140

@@ -200,9 +213,45 @@ For individual operations, the filters return authorization errors when access i
200213
- **Prompts**: Throws an `McpException` with "Access forbidden" message
201214
- **Resources**: Throws an `McpException` with "Access forbidden" message
202215

216+
### Filter Execution Order and Authorization
217+
218+
Authorization filters are applied automatically when you call `AddAuthorizationFilters()`. These filters run at a specific point in the filter pipeline, which means:
219+
220+
**Filters added before authorization filters** can see:
221+
- Unauthorized requests for operations before they are rejected by the authorization filters
222+
- Complete listings for unauthorized primitives before they are filtered out by the authorization filters
223+
224+
**Filters added after authorization filters** will only see:
225+
- Authorized requests that passed authorization checks
226+
- Filtered listings containing only authorized primitives
227+
228+
This allows you to implement logging, metrics, or other cross-cutting concerns that need to see all requests, while still maintaining proper authorization:
229+
230+
```csharp
231+
services.AddMcpServer()
232+
.WithHttpTransport()
233+
.AddListToolsFilter(next => async (context, cancellationToken) =>
234+
{
235+
// This filter runs BEFORE authorization - sees all tools
236+
Console.WriteLine("Request for tools list - will see all tools");
237+
var result = await next(context, cancellationToken);
238+
Console.WriteLine($"Returning {result.Tools?.Count ?? 0} tools after authorization");
239+
return result;
240+
})
241+
.AddAuthorizationFilters() // Authorization filtering happens here
242+
.AddListToolsFilter(next => async (context, cancellationToken) =>
243+
{
244+
// This filter runs AFTER authorization - only sees authorized tools
245+
var result = await next(context, cancellationToken);
246+
Console.WriteLine($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools");
247+
return result;
248+
})
249+
.WithTools<WeatherTools>();
250+
```
251+
203252
### Setup Requirements
204253

205-
To use authorization features, you must configure authentication and authorization in your ASP.NET Core application:
254+
To use authorization features, you must configure authentication and authorization in your ASP.NET Core application and call `AddAuthorizationFilters()`:
206255

207256
```csharp
208257
var builder = WebApplication.CreateBuilder(args);
@@ -214,6 +263,7 @@ builder.Services.AddAuthorization();
214263

215264
builder.Services.AddMcpServer()
216265
.WithHttpTransport()
266+
.AddAuthorizationFilters() // Required for authorization support
217267
.WithTools<WeatherTools>()
218268
.AddCallToolFilter(next => async (context, cancellationToken) =>
219269
{

src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Security.Claims;
23
using Microsoft.AspNetCore.Authorization;
34
using Microsoft.Extensions.DependencyInjection;
@@ -10,8 +11,10 @@ namespace ModelContextProtocol.AspNetCore;
1011
/// <summary>
1112
/// Evaluates authorization policies from endpoint metadata.
1213
/// </summary>
13-
internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? policyProvider = null) : IConfigureOptions<McpServerOptions>
14+
internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? policyProvider = null) : IConfigureOptions<McpServerOptions>, IPostConfigureOptions<McpServerOptions>
1415
{
16+
private static readonly string AuthorizationFilterInvokedKey = "ModelContextProtocol.AspNetCore.AuthorizationFilter.Invoked";
17+
1518
public void Configure(McpServerOptions options)
1619
{
1720
ConfigureListToolsFilter(options);
@@ -25,10 +28,25 @@ public void Configure(McpServerOptions options)
2528
ConfigureGetPromptFilter(options);
2629
}
2730

31+
public void PostConfigure(string? name, McpServerOptions options)
32+
{
33+
CheckListToolsFilter(options);
34+
CheckCallToolFilter(options);
35+
36+
CheckListResourcesFilter(options);
37+
CheckListResourceTemplatesFilter(options);
38+
CheckReadResourceFilter(options);
39+
40+
CheckListPromptsFilter(options);
41+
CheckGetPromptFilter(options);
42+
}
43+
2844
private void ConfigureListToolsFilter(McpServerOptions options)
2945
{
3046
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
3147
{
48+
context.Items[AuthorizationFilterInvokedKey] = true;
49+
3250
var result = await next(context, cancellationToken);
3351
await FilterAuthorizedItemsAsync(
3452
result.Tools, static tool => tool.McpServerTool,
@@ -37,6 +55,22 @@ await FilterAuthorizedItemsAsync(
3755
});
3856
}
3957

58+
private void CheckListToolsFilter(McpServerOptions options)
59+
{
60+
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
61+
{
62+
var result = await next(context, cancellationToken);
63+
64+
if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool))
65+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
66+
{
67+
throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
68+
}
69+
70+
return result;
71+
});
72+
}
73+
4074
private void ConfigureCallToolFilter(McpServerOptions options)
4175
{
4276
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
@@ -51,6 +85,22 @@ private void ConfigureCallToolFilter(McpServerOptions options)
5185
};
5286
}
5387

88+
context.Items[AuthorizationFilterInvokedKey] = true;
89+
90+
return await next(context, cancellationToken);
91+
});
92+
}
93+
94+
private void CheckCallToolFilter(McpServerOptions options)
95+
{
96+
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
97+
{
98+
if (HasAuthorizationMetadata(context.MatchedPrimitive)
99+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
100+
{
101+
throw new InvalidOperationException("Authorization filter was not invoked for tools/call operation, but authorization metadata was found on the tool. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
102+
}
103+
54104
return await next(context, cancellationToken);
55105
});
56106
}
@@ -59,6 +109,8 @@ private void ConfigureListResourcesFilter(McpServerOptions options)
59109
{
60110
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
61111
{
112+
context.Items[AuthorizationFilterInvokedKey] = true;
113+
62114
var result = await next(context, cancellationToken);
63115
await FilterAuthorizedItemsAsync(
64116
result.Resources, static resource => resource.McpServerResource,
@@ -67,10 +119,28 @@ await FilterAuthorizedItemsAsync(
67119
});
68120
}
69121

122+
private void CheckListResourcesFilter(McpServerOptions options)
123+
{
124+
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
125+
{
126+
var result = await next(context, cancellationToken);
127+
128+
if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource))
129+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
130+
{
131+
throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
132+
}
133+
134+
return result;
135+
});
136+
}
137+
70138
private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
71139
{
72140
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
73141
{
142+
context.Items[AuthorizationFilterInvokedKey] = true;
143+
74144
var result = await next(context, cancellationToken);
75145
await FilterAuthorizedItemsAsync(
76146
result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource,
@@ -79,10 +149,28 @@ await FilterAuthorizedItemsAsync(
79149
});
80150
}
81151

152+
private void CheckListResourceTemplatesFilter(McpServerOptions options)
153+
{
154+
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
155+
{
156+
var result = await next(context, cancellationToken);
157+
158+
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource))
159+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
160+
{
161+
throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
162+
}
163+
164+
return result;
165+
});
166+
}
167+
82168
private void ConfigureReadResourceFilter(McpServerOptions options)
83169
{
84170
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
85171
{
172+
context.Items[AuthorizationFilterInvokedKey] = true;
173+
86174
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
87175
if (!authResult.Succeeded)
88176
{
@@ -93,10 +181,26 @@ private void ConfigureReadResourceFilter(McpServerOptions options)
93181
});
94182
}
95183

184+
private void CheckReadResourceFilter(McpServerOptions options)
185+
{
186+
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
187+
{
188+
if (HasAuthorizationMetadata(context.MatchedPrimitive)
189+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
190+
{
191+
throw new InvalidOperationException("Authorization filter was not invoked for resources/read operation, but authorization metadata was found on the resource. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
192+
}
193+
194+
return await next(context, cancellationToken);
195+
});
196+
}
197+
96198
private void ConfigureListPromptsFilter(McpServerOptions options)
97199
{
98200
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
99201
{
202+
context.Items[AuthorizationFilterInvokedKey] = true;
203+
100204
var result = await next(context, cancellationToken);
101205
await FilterAuthorizedItemsAsync(
102206
result.Prompts, static prompt => prompt.McpServerPrompt,
@@ -105,10 +209,28 @@ await FilterAuthorizedItemsAsync(
105209
});
106210
}
107211

212+
private void CheckListPromptsFilter(McpServerOptions options)
213+
{
214+
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
215+
{
216+
var result = await next(context, cancellationToken);
217+
218+
if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt))
219+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
220+
{
221+
throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
222+
}
223+
224+
return result;
225+
});
226+
}
227+
108228
private void ConfigureGetPromptFilter(McpServerOptions options)
109229
{
110230
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
111231
{
232+
context.Items[AuthorizationFilterInvokedKey] = true;
233+
112234
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
113235
if (!authResult.Succeeded)
114236
{
@@ -119,6 +241,20 @@ private void ConfigureGetPromptFilter(McpServerOptions options)
119241
});
120242
}
121243

244+
private void CheckGetPromptFilter(McpServerOptions options)
245+
{
246+
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
247+
{
248+
if (HasAuthorizationMetadata(context.MatchedPrimitive)
249+
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
250+
{
251+
throw new InvalidOperationException("Authorization filter was not invoked for prompts/get operation, but authorization metadata was found on the prompt. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
252+
}
253+
254+
return await next(context, cancellationToken);
255+
});
256+
}
257+
122258
/// <summary>
123259
/// Filters a collection of items based on authorization policies in their metadata.
124260
/// For list operations where we need to filter results by authorization.
@@ -141,16 +277,7 @@ private async ValueTask FilterAuthorizedItemsAsync<T>(IList<T> items, Func<T, IM
141277
private async ValueTask<AuthorizationResult> GetAuthorizationResultAsync(
142278
ClaimsPrincipal? user, IMcpServerPrimitive? primitive, IServiceProvider? requestServices, object context)
143279
{
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))
280+
if (!HasAuthorizationMetadata(primitive))
154281
{
155282
return AuthorizationResult.Success();
156283
}
@@ -219,4 +346,19 @@ private async ValueTask<AuthorizationResult> GetAuthorizationResultAsync(
219346
? reqPolicyBuilder.Build()
220347
: AuthorizationPolicy.Combine(policy, reqPolicyBuilder.Build());
221348
}
349+
350+
private static bool HasAuthorizationMetadata([NotNullWhen(true)] IMcpServerPrimitive? primitive)
351+
{
352+
// If no primitive was found for this request or there is IAllowAnonymous metadata anywhere on the class or method,
353+
// the request should go through as normal.
354+
if (primitive is null || primitive.Metadata.Any(static m => m is IAllowAnonymous))
355+
{
356+
return false;
357+
}
358+
359+
return primitive.Metadata.Any(static m => m is IAuthorizeData or AuthorizationPolicy or IAuthorizationRequirementData);
360+
}
361+
362+
private static bool HasAuthorizationMetadata(IEnumerable<IMcpServerPrimitive?> primitives)
363+
=> primitives.Any(HasAuthorizationMetadata);
222364
}

src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.AspNetCore.Authorization;
12
using Microsoft.Extensions.DependencyInjection.Extensions;
23
using Microsoft.Extensions.Options;
34
using ModelContextProtocol.AspNetCore;
@@ -30,8 +31,7 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
3031
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
3132
builder.Services.AddDataProtection();
3233

33-
// Register authorization filter setup for automatic filter configuration
34-
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
34+
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
3535

3636
if (configureOptions is not null)
3737
{
@@ -40,4 +40,27 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
4040

4141
return builder;
4242
}
43+
44+
/// <summary>
45+
/// Adds authorization filters to support <see cref="AuthorizeAttribute"/>
46+
/// on MCP server tools, prompts, and resources. This method should always be called when using
47+
/// ASP.NET Core integration to ensure proper authorization support.
48+
/// </summary>
49+
/// <param name="builder">The builder instance.</param>
50+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
51+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
52+
/// <remarks>
53+
/// This method automatically configures authorization filters for all MCP server handlers. These filters respect
54+
/// authorization attributes such as <see cref="AuthorizeAttribute"/>
55+
/// and <see cref="AllowAnonymousAttribute"/>.
56+
/// </remarks>
57+
public static IMcpServerBuilder AddAuthorizationFilters(this IMcpServerBuilder builder)
58+
{
59+
ArgumentNullException.ThrowIfNull(builder);
60+
61+
// Allow the authorization filters to get added multiple times in case other middleware changes the matched primitive.
62+
builder.Services.AddTransient<IConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>();
63+
64+
return builder;
65+
}
4366
}

0 commit comments

Comments
 (0)