|
8 | 8 | using ModelContextProtocol.Tests.Utils; |
9 | 9 | using System.Collections.Concurrent; |
10 | 10 | using System.Net; |
| 11 | +using System.Security.Claims; |
11 | 12 | using System.Threading; |
12 | 13 | using System.Threading.Tasks; |
13 | 14 |
|
@@ -868,4 +869,76 @@ public async Task EndpointFilter_CanReadSessionId_BeforeAndAfterHandler() |
868 | 869 | }); |
869 | 870 | } |
870 | 871 | } |
| 872 | + |
| 873 | + [Fact] |
| 874 | + public async Task DeleteRequest_FromDifferentUser_IsRejected_AndSessionSurvives() |
| 875 | + { |
| 876 | + Assert.SkipWhen(Stateless, "Sessions don't exist in stateless mode."); |
| 877 | + |
| 878 | + Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools<EchoHttpContextUserTools>(); |
| 879 | + Builder.Services.AddHttpContextAccessor(); |
| 880 | + |
| 881 | + await using var app = Builder.Build(); |
| 882 | + |
| 883 | + // Pick the user from a test header so different HttpClient requests can act as different users. |
| 884 | + app.Use(next => async context => |
| 885 | + { |
| 886 | + var name = context.Request.Headers["X-Test-User"].ToString(); |
| 887 | + if (!string.IsNullOrEmpty(name)) |
| 888 | + { |
| 889 | + context.User = new ClaimsPrincipal(new ClaimsIdentity( |
| 890 | + [new Claim("name", name), new Claim(ClaimTypes.NameIdentifier, name)], |
| 891 | + "TestAuthType", "name", "role")); |
| 892 | + } |
| 893 | + await next(context); |
| 894 | + }); |
| 895 | + |
| 896 | + app.MapMcp(); |
| 897 | + await app.StartAsync(TestContext.Current.CancellationToken); |
| 898 | + |
| 899 | + const string initializeRequest = """ |
| 900 | + {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} |
| 901 | + """; |
| 902 | + |
| 903 | + using var initRequest = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/") |
| 904 | + { |
| 905 | + Content = new StringContent(initializeRequest, System.Text.Encoding.UTF8, "application/json"), |
| 906 | + }; |
| 907 | + initRequest.Headers.Add("X-Test-User", "Alice"); |
| 908 | + initRequest.Headers.Accept.ParseAdd("application/json"); |
| 909 | + initRequest.Headers.Accept.ParseAdd("text/event-stream"); |
| 910 | + |
| 911 | + using var initResponse = await HttpClient.SendAsync(initRequest, TestContext.Current.CancellationToken); |
| 912 | + Assert.True(initResponse.IsSuccessStatusCode); |
| 913 | + var sessionId = Assert.Single(initResponse.Headers.GetValues("Mcp-Session-Id")); |
| 914 | + |
| 915 | + // A DELETE from a different authenticated user must not be able to tear down Alice's session. |
| 916 | + using var bobDelete = new HttpRequestMessage(HttpMethod.Delete, "http://localhost:5000/"); |
| 917 | + bobDelete.Headers.Add("X-Test-User", "Bob"); |
| 918 | + bobDelete.Headers.Add("Mcp-Session-Id", sessionId); |
| 919 | + using var bobDeleteResponse = await HttpClient.SendAsync(bobDelete, TestContext.Current.CancellationToken); |
| 920 | + Assert.Equal(HttpStatusCode.Forbidden, bobDeleteResponse.StatusCode); |
| 921 | + |
| 922 | + // Alice should still be able to use the session. |
| 923 | + const string toolCallRequest = """ |
| 924 | + {"jsonrpc":"2.0","id":2,"method":"tools/list"} |
| 925 | + """; |
| 926 | + using var aliceCall = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/") |
| 927 | + { |
| 928 | + Content = new StringContent(toolCallRequest, System.Text.Encoding.UTF8, "application/json"), |
| 929 | + }; |
| 930 | + aliceCall.Headers.Add("X-Test-User", "Alice"); |
| 931 | + aliceCall.Headers.Add("Mcp-Session-Id", sessionId); |
| 932 | + aliceCall.Headers.Accept.ParseAdd("application/json"); |
| 933 | + aliceCall.Headers.Accept.ParseAdd("text/event-stream"); |
| 934 | + using var aliceCallResponse = await HttpClient.SendAsync(aliceCall, TestContext.Current.CancellationToken); |
| 935 | + Assert.True(aliceCallResponse.IsSuccessStatusCode); |
| 936 | + |
| 937 | + // Alice can still terminate her own session. |
| 938 | + using var aliceDelete = new HttpRequestMessage(HttpMethod.Delete, "http://localhost:5000/"); |
| 939 | + aliceDelete.Headers.Add("X-Test-User", "Alice"); |
| 940 | + aliceDelete.Headers.Add("Mcp-Session-Id", sessionId); |
| 941 | + using var aliceDeleteResponse = await HttpClient.SendAsync(aliceDelete, TestContext.Current.CancellationToken); |
| 942 | + Assert.True(aliceDeleteResponse.IsSuccessStatusCode); |
| 943 | + } |
871 | 944 | } |
0 commit comments