Skip to content

Commit d8dce51

Browse files
Copilothalter73
andcommitted
Update OAuth tests to use middleware with EnableBuffering()
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
1 parent 7e74052 commit d8dce51

1 file changed

Lines changed: 125 additions & 31 deletions

File tree

  • tests/ModelContextProtocol.AspNetCore.Tests/OAuth

tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs

Lines changed: 125 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Authentication;
22
using Microsoft.AspNetCore.Authentication.JwtBearer;
3+
using Microsoft.AspNetCore.Authorization;
34
using Microsoft.AspNetCore.Builder;
45
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.WebUtilities;
@@ -8,10 +9,12 @@
89
using ModelContextProtocol.AspNetCore.Authentication;
910
using ModelContextProtocol.Authentication;
1011
using ModelContextProtocol.Client;
12+
using ModelContextProtocol.Protocol;
1113
using ModelContextProtocol.Server;
1214
using System.Net;
1315
using System.Net.Http.Json;
1416
using System.Security.Claims;
17+
using System.Text.Json;
1518
using Xunit.Sdk;
1619

1720
namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
@@ -211,32 +214,66 @@ public async Task CanAuthenticate_WithTokenRefresh()
211214
{
212215
var hasForcedRefresh = false;
213216

214-
Builder.Services.AddHttpContextAccessor();
215217
Builder.Services.AddMcpServer(options =>
218+
{
219+
options.ToolCollection = new();
220+
});
221+
222+
// Wait for the OAuth server to be ready
223+
await TestOAuthServer.ServerStarted.WaitAsync(TestContext.Current.CancellationToken);
224+
225+
Builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
226+
{
227+
options.TokenValidationParameters.ValidAudience = McpServerUrl;
228+
});
229+
230+
var app = Builder.Build();
231+
232+
// Add middleware to intercept list tools requests and force a token refresh on the first call
233+
app.Use(async (context, next) =>
234+
{
235+
if (context.Request.Method == HttpMethods.Post && context.Request.Path == "/" && !hasForcedRefresh)
216236
{
217-
options.ToolCollection = new();
218-
})
219-
.AddListToolsFilter(next =>
220-
{
221-
return async (mcpContext, cancellationToken) =>
237+
// Enable buffering so we can read the request body multiple times
238+
context.Request.EnableBuffering();
239+
240+
try
222241
{
223-
if (!hasForcedRefresh)
242+
// Read the request body to check if it's calling tools/list
243+
var message = await JsonSerializer.DeserializeAsync(
244+
context.Request.Body,
245+
McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)),
246+
context.RequestAborted) as JsonRpcMessage;
247+
248+
// Reset the request body position so MapMcp can read it
249+
context.Request.Body.Position = 0;
250+
251+
// Check if this is a tools/list request
252+
if (message is JsonRpcRequest request && request.Method == "tools/list")
224253
{
225254
hasForcedRefresh = true;
226255

227-
var httpContext = mcpContext.Services!.GetRequiredService<IHttpContextAccessor>().HttpContext!;
228-
await httpContext.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
229-
await httpContext.Response.CompleteAsync();
230-
throw new Exception("This exception will not impact the client because the response has already been completed.");
256+
// Return 401 to force token refresh
257+
await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
258+
await context.Response.StartAsync(context.RequestAborted);
259+
await context.Response.Body.FlushAsync(context.RequestAborted);
260+
return; // Short-circuit, don't call next()
231261
}
232-
else
233-
{
234-
return await next(mcpContext, cancellationToken);
235-
}
236-
};
237-
});
262+
}
263+
catch (JsonException)
264+
{
265+
// If we can't deserialize, let MapMcp handle it
266+
context.Request.Body.Position = 0;
267+
}
268+
}
238269

239-
await using var app = await StartMcpServerAsync();
270+
await next(context);
271+
});
272+
273+
app.MapMcp().RequireAuthorization(new AuthorizeAttribute());
274+
await app.StartAsync(TestContext.Current.CancellationToken);
275+
276+
await using var _ = app;
240277

241278
await using var transport = new HttpClientTransport(new()
242279
{
@@ -451,29 +488,86 @@ public async Task AuthorizationFlow_UsesScopeFromForbiddenHeader()
451488
{
452489
var adminScopes = "admin:read admin:write";
453490

454-
Builder.Services.AddHttpContextAccessor();
455491
Builder.Services.AddMcpServer()
456492
.WithTools([
457493
McpServerTool.Create([McpServerTool(Name = "admin-tool")]
458-
async (IServiceProvider serviceProvider, ClaimsPrincipal user) =>
494+
(ClaimsPrincipal user) =>
459495
{
460-
if (!user.HasClaim("scope", adminScopes))
461-
{
462-
var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
463-
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
464-
httpContext.Response.Headers.WWWAuthenticate = $"Bearer error=\"insufficient_scope\", resource_metadata=\"{McpServerUrl}/.well-known/oauth-protected-resource\", scope=\"{adminScopes}\"";
465-
await httpContext.Response.CompleteAsync();
466-
467-
throw new Exception("This exception will not impact the client because the response has already been completed.");
468-
}
469-
496+
// Tool now just checks if user has the required scopes
497+
// If they don't, it shouldn't get here due to middleware
498+
Assert.True(user.HasClaim("scope", adminScopes), "User should have admin scopes when tool executes");
470499
return "Admin tool executed.";
471500
}),
472501
]);
473502

474503
string? requestedScope = null;
475504

476-
await using var app = await StartMcpServerAsync();
505+
// Wait for the OAuth server to be ready
506+
await TestOAuthServer.ServerStarted.WaitAsync(TestContext.Current.CancellationToken);
507+
508+
Builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
509+
{
510+
options.TokenValidationParameters.ValidAudience = McpServerUrl;
511+
});
512+
513+
var app = Builder.Build();
514+
515+
// Add middleware to intercept requests and check for admin-tool calls
516+
app.Use(async (context, next) =>
517+
{
518+
if (context.Request.Method == HttpMethods.Post && context.Request.Path == "/")
519+
{
520+
// Enable buffering so we can read the request body multiple times
521+
context.Request.EnableBuffering();
522+
523+
try
524+
{
525+
// Read the request body to check if it's calling admin-tool
526+
var message = await JsonSerializer.DeserializeAsync(
527+
context.Request.Body,
528+
McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)),
529+
context.RequestAborted) as JsonRpcMessage;
530+
531+
// Reset the request body position so MapMcp can read it
532+
context.Request.Body.Position = 0;
533+
534+
// Check if this is a tools/call request for admin-tool
535+
if (message is JsonRpcRequest request && request.Method == "tools/call")
536+
{
537+
var toolCallParams = JsonSerializer.Deserialize(
538+
request.Params,
539+
McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(CallToolRequestParams))) as CallToolRequestParams;
540+
541+
if (toolCallParams?.Name == "admin-tool")
542+
{
543+
// Check if user has required scopes
544+
var user = context.User;
545+
if (!user.HasClaim("scope", adminScopes))
546+
{
547+
// User lacks required scopes, return 403 before MapMcp processes the request
548+
context.Response.StatusCode = StatusCodes.Status403Forbidden;
549+
context.Response.Headers.WWWAuthenticate = $"Bearer error=\"insufficient_scope\", resource_metadata=\"{McpServerUrl}/.well-known/oauth-protected-resource\", scope=\"{adminScopes}\"";
550+
await context.Response.StartAsync(context.RequestAborted);
551+
await context.Response.Body.FlushAsync(context.RequestAborted);
552+
return; // Short-circuit, don't call next()
553+
}
554+
}
555+
}
556+
}
557+
catch (JsonException)
558+
{
559+
// If we can't deserialize, let MapMcp handle it
560+
context.Request.Body.Position = 0;
561+
}
562+
}
563+
564+
await next(context);
565+
});
566+
567+
app.MapMcp().RequireAuthorization(new AuthorizeAttribute());
568+
await app.StartAsync(TestContext.Current.CancellationToken);
569+
570+
await using var _ = app;
477571

478572
await using var transport = new HttpClientTransport(new()
479573
{

0 commit comments

Comments
 (0)