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_TOKENdeclares OAuth.GITLAB_TOKENandGITLAB_ACCESS_TOKENare 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_OAUTH2stays 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:
glab auth git-credential(internal/commands/auth/login/helper.go:118) still decides the username purely from storedis_oauth2, so an OAuth token inOAUTH_TOKENgetsusername=personal-access-tokenover 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.GLAB_IS_OAUTH2compares exactly againsttrue, soGLAB_IS_OAUTH2=1means PAT, unlikeGLAB_NO_PROMPTwhich accepts1.
Related to #8482.