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.Debugf either 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_OAUTH2 is documented. It was the only fix for this and appeared in no user-facing documentation, only in internal/config/schema.go. The new docs/source/authentication.md section 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 -v

The 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)

Edited by Jay McCure

Merge request reports

Loading
Loading