Skip to content

Commit cd1cbc4

Browse files
jeffhandleyCopilot
andcommitted
Proxy OAuth endpoints through MCP server in legacy auth test
Update CanAuthenticate_WithLegacyServerWithoutProtectedResourceMetadata to use McpServerUrl for auth metadata endpoints and proxy OAuth requests to the real OAuth server, matching the pattern used by the endpoint fallback test. Update TestOAuthServer RequireResource=false to reject requests that include a resource parameter, ensuring the client correctly omits it in legacy mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ee81c54 commit cd1cbc4

File tree

2 files changed

+55
-8
lines changed

2 files changed

+55
-8
lines changed

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

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,9 @@ public async Task CanAuthenticate_WithLegacyServerWithoutProtectedResourceMetada
10641064

10651065
await using var app = Builder.Build();
10661066

1067+
// Capture HttpClient for use in the proxy middleware.
1068+
var httpClient = HttpClient;
1069+
10671070
app.Use(async (context, next) =>
10681071
{
10691072
// Return 404 for PRM to simulate a legacy server that doesn't support RFC 9728.
@@ -1073,7 +1076,7 @@ public async Task CanAuthenticate_WithLegacyServerWithoutProtectedResourceMetada
10731076
return;
10741077
}
10751078

1076-
// Serve auth server metadata pointing to the real OAuth server endpoints.
1079+
// Serve auth server metadata pointing to the MCP server's own endpoints.
10771080
// In a real 2025-03-26 deployment, the MCP server itself would be the auth server.
10781081
if (context.Request.Path.StartsWithSegments("/.well-known/oauth-authorization-server") ||
10791082
context.Request.Path.StartsWithSegments("/.well-known/openid-configuration"))
@@ -1082,9 +1085,9 @@ public async Task CanAuthenticate_WithLegacyServerWithoutProtectedResourceMetada
10821085
await context.Response.WriteAsync($$"""
10831086
{
10841087
"issuer": "{{OAuthServerUrl}}",
1085-
"authorization_endpoint": "{{OAuthServerUrl}}/authorize",
1086-
"token_endpoint": "{{OAuthServerUrl}}/token",
1087-
"registration_endpoint": "{{OAuthServerUrl}}/register",
1088+
"authorization_endpoint": "{{McpServerUrl}}/authorize",
1089+
"token_endpoint": "{{McpServerUrl}}/token",
1090+
"registration_endpoint": "{{McpServerUrl}}/register",
10881091
"response_types_supported": ["code"],
10891092
"grant_types_supported": ["authorization_code", "refresh_token"],
10901093
"token_endpoint_auth_methods_supported": ["client_secret_post"],
@@ -1094,6 +1097,45 @@ await context.Response.WriteAsync($$"""
10941097
return;
10951098
}
10961099

1100+
// Proxy OAuth endpoints to the real OAuth server.
1101+
// In a real 2025-03-26 deployment, the MCP server itself would host these endpoints.
1102+
var path = context.Request.Path.Value;
1103+
if (path is "/authorize" or "/token" or "/register")
1104+
{
1105+
var targetUrl = $"{OAuthServerUrl}{path}{context.Request.QueryString}";
1106+
using var proxyRequest = new HttpRequestMessage(new HttpMethod(context.Request.Method), targetUrl);
1107+
1108+
if (context.Request.ContentLength > 0 || context.Request.ContentType is not null)
1109+
{
1110+
proxyRequest.Content = new StreamContent(context.Request.Body);
1111+
if (context.Request.ContentType is not null)
1112+
{
1113+
proxyRequest.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(context.Request.ContentType);
1114+
}
1115+
}
1116+
1117+
if (context.Request.Headers.Authorization.Count > 0)
1118+
{
1119+
proxyRequest.Headers.TryAddWithoutValidation("Authorization", context.Request.Headers.Authorization.ToString());
1120+
}
1121+
1122+
using var response = await httpClient.SendAsync(proxyRequest);
1123+
context.Response.StatusCode = (int)response.StatusCode;
1124+
1125+
if (response.Headers.Location is not null)
1126+
{
1127+
context.Response.Headers.Location = response.Headers.Location.ToString();
1128+
}
1129+
1130+
if (response.Content.Headers.ContentType is not null)
1131+
{
1132+
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
1133+
}
1134+
1135+
await response.Content.CopyToAsync(context.Response.Body);
1136+
return;
1137+
}
1138+
10971139
await next();
10981140
});
10991141

tests/ModelContextProtocol.TestOAuthServer/Program.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
6969

7070
/// <summary>
7171
/// Gets or sets a value indicating whether the authorization server requires a resource parameter.
72+
/// When <c>true</c>, the resource parameter must be present and match a valid resource.
73+
/// When <c>false</c>, the resource parameter must be absent to simulate legacy servers that
74+
/// do not support RFC 8707 resource indicators.
7275
/// </summary>
7376
/// <remarks>
7477
/// The default value is <c>true</c>.
@@ -297,8 +300,9 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
297300
return Results.Redirect($"{redirect_uri}?error=invalid_request&error_description=Only+S256+code_challenge_method+is+supported&state={state}");
298301
}
299302

300-
// Validate resource in accordance with RFC 8707
301-
if (RequireResource && (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)))
303+
// Validate resource in accordance with RFC 8707.
304+
// When RequireResource is false, the resource parameter must be absent (legacy mode).
305+
if (RequireResource ? (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)) : !string.IsNullOrEmpty(resource))
302306
{
303307
return Results.Redirect($"{redirect_uri}?error=invalid_target&error_description=The+specified+resource+is+not+valid&state={state}");
304308
}
@@ -344,9 +348,10 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
344348
type: "https://tools.ietf.org/html/rfc6749#section-5.2");
345349
}
346350

347-
// Validate resource in accordance with RFC 8707
351+
// Validate resource in accordance with RFC 8707.
352+
// When RequireResource is false, the resource parameter must be absent (legacy mode).
348353
var resource = form["resource"].ToString();
349-
if (RequireResource && (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)))
354+
if (RequireResource ? (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)) : !string.IsNullOrEmpty(resource))
350355
{
351356
return Results.BadRequest(new OAuthErrorResponse
352357
{

0 commit comments

Comments
 (0)