blob: 61da6f131b1785d64a2fad56307be1f2fcb25d12 [file] [view] [edit]
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.uid`s) 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
| Symptom | Cause |
|---|---|
| `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 mismatch | `token/info` `resource_owner_id` did not match the `/api/v4/user` id. |