| Git-over-HTTP with GitHub (experimental) |
| ======================================== |
| |
| > **Experimental.** Git-over-HTTP support for the GitHub provider is opt-in. |
| > GitHub access tokens are opaque, so they are validated remotely at GitHub's |
| > "check a token" endpoint and successful, expiring results are cached. |
| > Operational details (log lines, cache behaviour, claim handling) may change |
| > between minor versions. |
| |
| How to push and fetch over HTTPS against a Gerrit instance whose OAuth provider |
| is GitHub (github.com or GitHub Enterprise Server). The plugin-side configuration |
| is in [config.md](config.md); this page covers the operator and end-user workflow |
| once the plugin is configured. |
| |
| ## What the client presents: an access token issued for Gerrit's OAuth app |
| |
| GitHub is not OIDC and issues opaque tokens, so the Git credential cannot be |
| validated locally. The plugin introspects it at GitHub's "check a token" endpoint |
| (`POST /applications/{client_id}/token`), authenticating **as the OAuth app** |
| with `client-id:client-secret`. The token must be one **issued for this app**: |
| |
| - a classic OAuth-App user token (`gho_*`), or |
| - a GitHub-App OAuth user token (`ghu_*`). |
| |
| An HTTP `200` means the token exists and was issued for this app — GitHub's |
| audience check. A token issued for a *different* app returns `404` and is |
| **rejected**; a classic personal access token (`ghp_*`) is not tied to an app and |
| is likewise rejected. The response's `app.client_id` is re-checked against the |
| configured `client-id`. |
| |
| The browser login flow still fetches the profile (`id`, `login`, `email`, `name`) |
| from `GET /user`, but now also validates the token at check-token and binds |
| check-token's user `id` to the `/user` `id`, so the browser token gets the same |
| per-app audience check. |
| |
| ## Prerequisite — both client-id and client-secret are required |
| |
| The check-token endpoint is authenticated as the OAuth app, so **both** |
| `client-id` and `client-secret` must be configured. If either is missing the |
| plugin refuses to install the GitHub Git path and Gerrit fails to start with: |
| |
| ``` |
| GitHub token validation requires both client-id and client-secret to introspect |
| tokens at the check-token endpoint. |
| ``` |
| |
| ## Configuration |
| |
| ``` |
| [auth] |
| type = OAUTH |
| gitBasicAuthPolicy = OAUTH |
| |
| [plugin "gerrit-oauth-provider-github-oauth"] |
| root-url = https://github.com/ # or https://github.example.com/ for GHES |
| client-id = ... |
| client-secret = ... |
| enable-git-over-http = true |
| # enable-pkce = true # optional; when true the browser authorization-code flow uses PKCE |
| ``` |
| |
| - **`auth.type = OAUTH`** is required. With it, Gerrit already defaults |
| `gitBasicAuthPolicy` to `OAUTH`, so a Basic-auth password is accepted as an |
| OAuth token. The `gitBasicAuthPolicy = OAUTH` line is shown explicitly so a |
| deployment that overrides that policy site-wide (e.g. to `HTTP` or `LDAP`) does |
| not accidentally route Git/HTTP through password auth instead. |
| - **`root-url`** selects the API base: `https://github.com/` uses |
| `https://api.github.com/`; a GitHub Enterprise Server URL uses that host's |
| `.../api/v3/`. |
| - The check-token call is made only over `https` or to a loopback host, so a |
| mistyped GHES `root-url` cannot leak the token in cleartext. Point `root-url` at |
| an `https` GHES (or a loopback host for local testing). |
| - Git-over-HTTP is **opt-in per provider**; without `enable-git-over-http = true` |
| the GitHub Git path is not installed and only browser login works. |
| - Exactly one OAuth provider may have `enable-git-over-http = true`. If more than |
| one is enabled the plugin refuses to start. |
| |
| ## 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 `GitHubOAuthLoginProvider` POSTs the token to |
| `.../applications/{client_id}/token`, authenticated as the app, and: |
| |
| - treats `200` as a token issued for this app (`404`/other statuses are rejected); |
| - re-checks the response's `app.client_id` equals the configured `client-id`; |
| - maps the response `user.id` to the external id and `user.login` to the username. |
| |
| Any failure surfaces to git as `401`. |
| |
| ## Validation-result cache |
| |
| Each opaque-token validation is one HTTPS call to check-token. To avoid |
| re-validating the same token for every object during a push or fetch, successful |
| results are cached in the `oauth_token_validation` cache, keyed by a SHA-256 of |
| the token (never the raw token). An entry lives for the **lesser of** the token's |
| own expiry and a hard TTL (60 s by default). Only successful validations are |
| cached; failures never are. |
| |
| Note the token type matters here: |
| |
| - **GitHub-App user tokens (`ghu_*`)** expire, so check-token returns |
| `expires_at` and the result is cached until that instant (bounded by the hard |
| TTL). |
| - **Classic OAuth-App tokens (`gho_*`)** do not expire and carry no `expires_at`; |
| with no upper bound they are **not cached**, so each request re-checks at |
| GitHub. Lower `maxAge` does not change this — it only bounds the expiring case. |
| |
| Tune it in `gerrit.config`. The cache is plugin-qualified, so the section name is |
| `<plugin-name>.oauth_token_validation` (plugin name, a dot, then the cache name): |
| |
| ``` |
| [cache "gerrit-oauth-provider.oauth_token_validation"] |
| maxAge = 60s # hard upper bound on how long a validation result is reused |
| memoryLimit = 10000 # maximum number of cached tokens |
| ``` |
| |
| Hit-rate and eviction statistics (the REST/registry name uses hyphens): |
| `/a/config/server/caches/gerrit-oauth-provider-oauth_token_validation`. |
| |
| Under high-availability or multi-site this cache is **not** eviction-synced by |
| default (their default patterns cover only core caches). It is a short-lived |
| (default 60 s `maxAge`) in-memory cache keyed by a token hash, so independent |
| per-node validation is usually fine. To sync evictions anyway, add a repeatable |
| `cache.pattern` (singular) entry in the plugin's config |
| (`high-availability.config` or `multi-site.config`) matching the dot-qualified |
| name: |
| |
| [cache] |
| pattern = gerrit-oauth-provider.oauth_token_validation |
| |
| ## Token acquisition |
| |
| The credential is a GitHub access token issued for **Gerrit's** OAuth app. |
| |
| Developer machines must **not** be given Gerrit's `client-secret`. Acquire the |
| token with a flow that does not need it — for example the GitHub **OAuth device |
| flow** on Gerrit's OAuth app (client-id only), via a credential helper such as |
| `git-credential-oauth`. The device flow works only if the OAuth app has *Enable |
| Device Flow* turned on in its GitHub settings. Reserve secret-bearing acquisition |
| (the authorization-code grant that uses `client-secret`) for trusted, |
| operator-controlled automation. |
| |
| Present the token without putting it in the URL — a token in the URL leaks into |
| shell history, `ps`, and git trace logs: |
| |
| ```bash |
| git -c http.extraHeader="Authorization: Bearer $TOKEN" \ |
| ls-remote -h https://gerrit.example.com/a/myproject |
| ``` |
| |
| A classic personal access token (`ghp_*`) will not work — it is not issued for |
| the OAuth app and check-token rejects it. |
| |
| ## Verifying the token without git |
| |
| Introspect the token the same way the plugin does (as the app): |
| |
| ```bash |
| curl -sS -u "<client-id>:<client-secret>" \ |
| -H "Accept: application/vnd.github+json" \ |
| https://api.github.com/applications/<client-id>/token \ |
| -d '{"access_token":"'"$TOKEN"'"}' |
| ``` |
| |
| On GitHub Enterprise Server use the host's `/api/v3/` base instead: |
| `https://github.example.com/api/v3/applications/<client-id>/token`. |
| |
| A `200` with an `app.client_id` equal to your `client-id` means the token is |
| valid for this app; `404` means it was issued for a different app (or revoked). |
| |
| ## Troubleshooting |
| |
| | Symptom | Likely cause | |
| |---|---| |
| | Startup error "requires both client-id and client-secret" | Only one of `client-id`/`client-secret` is set. Configure both. | |
| | `401` with "does not belong to this Gerrit's GitHub OAuth app" | The token was issued for a different app, or is a `ghp_*` PAT. Use a token issued for Gerrit's OAuth app. | |
| | `401` with "rejected client credentials (HTTP 401)" | Gerrit's `client-id`/`client-secret` are wrong, not the user's token. | |
| | `401` with "belongs to a different GitHub OAuth app" | The check-token response's `app.client_id` did not match the configured `client-id`. | |
| | `401` with "Authentication error: username does not match" | The client-supplied username differs from the token owner's GitHub login. | |
| | `403 forbidden` after authenticating | Authentication succeeded but the user lacks ACL permission. | |