API: an unknown access token gets a 401 without invalid_token outside the MCP endpoints
Summary
Split from #634370 (closed), whose MCP part !266878 (merged) addresses. This issue keeps the rest: the API-wide change that issue left as a separate decision.
On the other API endpoints, a request with an access token GitLab does not know gets a 401 with no error, while an expired or revoked token gets error="invalid_token". RFC 6750 section 3.1 defines invalid_token for a token that is "expired, revoked, malformed, or invalid for other reasons".
The finders raise Gitlab::Auth::UnauthorizedError for a token they cannot find (auth_finders.rb#L439), and API::Helpers#initial_current_user answers it with unauthorized!. The guard maps Gitlab::Auth::TokenNotFoundError to invalid_token (api_guard.rb#L231-234), but outside the MCP endpoints nothing raises it.
Steps to reproduce
- Send
curl --include --header "Authorization: Bearer glpat-notarealtokenatall0000" "https://gitlab.example.com/api/v4/user". - Send the same request with a revoked personal access token.
Example Project
Not needed: the responses do not depend on any project.
What is the current bug behavior?
The unknown token gets 401, no WWW-Authenticate header, and {"message":"401 Unauthorized"}. The revoked one gets 401 with error="invalid_token" in the WWW-Authenticate header and the body.
What is the expected correct behavior?
An unknown token gets error="invalid_token" in the challenge and the body, as an expired or revoked one does. A request without a token keeps its 401 without error.
Relevant logs and/or screenshots
The responses are in the parent issue's "Relevant logs" section.
Output of checks
The code is cited from master at b7c44b3b2a1.
Possible fixes
Raise TokenNotFoundError instead of UnauthorizedError when a presented personal access token or OAuth token does not exist, so the guard's existing branch answers it. That changes the 401 of every endpoint using the default token lookup, so it may want a feature flag, and resolve_access_token, which rescues UnauthorizedError to fall back from an OAuth lookup to a personal access token, would need to handle the new error too. The override !266878 (merged) adds to API::Helpers::Mcp::AuthChallenge could then go.