Draft: fix(auth): keep is_oauth2 for an environment token glab stored itself
Note: WIP, may not be good path forward
What does this MR do and why?
Since 1.111.0, a host configured for OAuth whose token arrives through GITLAB_TOKEN
authenticates as a PAT (Private-Token) instead of as OAuth (Authorization: Bearer).
is_oauth2 from config.yml is discarded; only GLAB_IS_OAUTH2 is honoured.
That override comes from !3653 (merged) and is right for its target case, #8214 (closed): a glpat- in
GITLAB_TOKEN against an OAuth-configured host must not be sent as a Bearer token. It is
wrong for a token that came out of glab's own configuration. A wrapper script doing
export GITLAB_TOKEN="$(glab config get token --host gitlab.com)"hands glab back the token glab auth login stored, and glab then sends its own OAuth token
as a PAT and 401s. That pattern — one canonical GITLAB_TOKEN for a mixed toolchain — is
what a second reporter hit in #8482, on top of the Duo Agent Platform case that opened it.
This MR keeps the stored flag when the environment token is byte-identical to the token
stored for that host. Such a token is not foreign, so is_oauth2 still describes it
correctly. #8214 (closed) cannot regress: a credential the host knows nothing about differs from the
stored one by definition, so it still authenticates as a PAT.
Also in scope, both from the issue's remaining asks:
- The decision is logged with
dbg.Debugfeither way, so the auth scheme no longer changes with no signal at all. This was the part of #8482 I said was the genuine defect: callers whose setup commands are failure-tolerant see warnings in job logs rather than an error. GLAB_IS_OAUTH2is documented. It was the only fix for this and appeared in no user-facing documentation, only ininternal/config/schema.go. The newdocs/source/authentication.mdsection also warns that a copied OAuth access token cannot be refreshed in place, which is the next thing these scripts trip over.
Behaviour, before and after
is_oauth2: "true" stored for the host, no GLAB_IS_OAUTH2 set:
GITLAB_TOKEN |
Before | After |
|---|---|---|
| the token stored for the host | Private-Token |
Authorization: Bearer |
| any other token | Private-Token |
Private-Token (unchanged) |
glab auth status no longer suggests GLAB_IS_OAUTH2=true on a 401 for the first row: the
token is already sent as OAuth there, so that 401 means it expired.
Cost
On the narrow path where the environment supplies the token for an OAuth-configured host,
glab now reads the stored token to compare it. With use_keyring enabled that is one extra
keyring read. A read failure keeps the current behaviour rather than guessing, so a locked
keyring degrades to "authenticate as a PAT" instead of erroring.
Not in scope
An OAuth access token copied into the environment is pinned: the access token comes from the
environment while the expiry and refresh token come from config.yml, so once glab rotates
the stored token, the exported copy is sent against a still-valid-looking expiry and 401s
without a refresh attempt. Setting GLAB_IS_OAUTH2=true does not help there either. The
documentation now says so; the real fix is a glab auth token that refreshes before
printing, which I will file separately.
How to set up and validate locally
go test ./internal/api/ -run TestNewClientFromConfig_OAuthFlagProvenance -v
go test ./internal/config/ -run TestTokenMatchesStored -v
go test ./internal/commands/auth/status/ -run Test_statusRun_authFailureWithEnvTokenOnOAuthHost -vThe provenance matrix in internal/api/oauth_provenance_test.go pins every combination of
where the token and the flag came from, including the two rows added here.
Related: #8482, #8214 (closed), !3653 (merged)