Skip to content

Serve the sample authorization server over loopback HTTP - #1802

Open
anneheartrecord wants to merge 1 commit into
modelcontextprotocol:mainfrom
anneheartrecord:fix/sample-oauth-server-loopback-http
Open

Serve the sample authorization server over loopback HTTP#1802
anneheartrecord wants to merge 1 commit into
modelcontextprotocol:mainfrom
anneheartrecord:fix/sample-oauth-server-loopback-http

Conversation

@anneheartrecord

Copy link
Copy Markdown

Fixes #689.

Root cause

The sample never gets as far as the authorization server. Reproduced against current main, with the failure isolated to one request:

  1. POST http://localhost:7071/401, WWW-Authenticate: Bearer resource_metadata="http://localhost:7071/.well-known/oauth-protected-resource/"
  2. GET that document → {"resource":"http://localhost:7071","authorization_servers":["https://localhost:7029"], ...}
  3. GET https://localhost:7029/.well-known/oauth-authorization-serverfails

Step 3 fails because TestOAuthServer hosts the authorization server on the ASP.NET Core developer certificate, and VS Code's HTTP stack carries its own CA list instead of using the operating system trust store. Node reports it as TypeError: fetch failed, cause DEPTH_ZERO_SELF_SIGNED_CERT — which is exactly the Error populating auth metadata: TypeError: fetch failed line in the trace on microsoft/vscode#261120, where this was chased from the VS Code side and left as "something to do with your cert on localhost".

From there VS Code treats it as a server without metadata and falls back to getDefaultMetadataForUrl(mcpServerUrl), the compatibility path for the 2025-03-26 spec. That fallback has no registration endpoint, so it prompts for a client id (which reads as "DCR not supported"), and its authorization endpoint is http://localhost:7071/authorize — the 404 in the original report.

So the answer to "sample, TestOAuthServer, or SDK auth handling" is TestOAuthServer, and neither of the two symptoms is the bug — both are downstream of one unreachable metadata document. The .NET client sample works because HttpClient does use the OS trust store, where dotnet dev-certs https --trust put the certificate. The repo's own OAuth tests work because OAuthTestBase turns certificate validation off.

What changed

TestOAuthServer now listens over plain HTTP on loopback when it's run standalone, so its metadata is reachable without trusting anything first. --https (and the https launch profile) keeps the developer certificate available. The constructor still defaults to HTTPS, so the tests that construct Program directly are untouched.

ProtectedMcpServer points at http://localhost:7029 and sets RequireHttpsMetadata = false, scoped with a comment and a README note saying not to do that against anything but a loopback authority you control.

Verifying

I don't have VS Code on this machine, so I drove the flow it performs with Node's fetch — same HTTP stack, same certificate behaviour. Before: step 3 fails with DEPTH_ZERO_SELF_SIGNED_CERT and http://localhost:7071/authorize returns 404. After: metadata → dynamic client registration → authorization code → token → an authenticated initialize returning 200. The token exchange also proves the sample's JwtBearer backchannel fetches its signing keys over the HTTP authority.

dotnet build is clean and tests/ModelContextProtocol.AspNetCore.Tests passes (582 tests, net10.0). The new test covers the two things this change can regress: the scheme the discovery document advertises has to match the origin it's hosted on, and the standalone default has to stay HTTP.

One thing to call out

RFC 8414 wants an https issuer identifier, and in the default mode this fixture now publishes "issuer": "http://localhost:7029". That felt like the right trade for a loopback test server whose whole job is to be easy to point a client at — but it does change what plain dotnet run does, so if you'd rather keep HTTPS as the default and make it --http to opt out, say the word and I'll flip it. The samples would then need the README to tell people to pass the flag, which is the part I expect to keep tripping people up.

The ProtectedMcpServer sample pairs with TestOAuthServer, which hosts the
authorization server on the ASP.NET Core developer certificate. Clients that
keep their own CA list rather than using the OS trust store cannot fetch
https://localhost:7029/.well-known/oauth-authorization-server from it. VS Code
is one: the fetch fails, it treats that as a server without metadata, and falls
back to the pre-2025-06-18 defaults derived from the MCP server URL. That drops
the registration endpoint, so it asks for a client id, and then sends the
browser to http://localhost:7071/authorize, which 404s.

Host the standalone server over plain HTTP on loopback so its metadata is
reachable without trusting anything first, and keep the developer certificate
available behind --https. Tests construct Program directly and are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProtectedMcpServer sample doesn't work with VSCode auth flow

1 participant