Git-over-HTTP with GitLab (experimental)

Experimental. Git-over-HTTP support for the GitLab provider is opt-in.

What the client presents: an opaque GitLab access token

GitLab OAuth access tokens are opaque (not JWTs), so they cannot be validated locally. The plugin introspects the token at GitLab's /oauth/token/info endpoint and then reads the profile from /api/v4/user. The token must:

  • be issued for a trusted OAuth application — token/info's application.uid must be the configured client-id or one of the trusted-audience entries. A token minted for any other GitLab application is rejected;
  • carry the required scope(s) — token/info's scope must include every required-scope (default read_user);
  • resolve to a user whose token/info resource_owner_id matches the /api/v4/user id (subject binding).

Personal, Project, and Group access tokens are not OAuth-application tokens; they carry no matching application.uid and are therefore rejected.

Configuration

[auth]
  type = OAUTH
  gitBasicAuthPolicy = OAUTH

[plugin "gerrit-oauth-provider-gitlab-oauth"]
  root-url = https://gitlab.com/     # or https://gitlab.example.com/ for self-managed
  client-id = ...
  client-secret = ...
  enable-git-over-http = true
  # required-scope = read_user       # optional, repeatable; default read_user
  # trusted-audience = <helper-client-id>  # optional, repeatable; see "Token acquisition"
  • auth.type = OAUTH is required; gitBasicAuthPolicy = OAUTH is shown explicitly so a site-wide override does not route Git/HTTP through password auth.
  • Git-over-HTTP is opt-in per provider; without enable-git-over-http = true the GitLab Git path is not installed and only browser login works.
  • Exactly one OAuth provider may have enable-git-over-http = true.
  • required-scope (repeatable) is the defense-in-depth scope gate. It defaults to read_user (the minimum for /api/v4/user); set it to api or a narrower custom scope to require more.
  • trusted-audience (repeatable, optional) lists additional GitLab OAuth application ids (application.uids) whose tokens the Git path accepts, beyond client-id. Leave it unset and only browser (client-id) tokens pass — existing deployments are unchanged. Set it to register a separate client (a git credential helper's own client, or a CI client); see “Token acquisition”.
  • token/info and /api/v4/user are called only over https or to a loopback host, so a mistyped root-url cannot leak the token in cleartext. Point root-url at an https GitLab (or a loopback host for local testing).

Token refresh

Set enable-token-refresh = true (default false) to make GitLab refresh-capable; GitLab issues a refresh_token for the authorization-code flow by default, so no extra scope is requested. An expired token is renewed on read:

  • browser/REST reads (GET /accounts/{id}/oauthtoken) refresh it via OAuthTokenRefresher before returning it; and
  • the core oauth-token SSH command refreshes on demand and needs only enable-token-refresh on the issuing provider — not enable-git-over-http.

Neither refreshes during a Git-over-HTTP request, which validates the presented opaque token via /oauth/token/info as before.

How validation works on the wire

Every Git-over-HTTP request carries an Authorization header:

Authorization: Basic <base64("<username>:<access-token>")>

or

Authorization: Bearer <access-token>

The plugin's GitLabTokenInfoValidator:

  • GET <root>/oauth/token/info (Bearer) — checks application.uid is a trusted audience (client-id or a trusted-audience entry), scope ⊇ required-scope, and reads resource_owner_id;
  • GET <root>/api/v4/user (Bearer) — reads id/username/email/name/state;
  • binds resource_owner_id == /api/v4/user id, mapping the user the same way the browser flow does (shared GitLabUserInfoMapper);
  • rejects the token unless state is active, so a valid token for a blocked, deactivated, banned, or ldap_blocked account cannot push;
  • caches successes (see below). Any failure surfaces to git as 401.

Validation-result cache

Successful validations are cached in the oauth_token_validation cache (keyed by a SHA-256 of the token, never the raw token) for the lesser of the token's own expiry (expires_in from token/info) and a hard TTL (60 s by default). Tokens without an expires_in are not cached. Tune it under the plugin-qualified name:

[cache "gerrit-oauth-provider.oauth_token_validation"]
  maxAge = 60s
  memoryLimit = 10000

Statistics (the REST/registry name uses hyphens): /a/config/server/caches/gerrit-oauth-provider-oauth_token_validation.

Token acquisition (git credential helpers)

Unlike Google, GitLab has true public OAuth clients (PKCE, loopback, device flow), so off-the-shelf credential helpers such as git-credential-oauth can obtain and refresh tokens for git clone/fetch/push. The Git path accepts a token only if its token/info application.uid is a trusted audience, so the helper's client must be one Gerrit trusts.

Recommended: a separate public helper app + trusted-audience. Register a dedicated non-confidential GitLab OAuth application for the helper (public / PKCE, no client secret), keep Gerrit‘s browser client-id for SSO, and list the helper’s client id in trusted-audience:

[plugin "gerrit-oauth-provider-gitlab-oauth"]
  client-id        = <browser-client-id>   # Gerrit browser SSO
  trusted-audience = <helper-client-id>    # git credential helper / CI client
  enable-git-over-http = true

A public helper app needs no secret to distribute, which is exactly why a normal helper can use it. Gerrit's browser SSO app is typically confidential (has a secret); reusing it for a helper would mean handing that secret to every client, defeating the point of a separate audience.

Alternative: point the helper at Gerrit's client-id. Only appropriate if Gerrit's app is itself usable as a public/PKCE client, or for trusted, secret-capable automation where distributing the client secret is acceptable. For git-credential-oauth, set oauthClientId to that application.

git-credential-oauth endpoint gotcha. Configure the helper under the Gerrit remote host (that is the URL git authenticates against), but point its OAuth endpoints at GitLab — the helper cannot infer a GitLab host from a https://gerrit.example.com/... remote, and a relative /oauth/authorize would resolve against Gerrit, not GitLab:

git config --global credential.https://gerrit.example.com.helper oauth
git config --global credential.https://gerrit.example.com.oauthClientId <helper-client-id>
git config --global credential.https://gerrit.example.com.oauthScopes read_user
git config --global credential.https://gerrit.example.com.oauthAuthURL https://gitlab.example.com/oauth/authorize
git config --global credential.https://gerrit.example.com.oauthTokenURL https://gitlab.example.com/oauth/token

The read_user scope above is sized for Gerrit validation only — it is all the validator needs (application.uid + /api/v4/user + required-scope). Add any other scopes your helper or workflow expects (upstream GitLab examples often add read_repository/write_repository); a too-narrow scope surfaces as a helper-side failure, not a Gerrit auth failure.

Either way, request at least read_user (or whatever required-scope demands). Personal/Project/Group access tokens carry no application.uid and are rejected.

Troubleshooting

SymptomCause
401, “Token belongs to a different GitLab application”The token's application.uid is neither client-id nor a trusted-audience entry. Use a token issued for a trusted app, or add its client id to trusted-audience.
401, “Token is missing the required scope”The token lacks a required-scope value (default read_user).
401, subject mismatchtoken/info resource_owner_id did not match the /api/v4/user id.