fix(auth): use the configured subfolder when refreshing OAuth tokens

Why

Reported in #8532 (closed): on a self-managed instance installed under a subfolder, refreshing an expired OAuth2 access token goes to

https://gitlab.example.com/oauth/token

instead of

https://gitlab.example.com/<subfolder>/oauth/token

so the refresh reaches the wrong endpoint and fails.

NewConfigTokenSource built its endpoint from the hostname alone:

oauth2Config := gitlaboauth2.NewOAuth2Config(fmt.Sprintf("%s://%s", protocol, hostname), clientID, redirectURL, scopes)

which is the same construction #8399 (closed) replaced for the authorization URL. That fix (!3604 (merged)) added oauthBaseURL and routed StartFlow and the device flow through it — but the refresh path kept building its own URL, and so kept ignoring hosts.<hostname>.subfolder.

It was the only remaining site. Every OAuth base URL in internal/:

site before
oauth2.go (authorization) oauthBaseURL ✅
device.go (device flow) oauthBaseURL ✅
token_source.go (refresh) hand-built from hostname ❌

What

oauthBaseURL takes a protocol, and all three flows call it, so the base URL is built in one place and a future flow cannot reintroduce the omission by writing its own.

Nothing else changes. AuthEndpoint already falls back to DefaultProtocol on an empty string and trims slashes around the subfolder, so the two existing callers keep their exact behaviour.

Verification

check result
go test -race ./internal/oauth2/... ok
go build ./..., go vet ./internal/oauth2/... clean

The regression test drives a real refresh against an httptest server with an expired token and asserts the path the server actually received:

subfolder expected path
gitlab /gitlab/oauth/token
/gitlab/ /gitlab/oauth/token
(none) /oauth/token

It asserts on the received path rather than on the constructed oauth2.Config, because the constructed value is exactly what looked right before and was wrong once the URL left the helper.

Mutation-tested. Restoring the old fmt.Sprintf("%s://%s", protocol, hostname):

--- FAIL: .../subfolder_configured           expected: "/gitlab/oauth/token"
--- FAIL: .../surrounding_slashes_are_trimmed expected: "/gitlab/oauth/token"

Both subfolder cases fail and no subfolder still passes, so the test is pinning the subfolder specifically and not just any change to the URL.

Closes #8532 (closed)

🤖 Generated with Claude Code

Merge request reports

Loading
Loading