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

  1. Send curl --include --header "Authorization: Bearer glpat-notarealtokenatall0000" "https://gitlab.example.com/api/v4/user".
  2. 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.

Edited by 🤖 GitLab Bot 🤖