---
name: github-oauth-app
description: Registers a GitHub OAuth App or a GitHub App to obtain OAuth2 credentials — client ID, client secret, and for a GitHub App the private key that mints installation tokens — with the right callback URLs, scopes or fine-grained permissions, org approval path, and a safe credential handoff. Use when asked to get GitHub OAuth credentials, create a GitHub OAuth App or GitHub App, choose between the two, register a GitHub App from a manifest, rotate a GitHub client secret or private key, or fix a GitHub authorization error like `redirect_uri_mismatch`, `incorrect_client_credentials`, `bad_verification_code`, or a customer whose org owner has not approved the app. Covers GitHub.com, GitHub Enterprise Cloud and GitHub Enterprise Server. For any other vendor's developer portal, use that vendor's skill instead.
---

# GitHub OAuth2 App Registration

Get a working GitHub OAuth2 client for a platform that connects many customers' GitHub accounts. A successful run
produces: a registered app, every callback URL, the scope set or permission set, a client ID, a client secret, and —
for a GitHub App — a downloaded private key and a webhook secret.

**The expensive decision comes before any of that: OAuth App or GitHub App.** They are two different products with
two different credential shapes, two permission models, two token lifecycles, two rate-limit formulas, and two
install stories. Choosing wrong is not a settings change — the client ID differs, so migrating re-onboards every
customer you already have. Read §A before you click **New**.

Three other things cost a cycle if missed: **callback URL wildcard matching became a per-URL setting on 2026-08-03**,
so a new app matches redirect URIs exactly and multi-region callbacks must be sent explicitly (§4); the GitHub App
**private key is downloadable exactly once** and is as sensitive as the client secret (§6); and an org owner can
block both app types independently of anything you configure (§8).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **OAuth App or GitHub App** | The fork. If the user has not decided, walk §A with them before anything else |
| **App name**, description, homepage URL | The name must be unique across GitHub; GitHub App names cap at 34 characters |
| **Owner account** | A personal account or an organization the user administers (§2) |
| **Callback URLs** | Every callback host the platform serves (§4) |
| **Scope set / permission set** | What the connector actually calls (§5) |
| **Public or private install** | "Any account" (multi-tenant) vs "Only on this account" (§3) |
| **Webhook URL + whether webhooks are used** | GitHub Apps get centralized webhooks; a webhook secret is generated here (§3) |
| **GitHub.com, Enterprise Cloud, or Enterprise Server** | GHES needs its own registration on the customer's instance (§9) |
| **New app, or an edit to an existing one** | Re-registration orphans existing connections (§1) |

## Quick Start

1. Decide **OAuth App vs GitHub App** — this is the run's real decision (§A).
2. Confirm a **new** app is needed; an edit keeps every existing customer connected (§1).
3. Sign in as the owning personal account or org admin (§2).
4. Register the app — **Settings → Developer settings → GitHub Apps / OAuth Apps → New** (§3).
5. Add **every** callback URL, and decide wildcard matching deliberately (§4).
6. Set scopes (OAuth App) or fine-grained permissions and events (GitHub App) (§5).
7. Capture client ID, client secret, and — GitHub App only — **generate and download the private key** (§6).
8. Confirm token lifetimes and that the client stores rotated refresh tokens (§7).
9. Work the org-approval path customers will hit (§8).
10. Verify with a real authorize → callback → refresh → read round trip on a **second** account (§10).
11. Hand the credentials over; never commit them (§11).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **GitHub's own guidance is that GitHub Apps are preferred to OAuth Apps**, for finer permissions, short-lived
  tokens and independence from the installing user. OAuth Apps are not deprecated and no sunset has been announced;
  GitHub names enterprise-level resources as the case where an OAuth App is still the answer.
- **Callback URL wildcard matching is now an explicit per-URL setting.** Apps that had a *single* callback URL
  enabled before **2026-08-03** have wildcard matching on, preserving the old behaviour — which is why nearly every
  OAuth App and many older GitHub Apps behave loosely while a freshly registered app matches exactly. GitHub warns
  that wildcard matching lets an attacker send authorization codes to any subdomain or subdirectory of the callback
  URL. Leave it off.
- **Both app types now expire user tokens by default.** Newly created OAuth Apps and GitHub Apps issue user access
  tokens valid **8 hours** (`expires_in: 28800`) with a refresh token valid **6 months**
  (`refresh_token_expires_in: 15897600`). The old never-expiring OAuth token is an opt-out, not the default.
- **Installation access tokens are moving to a stateless format.** From **2026-04-27** GitHub began a staged rollout
  of `ghs_APPID_JWT`-style installation tokens. They are no longer the old fixed 40-character strings. Any code,
  column or validator that assumes a token length will start failing mid-rollout.
- **Up to 10 callback URLs** per app, both types. **Up to 25 private keys** per GitHub App. A user or org may own
  **up to 100 OAuth Apps**.
- **Rate limits** (§7) differ by an order of magnitude between the two models, and that difference grows with the
  customer.

If the developer settings pages do not look like this, stop and report what you actually see rather than clicking on.

## A. OAuth App vs GitHub App — decide this first

| | **OAuth App** | **GitHub App** |
| --- | --- | --- |
| **Who grants access** | An individual user authorizes it | An org owner or repo admin **installs** it, picking repositories |
| **Access granularity** | Coarse scopes; `repo` is effectively all-or-nothing on private repos | Fine-grained repository / organization / account permissions, each read, write or admin |
| **Repository targeting** | Every repo the user can see, within the scope | Only the repositories chosen at install |
| **Credentials** | Client ID + client secret | Client ID + client secret **+ a private key (.pem)**, plus a webhook secret |
| **Token types** | User access token only | **Installation** token (acts as the app/bot) and **user-to-server** token (acts as the user) |
| **Token lifetime** | 8h user token + 6-month refresh token (default) | Installation token: **1 hour**, re-minted from the private key. User token: 8h + 6-month refresh |
| **Identity in the API** | The authorizing user (`@octocat`) | A bot (`@yourapp[bot]`) for installation tokens |
| **Survives the user leaving** | No — access dies with the user's membership or token | Yes — the installation is owned by the org |
| **Rate limit** | 5,000 req/hr, fixed (15,000 for an Enterprise Cloud-owned app acting for a user) | 5,000 base, **+50/hr per repo above 20 and +50/hr per org user above 20, to a 12,500 ceiling**; 15,000 flat for Enterprise Cloud installs |
| **Webhooks** | Configure per repository and per org yourself | One centralized webhook for everything the installation covers |
| **Org gatekeeping** | Org **OAuth App access restrictions** — owner approves the app for the org | Org controls who may **install**, and permission changes need re-approval |
| **Enterprise-level resources** | Still the supported path | Limited |
| **Scopes in the authorize URL** | Required, space-delimited | **Not accepted** — access comes from the app's configured permissions |

**For a multi-tenant connector, register a GitHub App.** The reasons are cumulative, not stylistic: customer security
teams will refuse a connector that asks for blanket `repo`; installation tokens keep working when the employee who
connected the integration leaves; the installation rate limit grows with the customer instead of throttling your
largest accounts at a flat 5,000/hr; and one centralized webhook replaces per-repository webhook plumbing.

**Choose an OAuth App only when** the connector genuinely needs to act *as the user* across everything that user can
see (a code-search or "show me all my repos" product), when it must reach enterprise-level resources, or when the
platform's connector already exists as an OAuth App and you are just adding a callback or a scope.

**Switching later is a migration, not a config change.** A GitHub App has a different client ID, a different consent
screen, an install step the OAuth flow does not have, and tokens that cannot be carried over. Every existing customer
re-authorizes. Plan it as a project with a dual-run window, or do not start it.

## 1. Reuse the existing app, or register a new one

A new registration means a new client ID, and every stored token is bound to the old one — every customer
re-authorizes. **Edit the existing app** for: adding a callback URL, adding a scope or permission, rotating a
compromised secret, enabling device flow, or diagnosing an authorization failure.

**Register a new app** only when the user asks for one: a replacement for a compromised app, a separate app for a
separate product, a GHES instance that needs its own (§9), or a deliberate OAuth App → GitHub App migration. Say
which path you are taking before you start clicking.

One asymmetry worth knowing: **adding permissions to an existing GitHub App is not free either.** Each account where
the app is installed is prompted to approve the new permissions, and an installation that declines keeps running on
the old ones. So a permission change ships as a slow, partial rollout you have to tolerate in code — not as a flag
day. Adding an OAuth scope has no such prompt; the user simply re-authorizes and the new scope appears on the token.

## 2. Account: sign in or sign up

Apps can be owned by a **personal account** or by an **organization** the user administers. Prefer an organization
for anything a team will maintain — a personal-account app dies with the person's access. Ownership is chosen at
registration.

Sign-up, email verification, 2FA (GitHub requires it), accepting terms, and organization membership are things only
the human can do. Hand back rather than looping on a blocked step.

**If this session has no browser automation** — the usual case for a CLI or cloud run — do not pretend to click.
Give the user an exact, ordered click path with the literal values to paste (§4 callback URLs, §5 scopes or
permissions), then continue once they report back with the client ID. For a GitHub App there is a scripted
alternative: the **manifest flow** (§3) hands the whole registration over as one URL.

## 3. Create the app

**GitHub App** — Settings → Developer settings → **GitHub Apps** → **New GitHub App**
(`https://github.com/settings/apps/new`; sign-in required, so this URL 403s to an unauthenticated fetch).

Fields that carry more weight than they look:

- **GitHub App name** — unique across all of GitHub, max 34 characters. It becomes the bot identity and the install
  URL slug, so it is hard to change later without confusing customers.
- **Callback URL** — up to 10 (§4).
- **Expire user authorization tokens** — leave selected. GitHub recommends it and it is the default.
- **Request user authorization (OAuth) during installation** — decides whether a fresh install also returns an
  authorization `code`. Read the install-callback trap below before choosing.
- **Setup URL** + **Redirect on update** — where GitHub sends users after an install, and whether it also does so
  when an installation is *modified* (repositories added or removed).
- **Webhook URL / Active / Webhook secret** — set a secret. GitHub signs deliveries with HMAC-SHA256 in the
  `X-Hub-Signature-256` header; without a secret you cannot tell a real delivery from a forged one.
- **Permissions** and **Subscribe to events** (§5). An event you lack the permission for will not even appear in the
  list.
- **Where can this GitHub App be installed?** — **Any account** for a multi-tenant connector; **Only on this
  account** restricts installation to the owning account.

> **The install-callback trap.** A *fresh* install redirects to the **setup URL** with an `installation_id` — and no
> authorization code, unless you enabled "Request user authorization (OAuth) during installation". A user who is
> *already* installed and simply authorizes comes back to the **callback URL** with a `code` and no
> `installation_id`. A connector must handle both shapes, and resolve the installation from a user token in the
> second case. GitHub also warns explicitly that **a bad actor can hit the setup URL with a spoofed
> `installation_id`** — never mint a token from a callback-supplied installation id without first generating a user
> access token and confirming that installation belongs to that user.

**Manifest flow** (GitHub App only, and the best option for a scripted or repeatable registration): POST a JSON
manifest — name, URLs, `hook_attributes`, permissions, events, callback URLs — to GitHub's app-creation endpoint, let
the user name and confirm it, and GitHub redirects back with a temporary code. **Exchange that code within one hour**
for the app's ID, its **`pem` private key**, and a generated **webhook secret**. There is also a URL-parameter
variant (`https://github.com/settings/apps/new?name=…&public=true&request_oauth_on_install=true&…`) that pre-fills
the form for a human to review — the right tool when you have no browser automation.

**OAuth App** — Settings → Developer settings → **OAuth Apps** → **New OAuth App**
(`https://github.com/settings/developers`). Fields: application name, homepage URL, description, authorization
callback URL (up to 10), **Enable Device Flow** (leave off unless asked — GitHub flags it as a phishing risk), and
token expiration, which is on by default.

## 4. Callback URLs

Register **every** callback host the platform serves. GitHub accepts up to 10 per app, so all four fit in one
registration. For Unified.to these are one per data center; confirm the current list with the platform owner rather
than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Rules that bite:

- **If `redirect_uri` is omitted, GitHub uses the first callback URL.** With four data centers registered, an
  authorize request that forgets `redirect_uri` silently sends EU customers to the US callback. Always send it.
- **Wildcard matching is now opt-in per URL** (see Platform state). With it off — the default for anything registered
  after 2026-08-03 — the redirect URI must match exactly. With it on, host (excluding subdomains) and port must match
  and the path must be a subdirectory, so `https://oauth.example.com/path` and `https://example.com/path/sub` match
  `https://example.com/path`, but a different port or a sibling path does not. **Leave it off** for fixed,
  server-side callbacks like these.
- A mismatch surfaces as `redirect_uri_mismatch` at connect time, not at save time.
- Loopback is special-cased for desktop clients: literal `127.0.0.1` (not `localhost`) may use a different port than
  the registered URL. Irrelevant to a server-side connector, and not a workaround for a wrong callback.

## 5. Scopes (OAuth App) or permissions (GitHub App)

**OAuth App scopes** are space-delimited in the authorize URL (`%20`-encoded). GitHub normalizes overlapping
requests, so asking for `user`, `user:email` and `read:user` together yields one non-redundant token.

| Scope | Grants |
| --- | --- |
| `repo` | Full control of private and public repositories — code, statuses, deployments, invitations, collaborators, webhooks, **and** org-owned projects, invitations, team memberships and webhooks |
| `public_repo` | The same, public repositories only |
| `repo:status` | Commit statuses without code access |
| `read:org` | Read org membership, projects and team membership |
| `admin:org` | Fully manage the org, its teams, projects and memberships |
| `user` | Read/write profile info; includes `user:email` and `user:follow` |
| `read:user` / `user:email` | Read profile / read email addresses |
| `workflow`, `gist`, `notifications`, `project` | Feature-specific |

**The `repo` problem.** There is no read-only private-repository scope. Anything that reads a private repo's contents
needs `repo`, which also grants *write* to code and reaches org-owned resources. That is the single biggest reason
customer security reviews reject an OAuth App connector, and it is not solvable by wording the consent screen better
— it is solvable by registering a GitHub App with `Contents: read`.

**GitHub App permissions** replace scopes entirely; the authorize URL does not accept a `scope` parameter for a
GitHub App client. Permissions are grouped into **repository**, **organization**, **account** (and, on enterprise
accounts, **enterprise**) sets, each set to read, write or admin. Pick the minimum: a user-to-server token gets the
**intersection** of the app's permissions and the user's own, so over-requesting does not buy reach — it only costs
you approvals. Webhook events are gated by the matching permission.

## 6. Capture the credentials

From the app's settings page:

- **Client ID** — public, stable. Note that a GitHub App's client ID is **not** its App ID; both are shown, and they
  are used in different places.
- **Client secret** — **Generate a new client secret**. Copy it at generation; treat it as show-once and generate a
  replacement if it is lost. To rotate without downtime, generate the new secret, deploy it, then delete the old one.
- **Private key — GitHub App only, and this is the part people lose.** Click **Generate a private key**; the browser
  downloads a **PKCS#1** `.pem` **once**. GitHub keeps only the public half; there is no way to re-download it. This
  key signs the app JWT that mints installation access tokens, so anyone holding it can act as your app on every
  installation — **it is at least as sensitive as the client secret.** An app may hold up to **25** keys, which is
  how you rotate: generate the new one, deploy, then delete the old. Keys do not expire; they must be revoked
  manually. Verify a key matches what GitHub holds by comparing SHA-256 fingerprints:

  ```bash
  openssl rsa -in PATH_TO_PEM_FILE -pubout -outform DER | openssl sha256 -binary | openssl base64
  ```

- **Webhook secret** — GitHub App only; store it wherever the webhook receiver reads it.
- **Endpoints** — `https://github.com/login/oauth/authorize`, `https://github.com/login/oauth/access_token`,
  API base `https://api.github.com`. On GHES all three live on the instance host (§9).

Report each secret once so the user can paste it into their secret store, say plainly that it is now in the
transcript and can be rotated, then move on.

## 7. Token lifetimes, refresh, and rate limits

**User access tokens (both app types).** 8 hours, refresh token 6 months, both rotated on every refresh: once you use
a refresh token, "that refresh token and the old user access token will no longer work". A client that keeps reusing
the original refresh token fails on the second refresh, not the first — which is why this bug reaches production.
Refresh via `POST https://github.com/login/oauth/access_token` with `client_id`, `client_secret`, `refresh_token` and
`grant_type=refresh_token`. If the refresh token itself expires, the customer must re-authorize. Toggling expiration
**on** for an app does not retroactively expire tokens already issued — delete those explicitly via
`DELETE /applications/{client_id}/token`.

**Installation access tokens (GitHub App).** Not an OAuth refresh at all: you sign a short-lived RS256 JWT with the
private key (GitHub caps app JWTs at 10 minutes — back-date `iat` slightly to absorb clock skew), then
`POST /app/installations/{installation_id}/access_tokens`. The result lives **1 hour** and can optionally be narrowed
to specific repositories (up to 500) or to a subset of the app's permissions. There is nothing to store long-term
except the private key and the installation id — which is a real operational advantage, and the reason a GitHub App
connector has no "refresh token expired" failure mode.

**Rate limits.** OAuth App user tokens: 5,000 req/hr per user (15,000 when the request is made on a user's behalf by
an app owned by an Enterprise Cloud org). GitHub App installations: 5,000 base, **+50/hr for each repository beyond
20 and +50/hr for each org user beyond 20, capped at 12,500**; Enterprise Cloud installations get a flat 15,000.
Secondary limits apply to both: no more than 100 concurrent requests, 900 points/minute per endpoint, 90 seconds of
CPU per 60 seconds of real time, and **2,000 OAuth access token requests per hour** — that last one is the one a
connector doing bulk re-authorization trips. GitHub returns primary and secondary rate-limit rejections as **HTTP
403**, not 429; a client that only retries on 429 will treat throttling as a permission error.

## 8. Org approval — the part that fails for customers, not for you

Both models have an org gate, and they are different gates.

**OAuth Apps: organization OAuth App access restrictions.** New organizations have them **on by default**. With
restrictions on, a member cannot authorize a third-party OAuth App for org resources — they can only *request*
approval, and only an **organization owner** can grant it. When an org first enables restrictions, already-authorized
OAuth apps **immediately lose access** to that org's resources, and hook deliveries from private org repositories
stop going to unapproved apps. So a connector that worked yesterday can break for one customer with no change on your
side. The fix is always customer-side: an owner approves the app for the org.

**GitHub Apps: installation permission.** By default a repository admin may install an app that requests neither
organization permissions nor the repository-administration permission; anything broader needs an **organization
owner**. Owners can further restrict installation to owners only, and can separately **disable app access requests**
entirely. With both set, a repo admin can neither install nor ask. When a member lacks permission to install, GitHub
emails the org owner with the request — but note this works when installing directly from the app owner, **not** when
installing from GitHub Marketplace.

Say which gate applies in your summary, and give the customer the exact ask ("an org owner must approve *AppName* at
Settings → Third-party access", or "an org owner must install *AppName* and grant it these repositories").

## 9. GitHub.com vs Enterprise Cloud vs Enterprise Server

- **GitHub.com / Enterprise Cloud** — same hosts and endpoints. Enterprise Cloud changes the *numbers* (15,000/hr)
  and adds enterprise-level permissions, not the flow.
- **GitHub Enterprise Server** — a different product for this purpose. **An app registered on GitHub.com cannot be
  installed on a GHES instance**; each instance must register its own. Every URL moves to the instance host: API
  base `https://HOSTNAME/api/v3`, GraphQL at `/api/v3/graphql`, and OAuth at `https://HOSTNAME/login/oauth/authorize`
  and `/login/oauth/access_token`. New endpoints and webhooks reach GHES later than GitHub.com, and the
  `x-github-enterprise-version` response header is how you branch on that. Practically, a GHES customer either hands
  you credentials from their own instance or self-hosts the app.

This is why a platform typically carries a **third** connector for GHES rather than a hostname toggle on the first
two — see the product note below.

## Product state — Unified.to (verified 2026-09-20; confirm with the connector's owner)

As of 2026-09-20 Unified.to ships **both** models side by side, which makes this the rare place a reader can compare
them against one product:

- **The GitHub connector uses the OAuth App model.** It offers OAuth2 plus a personal-access-token option, sends
  `user` and `user:email` as login scopes, requests scopes per object type, exchanges the code as a JSON POST, and
  supports the refresh grant. Its scope map is dominated by **`repo`** — repository, branch, commit, pull request,
  file, project and Copilot-agent objects all request it — with `issues` for issue and comment objects, `read:org`
  plus `read:user`/`user` almost everywhere, and `admin:org` only for organization writes. It appends
  `prompt=select_account` to the authorize URL, pins the REST API version by header, and remaps GitHub's 403
  rate-limit responses to 429 so retry and webhook sync treat them as throttling. Two dated quirks worth checking
  before you promise anything: platform-shared OAuth credentials for this connector are **switched off**, so every
  workspace must bring its own client ID and secret; and the help link it points customers at for obtaining those
  credentials **404s**.
- **The GitHub App connector uses the installation model.** It requires the App's **private key** alongside the
  client ID and secret — mandatory, because the connector exists specifically to mint installation tokens — and it
  re-mints a fresh 1-hour installation token on every "refresh" instead of exchanging an OAuth refresh token. It
  **strips the `scope` parameter** from the authorize URL, because a GitHub App's access comes from its configured
  permissions; the scope map it inherits is effectively inert, so **the permissions you tick at registration are the
  real contract** for this connector. It sends customers to the App's install page (the account and repository
  picker) rather than a bare authorize URL, and handles both callback shapes from §3: an `installation_id` from a
  fresh install, or a bare `code` from an already-installed user, in which case it exchanges the code for a user
  token and looks the installation up. Two things to flag: when a user has **several installations it takes the
  first**, which is wrong for anyone installed on more than one org; and it mints from the callback-supplied
  installation id, which is exactly the spoofing risk GitHub warns about in §3. Its object coverage matches the
  OAuth connector minus the Copilot-agent object.
- **A third connector covers GitHub Enterprise Server**, taking the instance hostname and a personal access token or
  OAuth2, with all endpoints rewritten to that host per §9. That split exists for the reason in §9 — it is not
  redundancy. This skill does not cover registering an app on a customer's GHES instance.

Treat all of the above as dated observation, not contract. Confirm scope sets, the shared-credentials switch and the
multiple-installations behaviour with the connector's owner before you register anything against them.

## 10. Verify end-to-end

Authorizing on the account that owns the app proves little. Test the path a customer takes.

1. Authorize from a **second account or org**, through the platform's real connect flow.
2. **GitHub App:** install on an org with a *subset* of repositories selected, then confirm a read call returns only
   those repositories — that is the whole point of the model, and the easiest thing to get silently wrong.
3. Confirm the token response carries what you expect: a `refresh_token` and `expires_in: 28800` for a user token, or
   a 1-hour installation token.
4. Force a refresh, then force a **second** refresh using the token the first returned. That is what catches a client
   that ignores rotation.
5. Send `redirect_uri` explicitly and confirm a non-default data center callback works.
6. If webhooks are on, deliver one and verify the `X-Hub-Signature-256` HMAC.

| Symptom | Cause |
| --- | --- |
| `redirect_uri_mismatch` | Callback not registered exactly; or wildcard matching off and the URI differs (§4) |
| EU/AU customers land on the US callback | `redirect_uri` omitted — GitHub used the first registered callback (§4) |
| `incorrect_client_credentials` | Wrong client ID or secret; or GitHub App client ID confused with App ID (§6) |
| `bad_verification_code` | Code already used or older than 10 minutes |
| `unverified_user_email` | The user's primary GitHub email is unverified — customer-side fix |
| `application_suspended` | GitHub suspended the app; contact GitHub Support |
| `device_flow_disabled` | Device flow not enabled in app settings |
| Auth succeeds, org's private repos invisible | OAuth App: org restrictions not approved. GitHub App: repo not selected at install (§8) |
| Worked yesterday, one customer now 403s | Org enabled OAuth App restrictions, or revoked/narrowed the installation (§8) |
| Second refresh fails | Client is not storing the rotated refresh token (§7) |
| GitHub App auth fails for everyone after a key change | Private key deleted before the replacement was deployed (§6) |
| Install succeeds but no token minted | Fresh install returned `installation_id` to the setup URL with no `code` (§3) |
| Throttled but the client never backs off | GitHub returns rate limits as **403**, not 429 (§7) |
| Installation token rejected by a length check | Stateless `ghs_APPID_JWT` format rollout (Platform state) |
| Nothing works against a customer's self-hosted instance | GHES needs its own app on its own host (§9) |

## 11. Hand off — never commit the secret or the key

- **Do not** write the client secret, the private key, or the webhook secret into source control, a test, a fixture,
  a committed `.env`, a ticket, a PR body, or a chat channel. Values go to the user, for the secret store or console.
- The **private key deserves the same handling as the secret** — it mints tokens for every installation. If it has to
  cross a boundary, base64 it into a secret manager entry, never a repository.
- If a code change is needed (a callback host, a scope, an added permission), keep it credential-free and say what
  the human must set out of band.
- Close with: app type (OAuth App or GitHub App) and why; app name and owning account; client ID; where each secret
  was delivered; authorize/token endpoints and API base; the exact scope strings or the permission list; install
  visibility (public/private); token lifetimes; the org-approval ask customers will need; and anything left for the
  user to do.

## Stop and ask

Hand back to a human rather than guessing when: the OAuth App vs GitHub App choice is not settled and the answer
changes the product (§A); someone proposes migrating a live connector between the two models — that re-authorizes
every customer; the client does not store rotated refresh tokens (report it, do not register and hope); a GitHub App
private key is missing and the app has live installations; the fix requires an org owner to approve or install
something, or to change org-wide app policy; a GHES customer needs their own registration on their own instance; a
Marketplace listing, publisher verification, or a security questionnaire asks for compliance, legal or volume claims;
or the developer settings pages do not match the **Platform state** section.

## References

Every link verified to return 200 on 2026-09-20. The `github.com/settings/...` registration pages require sign-in and
are deliberately not listed here.

- GitHub Apps vs OAuth Apps — https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps
- Registering a GitHub App — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app
- Creating an OAuth App — https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
- About the user authorization callback URL (wildcard matching) — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-user-authorization-callback-url
- About the setup URL (spoofed `installation_id` warning) — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-setup-url
- Authorizing OAuth apps (web + device flow) — https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
- Scopes for OAuth apps — https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
- Choosing permissions for a GitHub App — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app
- Generating an installation access token — https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
- Generating a user access token — https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app
- Refreshing user access tokens — https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens
- Managing private keys for GitHub Apps — https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps
- Registering a GitHub App from a manifest — https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
- Registering a GitHub App using URL parameters — https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-using-url-parameters
- Making a GitHub App public or private — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/making-a-github-app-public-or-private
- Making your GitHub App available for GitHub Enterprise Server — https://docs.github.com/en/apps/sharing-github-apps/making-your-github-app-available-for-github-enterprise-server
- About OAuth App access restrictions — https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions
- Limiting OAuth app and GitHub App access requests and installations — https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/limiting-oauth-app-and-github-app-access-requests-and-installations
- Requesting a GitHub App from your organization owner — https://docs.github.com/en/apps/using-github-apps/requesting-a-github-app-from-your-organization-owner
- Installing a GitHub App from a third party — https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party
- Rate limits for the REST API — https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- Validating webhook deliveries — https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
- Troubleshooting authorization request errors — https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-authorization-request-errors
- Troubleshooting access token request errors — https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors
- Best practices for creating an OAuth app — https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/best-practices-for-creating-an-oauth-app
- Best practices for creating a GitHub App — https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app
- Token expiration and revocation — https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation
