Add RFC 9728 protected resource metadata and WWW-Authenticate on 401 - #3
Merged
Merged
Conversation
MCP authorization revision 2025-06-18 requires a resource server to publish protected resource metadata and to point at it from every 401. We implemented the 2025-03-26 requirements (RFC 8414, PKCE, RFC 7591) in dff36ac and never revisited the spec afterwards. It stayed invisible because Claude and ChatGPT also probe the legacy /.well-known/oauth-authorization-server path; a client that follows the current spec strictly cannot discover the authorization server at all and fails before any OAuth window opens. - Serve the RFC 9728 document at /.well-known/oauth-protected-resource and at the resource-path form .../oauth-protected-resource/mcp. Clients differ in which they probe, so both answer. - Send WWW-Authenticate from all four 401 sites via a single unauthorized() helper, so a fifth site cannot silently omit it. - Follow RFC 6750 section 3.1: no error code when no credential was presented, error="invalid_token" when one was presented and rejected. authorization_servers and the issuer in the RFC 8414 document both derive from resolveBaseUrl(). That is deliberate: once WWW-Authenticate exists, conformant clients follow it INSTEAD of the legacy path, so a disagreement between the two documents would break the clients that work today. Managed mode only. Selfhosted has no OAuth provider, and advertising metadata there would point clients at /authorize and /token that do not exist. Not included: grok.com in DEFAULT_TRUSTED_REDIRECT_HOSTS (a policy decision, not a compliance fix) and RFC 8707 resource indicators (they only start to matter once issuer and resource are no longer the same origin). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WUg1nG8m7NYmqdToqHsHvK
/health reports pkg.version (src/config/env.ts:10), so a distinct version is how a stage deploy is confirmed to be the new build rather than the 1.6.0 currently running in production.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A user reported on 2026-09-01 that Grok's custom MCP connector cannot connect to mcp.seatable.com: no OAuth window opens at all. Their probes, all reproduced against stage before this change:
GET /health1.6.0POST /mcpwithout AuthorizationWWW-AuthenticateGET /.well-known/oauth-authorization-serverGET /.well-known/oauth-protected-resourceGET /.well-known/oauth-protected-resource/mcpMCP authorization revision 2025-06-18 requires a resource server to publish RFC 9728 protected resource metadata and to point at it from every 401. We implemented the 2025-03-26 requirements in dff36ac — RFC 8414, PKCE, RFC 7591 — and never revisited the spec after it moved. The v1.6.0 hardening was scoped to an external report's three findings, so it was a remediation rather than a conformance audit.
It stayed invisible because Claude and ChatGPT also probe the legacy
/.well-known/oauth-authorization-serverpath. A client that follows the current spec strictly has nothing to go on and fails before any OAuth flow begins.Not a vulnerability
Worth stating explicitly, since RFC 9728 is usually discussed in a security context. The property it protects — audience binding — is already enforced here, and more strictly: access tokens are AES-256-GCM sealed envelopes keyed from
SEATABLE_TOKEN_SECRET, so a token we issued cannot be opened by anyone else and a foreign token cannot be opened by us. Issuer and resource are the same origin, the same process. Nothing becomes exploitable through the absence of this metadata.What does change is the future: the moment issuer and resource are no longer the same origin — the planned move to a real authorization endpoint in SeaTable core — that binding stops being emergent and the missing declaration starts to matter. This lays the groundwork.
What changed
src/auth/oauthProvider.ts—handleProtectedResourceMetadata(), plusresourceMetadataUrl()andchallenge().authorization_serversand theissuerin the RFC 8414 document both derive fromresolveBaseUrl(). That is deliberate and is the main risk this PR carries: onceWWW-Authenticateexists, conformant clients follow it instead of the legacy path, so a disagreement between the two documents would break the clients that work today. Sharing one source makes divergence structurally impossible rather than merely tested.src/http/httpServer.ts— both well-known locations routed (clients differ in which they probe; the reporter probed both), and a singleunauthorized()helper that all four 401 sites now go through. Previously each site built its ownwriteHead— that is how a fifth one would silently ship without the header.Per RFC 6750 §3.1: no error code when no credential was presented,
error="invalid_token"when one was presented and rejected.Managed mode only. Selfhosted has no OAuth provider, and advertising metadata there would point clients at
/authorizeand/tokenthat do not exist.Tests
tests/oauthProtectedResource.spec.ts— 13 tests, written first and red before the fix. Two were green from the start on purpose: they guard that selfhosted keeps answering 404.Full suite: 313 passed (28 files). Lint and typecheck clean.
Verified on stage
Deployed as
testing-1.6.1on mcp.st-stage.de.Protocol level — every probe from the report now inverts:
Client level, in this order:
list_tablessucceeded, no re-authorization. This is the test that matters for existing Claude and ChatGPT users.Deliberately not included
grok.cominDEFAULT_TRUSTED_REDIRECT_HOSTS. A policy decision about which external agents skip the acknowledgement step, not a compliance fix. Grok will work either way; it will see the acknowledgement page once.Known gap in the verification
Claude Code authorizes over a loopback callback, and
oauthProvider.ts:196lets loopback skip the acknowledgement page. The path Grok will actually take was therefore not exercised on stage. It can only be confirmed by the reporter against production.