fix(auth): send OAUTH_TOKEN as an OAuth token

What this fixes

The token configuration key resolves from three environment variables (internal/config/schema.go:245), but the guard added in !3653 (merged) that stops an environment token inheriting a host's stored is_oauth2 flag named only two of them (internal/api/client.go). A token supplied through OAUTH_TOKEN therefore took its scheme from the configuration file:

Environment Stored is_oauth2 Header sent on main
OAUTH_TOKEN=… (none) Private-Token:
OAUTH_TOKEN=… "true" Authorization: Bearer

A variable named OAUTH_TOKEN was sent as a personal access token whenever the host had no stored credentials — the container/CI shape described in #8482, reached through the one variable whose name already declares the answer.

What changed

An environment token's scheme now follows the variable that supplied it:

  • OAUTH_TOKEN declares OAuth.
  • GITLAB_TOKEN and GITLAB_ACCESS_TOKEN are provenance-neutral, so they keep defaulting to a personal access token rather than inheriting the stored flag. !3653 (merged)'s isolation is intact.
  • GLAB_IS_OAUTH2 stays authoritative in both directions.

Exactly two cells of the matrix change, both for OAUTH_TOKEN with no explicit GLAB_IS_OAUTH2: stored is_oauth2 absent, and stored is_oauth2: "false".

The variable list was hand-enumerated in two places that had already drifted, so a test now asserts every entry of EnvKeyEquivalence("token") is handled by EnvironmentTokenIsOAuth2. Adding a fourth variable to the schema fails that test rather than silently inheriting the stored flag.

glab auth status gained a matching hint. Its existing scheme warning is gated on stored OAuth credentials, so a token injected where nothing is stored got only the generic advice about a wrapper injecting a bad token. It now names the header used when nothing is stored for the host, and stays quiet for OAUTH_TOKEN, for an explicit GLAB_IS_OAUTH2, and for hosts with a stored personal access token (where an expired token is the likelier cause).

Not a behaviour break

A personal access token left in OAUTH_TOKEN is now sent as Bearer. Verified against gitlab.com that GitLab accepts a personal access token in either header (both 200), so that arrangement keeps working. Only the reverse fails: an OAuth token in Private-Token returns 401. GLAB_IS_OAUTH2=false restores the previous header either way.

Documentation

docs/source/authentication.md states the rule in three lines, documents the precedence order between the three variables, and drops the previous claim that a personal access token sent as Authorization: Bearer fails — it does not.

The section no longer shows how to re-export a stored OAuth token into a variable. glab pairs an environment copy with the stored expiry date and refresh token, so the copy can spend the single-use refresh token, persist the rotated one, and then keep being sent stale. Recommending the copy and then warning against it in the next paragraph was contradictory; the advice is now just "don't".

Review notes

Reviewed against Kai's findings in !3787 (closed) (that MR is documentation-only for the same area and is superseded by this one — happy to close it).

Deliberate divergence from that review worth a second opinion: it concluded the auth status gate could stay as it is and only the prose needed scoping. That reasoning predates OAUTH_TOKEN carrying meaning, so the hint is included here.

Known gaps left out of scope, both pre-existing:

  1. glab auth git-credential (internal/commands/auth/login/helper.go:118) still decides the username purely from stored is_oauth2, so an OAuth token in OAUTH_TOKEN gets username=personal-access-token over git-HTTPS. Both paths were wrong for that config before this change, so this is not a regression, but they now disagree. Fixing it means teaching that branch to handle an access-token-only source, which needs its own review.
  2. GLAB_IS_OAUTH2 compares exactly against true, so GLAB_IS_OAUTH2=1 means PAT, unlike GLAB_NO_PROMPT which accepts 1.

Related to #8482.

Merge request reports

Loading
Loading