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; this page covers the operator and end-user workflow once the plugin is configured.
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:
gho_*), orghu_*).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.
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.
[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/.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).enable-git-over-http = true the GitHub Git path is not installed and only browser login works.enable-git-over-http = true. If more than one is enabled the plugin refuses to start.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:
200 as a token issued for this app (404/other statuses are rejected);app.client_id equals the configured client-id;user.id to the external id and user.login to the username.Any failure surfaces to git as 401.
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:
ghu_*) expire, so check-token returns expires_at and the result is cached until that instant (bounded by the hard TTL).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
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:
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.
Introspect the token the same way the plugin does (as the app):
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).
| 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. |