Answer an unknown token on the MCP endpoints with invalid_token
What does this MR do and why?
On the MCP endpoints, /api/v4/mcp and /api/v4/orbit/mcp, an unknown token gets the 401 of a request without one: no error, and {"message":"401 Unauthorized"}. !261813 (merged) gave these endpoints their challenge with error="invalid_token" for an expired or revoked token, which got invalid_token with the OAuth realm before. RFC 6750 section 3.1 defines it for any token that is not valid.
The finders raise UnauthorizedError for an unknown token (auth_finders.rb#L439), and initial_current_user answers that with unauthorized! before the guard can. API::Helpers::Mcp::AuthChallenge, which only the MCP endpoints include, now overrides find_current_user! to raise TokenNotFoundError instead when a token was sent. The guard answers it with invalid_token, "Bad Access Token." and the endpoint's challenge. A token with no user or an invalid composite identity gets that too; a request without a token keeps its 401. The REST API authentication page names the unknown token too.
It is not in unauthorized!, the issue's proposal, because /api/v4/mcp also calls that for a valid mcp token in PRIVATE-TOKEN; a new example pins it.
The review of !261813 (merged) asked for this. It comes from my work on gitlab-mcp-server, whose upstream-bugs.md records its work on dependencies and sibling projects.
Checks I ran
All on master at b7c44b3b2a1, with Ruby 3.3.11, the second version .tool-versions lists, rather than the 3.4.10 of .ruby-version:
- The new examples with
master'slib/api/helpers/mcp/auth_challenge.rb: in the request specs, 4 examples, 3 failures, the two unknown-token examples of/api/v4/mcpand the Orbit one, each on the body{"message"=>"401 Unauthorized"}and the challenge withouterror; inspec/lib/api/helpers/mcp/auth_challenge_spec.rb, 9 examples, 1 failure, thefind_current_user!example for a request with a token. The examples for a valid token inPRIVATE-TOKENand for a request without a token pass there by design. - With the change made in
unauthorized!instead, the example for the valid token inPRIVATE-TOKENfails, since that request then getserror="invalid_token". Without the check that a token is present, thefind_current_user!example for a request without a token fails. - On the branch,
spec/requests/api/mcp/base_spec.rb,ee/spec/requests/api/orbit/mcp_spec.rbandspec/requests/api/api_guard_spec.rb: 170 examples, 0 failures, 8 pending (skips inside the granular token shared examples).auth_challenge_spec.rbunderfast_spec_helper: 9 examples, 0 failures. - RuboCop on the four Ruby files: no offenses. Both commit message linters: no problems. markdownlint-cli2 0.23.2 and Vale 3.21.0 on the page: no issues, and the same Vale suggestions as on
master.
References
Related to #634370 (closed)
The issue also reports other endpoints, such as GET /api/v4/user; this MR leaves them out.
Follow-up for them: #637638
Screenshots or screen recordings
Not a UI change.
How to set up and validate locally
-
Send an MCP request with an unknown token:
curl -i -H "Authorization: Bearer glpat-notarealtokenatall0000" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-06-18"}}' "http://gdk.test:3000/api/v4/mcp"The
401carrieserror="invalid_token"in its challenge and body. -
Without the
Authorizationheader, the401is unchanged.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.