Skip to content

Commit dfa29e1

Browse files
committed
Add TestOAuthServer
1 parent 2b9eae4 commit dfa29e1

34 files changed

+1260
-80
lines changed

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
</Folder>
3636
<Folder Name="/tests/">
3737
<Project Path="tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj" />
38+
<Project Path="tests/ModelContextProtocol.TestOAuthServer/ModelContextProtocol.TestOAuthServer.csproj" />
3839
<Project Path="tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj" />
3940
<Project Path="tests/ModelContextProtocol.TestServer/ModelContextProtocol.TestServer.csproj" />
4041
<Project Path="tests/ModelContextProtocol.TestSseServer/ModelContextProtocol.TestSseServer.csproj" />

samples/ProtectedMCPClient/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using System.Text;
88
using System.Web;
99

10-
Console.WriteLine("Protected MCP Weather Server");
10+
Console.WriteLine("Protected MCP Client");
1111
Console.WriteLine();
1212

1313
var serverUrl = "http://localhost:7071/";
@@ -30,7 +30,9 @@
3030
var tokenProvider = new GenericOAuthProvider(
3131
new Uri(serverUrl),
3232
httpClient,
33-
clientId: clientId,
33+
//clientId: clientId,
34+
clientId: "demo-client",
35+
clientSecret: "demo-secret",
3436
redirectUri: new Uri("http://localhost:1179/callback"),
3537
authorizationRedirectDelegate: HandleAuthorizationUrlAsync,
3638
loggerFactory: consoleLoggerFactory);
@@ -169,5 +171,6 @@ static void OpenBrowser(Uri url)
169171
catch (Exception ex)
170172
{
171173
Console.WriteLine($"Error opening browser. {ex.Message}");
174+
Console.WriteLine($"Please manually open this URL: {url}");
172175
}
173176
}

samples/ProtectedMCPServer/Program.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88

99
var builder = WebApplication.CreateBuilder(args);
1010

11-
var serverUrl = "http://localhost:7071/";
12-
var tenantId = builder.Configuration["TenantId"];
13-
var clientId = builder.Configuration["ClientId"];
14-
var instance = "https://login.microsoftonline.com/";
11+
var serverUrl = "http://localhost:7071";
12+
var inMemoryOAuthServerUrl = "https://localhost:7029";
13+
var demoClientId = "demo-client";
1514

1615
builder.Services.AddAuthentication(options =>
1716
{
@@ -20,21 +19,20 @@
2019
})
2120
.AddJwtBearer(options =>
2221
{
23-
options.Authority = $"{instance}{tenantId}/v2.0";
22+
// Configure to validate tokens from our in-memory OAuth server
23+
options.Authority = inMemoryOAuthServerUrl;
2424
options.TokenValidationParameters = new TokenValidationParameters
2525
{
2626
ValidateIssuer = true,
2727
ValidateAudience = true,
2828
ValidateLifetime = true,
2929
ValidateIssuerSigningKey = true,
30-
ValidAudience = clientId,
31-
ValidIssuer = $"{instance}{tenantId}/v2.0",
30+
ValidAudience = demoClientId,
31+
ValidIssuer = inMemoryOAuthServerUrl,
3232
NameClaimType = "name",
3333
RoleClaimType = "roles"
3434
};
3535

36-
options.MetadataAddress = $"{instance}{tenantId}/v2.0/.well-known/openid-configuration";
37-
3836
options.Events = new JwtBearerEvents
3937
{
4038
OnTokenValidated = context =>
@@ -62,14 +60,14 @@
6260
{
6361
var metadata = new ProtectedResourceMetadata
6462
{
65-
Resource = new Uri("http://localhost:7071/"),
63+
Resource = new Uri(serverUrl),
6664
BearerMethodsSupported = { "header" },
6765
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
68-
AuthorizationServers = { new Uri($"{instance}{tenantId}/v2.0") }
66+
AuthorizationServers = { new Uri(inMemoryOAuthServerUrl) }
6967
};
7068

7169
metadata.ScopesSupported.AddRange([
72-
$"api://{clientId}/weather.read"
70+
"mcp:tools"
7371
]);
7472

7573
return metadata;
@@ -99,7 +97,9 @@
9997
app.MapMcp().RequireAuthorization();
10098

10199
Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
102-
Console.WriteLine($"PRM Document URL: {serverUrl}.well-known/oauth-protected-resource");
100+
Console.WriteLine($"Using in-memory OAuth server at {inMemoryOAuthServerUrl}");
101+
Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
102+
Console.WriteLine($"Demo Client ID: {demoClientId}");
103103
Console.WriteLine("Press Ctrl+C to stop the server");
104104

105105
app.Run(serverUrl);

samples/ProtectedMCPServer/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"environmentVariables": {
77
"ASPNETCORE_ENVIRONMENT": "Development"
88
},
9-
"applicationUrl": "https://localhost:55598;http://localhost:55599"
9+
"applicationUrl": "http://localhost:7029"
1010
}
1111
}
1212
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.WebUtilities;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.IdentityModel.Tokens;
6+
using ModelContextProtocol.AspNetCore.Authentication;
7+
using ModelContextProtocol.AspNetCore.Tests.Utils;
8+
using ModelContextProtocol.Authentication;
9+
using ModelContextProtocol.Client;
10+
using System.Net;
11+
12+
namespace ModelContextProtocol.AspNetCore.Tests;
13+
14+
public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
15+
{
16+
private const string McpServerUrl = "http://localhost:5000";
17+
private const string OAuthServerUrl = "https://localhost:7029";
18+
private const string ClientId = "demo-client";
19+
20+
private readonly CancellationTokenSource _testCts = new();
21+
private readonly Task _oAuthRunTask;
22+
23+
public AuthTests(ITestOutputHelper outputHelper)
24+
: base(outputHelper)
25+
{
26+
SocketsHttpHandler.AllowAutoRedirect = false;
27+
28+
var oAuthServerProgram = new TestOAuthServer.Program(XunitLoggerProvider, KestrelInMemoryTransport);
29+
_oAuthRunTask = oAuthServerProgram.RunServerAsync(cancellationToken: _testCts.Token);
30+
31+
Builder.Services.AddAuthentication(options =>
32+
{
33+
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
34+
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
35+
})
36+
.AddJwtBearer(options =>
37+
{
38+
options.Backchannel = HttpClient;
39+
options.Authority = OAuthServerUrl;
40+
options.TokenValidationParameters = new TokenValidationParameters
41+
{
42+
ValidateIssuer = true,
43+
ValidateAudience = true,
44+
ValidateLifetime = true,
45+
ValidateIssuerSigningKey = true,
46+
ValidAudience = ClientId,
47+
ValidIssuer = OAuthServerUrl,
48+
NameClaimType = "name",
49+
RoleClaimType = "roles"
50+
};
51+
})
52+
.AddMcp(options =>
53+
{
54+
options.ProtectedResourceMetadataProvider = context =>
55+
{
56+
var metadata = new ProtectedResourceMetadata
57+
{
58+
Resource = new Uri(McpServerUrl),
59+
BearerMethodsSupported = { "header" },
60+
AuthorizationServers = { new Uri(OAuthServerUrl) }
61+
};
62+
63+
metadata.ScopesSupported.AddRange([
64+
"mcp:tools"
65+
]);
66+
67+
return metadata;
68+
};
69+
});
70+
71+
Builder.Services.AddAuthorization();
72+
}
73+
74+
public async ValueTask DisposeAsync()
75+
{
76+
_testCts.Cancel();
77+
try
78+
{
79+
await _oAuthRunTask;
80+
}
81+
catch (OperationCanceledException)
82+
{
83+
}
84+
finally
85+
{
86+
_testCts.Dispose();
87+
}
88+
}
89+
90+
[Fact]
91+
public async Task CanAuthenticate()
92+
{
93+
Builder.Services.AddMcpServer().WithHttpTransport();
94+
95+
await using var app = Builder.Build();
96+
97+
app.MapMcp().RequireAuthorization();
98+
99+
await app.StartAsync(TestContext.Current.CancellationToken);
100+
101+
var tokenProvider = new GenericOAuthProvider(
102+
new Uri(McpServerUrl),
103+
HttpClient,
104+
clientId: "demo-client",
105+
clientSecret: "demo-secret",
106+
redirectUri: new Uri("http://localhost:1179/callback"),
107+
authorizationRedirectDelegate: HandleAuthorizationUrlAsync,
108+
loggerFactory: LoggerFactory);
109+
110+
await using var transport = new SseClientTransport(new SseClientTransportOptions()
111+
{
112+
Endpoint = new("http://localhost:5000"),
113+
CredentialProvider = tokenProvider,
114+
}, HttpClient, LoggerFactory);
115+
116+
await using var client = await McpClientFactory.CreateAsync(
117+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
118+
}
119+
120+
private async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
121+
{
122+
var redirectResponse = await HttpClient.GetAsync(authorizationUrl, cancellationToken);
123+
Assert.Equal(HttpStatusCode.Redirect, redirectResponse.StatusCode);
124+
var location = redirectResponse.Headers.Location;
125+
126+
if (location is not null && !string.IsNullOrEmpty(location.Query))
127+
{
128+
var queryParams = QueryHelpers.ParseQuery(location.Query);
129+
return queryParams["code"];
130+
}
131+
132+
return null;
133+
}
134+
}

tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public async Task CallTool_EchoSessionId_ReturnsTheSameSessionId()
118118
Assert.False(result1.IsError);
119119
Assert.False(result2.IsError);
120120
Assert.False(result3.IsError);
121-
121+
122122
var textContent1 = Assert.Single(result1.Content.OfType<TextContentBlock>());
123123
var textContent2 = Assert.Single(result2.Content.OfType<TextContentBlock>());
124124
var textContent3 = Assert.Single(result3.Content.OfType<TextContentBlock>());
@@ -267,10 +267,10 @@ public async Task Sampling_Sse_TestServer()
267267

268268
// Call the server's sampleLLM tool which should trigger our sampling handler
269269
var result = await client.CallToolAsync("sampleLLM", new Dictionary<string, object?>
270-
{
271-
["prompt"] = "Test prompt",
272-
["maxTokens"] = 100
273-
},
270+
{
271+
["prompt"] = "Test prompt",
272+
["maxTokens"] = 100
273+
},
274274
cancellationToken: TestContext.Current.CancellationToken);
275275

276276
// assert
@@ -288,7 +288,7 @@ public async Task CallTool_Sse_EchoServer_Concurrently()
288288
for (int i = 0; i < 4; i++)
289289
{
290290
var client = (i % 2 == 0) ? client1 : client2;
291-
var result = await client.CallToolAsync(
291+
var result = await client.CallToolAsync(
292292
"echo",
293293
new Dictionary<string, object?>
294294
{

tests/ModelContextProtocol.AspNetCore.Tests/MapMcpSseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public async Task Allows_Customizing_Route(string pattern)
2020

2121
await app.StartAsync(TestContext.Current.CancellationToken);
2222

23-
using var response = await HttpClient.GetAsync($"http://localhost{pattern}/sse", HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken);
23+
using var response = await HttpClient.GetAsync($"http://localhost:5000{pattern}/sse", HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken);
2424
response.EnsureSuccessStatusCode();
2525
using var sseStream = await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken);
2626
using var sseStreamReader = new StreamReader(sseStream, System.Text.Encoding.UTF8);

tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public async Task StreamableHttpMode_Works_WithRootEndpoint()
5656

5757
await using var mcpClient = await ConnectAsync("/", new()
5858
{
59-
Endpoint = new Uri("http://localhost/"),
59+
Endpoint = new("http://localhost:5000/"),
6060
TransportMode = HttpTransportMode.AutoDetect
6161
});
6262

@@ -82,7 +82,7 @@ public async Task AutoDetectMode_Works_WithRootEndpoint()
8282

8383
await using var mcpClient = await ConnectAsync("/", new()
8484
{
85-
Endpoint = new Uri("http://localhost/"),
85+
Endpoint = new("http://localhost:5000/"),
8686
TransportMode = HttpTransportMode.AutoDetect
8787
});
8888

@@ -110,7 +110,7 @@ public async Task AutoDetectMode_Works_WithSseEndpoint()
110110

111111
await using var mcpClient = await ConnectAsync("/sse", new()
112112
{
113-
Endpoint = new Uri("http://localhost/sse"),
113+
Endpoint = new("http://localhost:5000/sse"),
114114
TransportMode = HttpTransportMode.AutoDetect
115115
});
116116

@@ -138,7 +138,7 @@ public async Task SseMode_Works_WithSseEndpoint()
138138

139139
await using var mcpClient = await ConnectAsync(transportOptions: new()
140140
{
141-
Endpoint = new Uri("http://localhost/sse"),
141+
Endpoint = new("http://localhost:5000/sse"),
142142
TransportMode = HttpTransportMode.Sse
143143
});
144144

tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ protected async Task<IMcpClient> ConnectAsync(
3333

3434
await using var transport = new SseClientTransport(transportOptions ?? new SseClientTransportOptions()
3535
{
36-
Endpoint = new Uri($"http://localhost{path}"),
36+
Endpoint = new Uri($"http://localhost:5000{path}"),
3737
TransportMode = UseStreamableHttp ? HttpTransportMode.StreamableHttp : HttpTransportMode.Sse,
3838
}, HttpClient, LoggerFactory);
3939

tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3535
<PrivateAssets>all</PrivateAssets>
3636
</PackageReference>
37+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
3738
<PackageReference Include="Microsoft.Extensions.AI" />
3839
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
3940
<PackageReference Include="Microsoft.Extensions.Logging" />
@@ -56,6 +57,7 @@
5657
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
5758
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
5859
<ProjectReference Include="..\ModelContextProtocol.TestSseServer\ModelContextProtocol.TestSseServer.csproj" />
60+
<ProjectReference Include="..\ModelContextProtocol.TestOAuthServer\ModelContextProtocol.TestOAuthServer.csproj" />
5961
</ItemGroup>
6062

6163
</Project>

0 commit comments

Comments
 (0)