Advertise required scope in MCP WWW-Authenticate challenge
What does this MR do and why?
Add an RFC 6750 §3 scope attribute to the WWW-Authenticate challenge the MCP
server returns on 401 responses, set to the resource's scope (mcp for
/api/v4/mcp, mcp_orbit for /api/v4/orbit/mcp):
WWW-Authenticate: Bearer realm="GitLab", scope="mcp", resource_metadata="…/.well-known/oauth-protected-resource/api/v4/mcp"The MCP authorization spec (revision 2025-11-25) makes this a SHOULD for
MCP servers:
MCP servers SHOULD include a
scopeparameter in theWWW-Authenticateheader as defined in RFC 6750 Section 3 to indicate the scopes required for accessing the resource. […] preventing clients from requesting excessive permissions.
We already advertise that revision in
API::Mcp::Handlers::InitializeRequest::SUPPORTED_PROTOCOL_VERSIONS, so today we
claim a revision whose scope-advertisement SHOULD we don't satisfy.
The same spec fixes the client's scope-selection order: the challenge's scope
first, and only if it's absent, every scope in the protected resource
metadata's scopes_supported. The challenge is therefore the one signal that
lets a general-purpose client request exactly one scope with no GitLab-specific
knowledge.
Why now, when the known client bug is fixed
The original trigger was a Claude Code bug
(anthropics/claude-code#55954)
that read scopes_supported from the authorization server metadata instead
of the protected resource metadata, producing a 23-scope authorize URL
including sudo, admin_mode and user:* that GitLab rejected with
invalid_scope. That is fixed upstream and #603748 (closed) is closed.
Still worth doing:
- It's a SHOULD for a spec revision we advertise support for.
- Without it, a correctly-behaving client falls through to step 2 and requests
our whole
scopes_supportedlist — valid, but a noisy consent screen. - It removes the failure class rather than the one instance: any future client following the documented order gets the right scope for free.
Relationship to !219070 (merged)
!219070 (merged) already forces the granted scope server-side at the authorization
endpoint, so the issued token is correct regardless of what a client requests.
This is complementary — it fixes what clients request, so the authorize URL
and consent screen show one scope instead of the full list. !219070 bounds the
outcome; this bounds the request.
Also in this MR
The path→scope mapping moved to Gitlab::Auth::MCP_RESOURCE_SCOPES, read by both
the challenge and Oauth::ProtectedResourceMetadataController, so the two cannot
disagree about a resource's scope.
How to set up and validate locally
Restart the web server on this branch and inspect the challenge:
gdk restart rails-web
curl -sk -X POST https://gdk.test:3443/api/v4/mcp -D - -o /dev/null | grep -i www-authenticate
# => Bearer realm="GitLab", scope="mcp", resource_metadata="…/api/v4/mcp"
curl -sk -X POST https://gdk.test:3443/api/v4/orbit/mcp -D - -o /dev/null | grep -i www-authenticate
# => Bearer realm="GitLab", scope="mcp_orbit", resource_metadata="…/api/v4/orbit/mcp"Confirm the challenge and the metadata document agree:
curl -sk https://gdk.test:3443/.well-known/oauth-protected-resource/api/v4/mcp
# => {"resource":"…/api/v4/mcp","authorization_servers":["…"],"scopes_supported":["mcp"]}
curl -sk https://gdk.test:3443/.well-known/oauth-protected-resource/api/v4/orbit/mcp
# => {"resource":"…/api/v4/orbit/mcp","authorization_servers":["…"],"scopes_supported":["mcp_orbit"]}To watch a real client, add the server to ~/.cursor/mcp.json and launch Cursor
with the mkcert root CA trusted, then check the authorize URL's scope=
parameter is mcp rather than the full list:
NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" open -a CursorReferences
Resolved separately, but this is the standards-compliant version of the same fix:
- #603748 (closed) — MCP Server OAuth flow scope error has regressed on GitLab.com (closed)
Builds on / complements:
- !243210 (merged) — Add WWW-Authenticate challenge to MCP 401 responses
- !219146 (merged) — Add scopes_supported to Protected Resource Metadata
- !219070 (merged) — Force MCP scope to MCP API endpoint
Parent epic:
- gitlab-org#19476 (closed) —
🐞 GitLab MCP Server bugs
MR acceptance checklist
- Applied appropriate MR type label
- Requested Application Security review (touches OAuth challenge / auth path)