Skip to content

Support GitHub App authentication across multiple installations - #3199

Open
vedant381 wants to merge 1 commit into
github:mainfrom
vedant381:app-auth-multiple-installations
Open

Support GitHub App authentication across multiple installations#3199
vedant381 wants to merge 1 commit into
github:mainfrom
vedant381:app-auth-multiple-installations

Conversation

@vedant381

Copy link
Copy Markdown

Summary

Makes --app-installation-id optional so a single app ID and private key can authenticate against every organization a GitHub App is installed on, routing each API request to the installation that owns the resource it addresses.

Why

A GitHub App can be installed on several accounts, but each installation has its own ID and mints its own access token. The current implementation takes one installation ID, so an enterprise with repositories spread across organizations needs one server process per organization.

Fixes #2885

What changed

  • internal/githubapp: new MultiProvider, which lists the app's installations (GET /app/installations), caches the account-login-to-installation map, and lazily creates a cached single-installation Provider per installation. The directory is refreshed on a lookup miss, at most every 10 minutes, so installing the app on a new organization is picked up without a restart.
  • internal/githubapp: new OwnerFromRequest, which determines the owning account from an outbound request — REST from the path (/repos/{owner}/…, /orgs/{org}/…, /users/{user}/…, including the GHES /api/v3 prefix), GraphQL from the owner/login variable in the request body (read via GetBody, so the body is left intact for the transport below).
  • pkg/http/transport: BearerAuthTransport gains an optional RequestTokenProvider func(*http.Request) string. Routing needs the request, which the existing func() string provider cannot see. It takes precedence over TokenProvider and Token; when all are unset, behavior is unchanged.
  • cmd/github-mcp-server, internal/ghmcp, pkg/github: plumb the request-scoped provider through, and select multi-installation mode when --app-installation-id is omitted. Passing an installation ID keeps the existing single-installation path exactly as it was.
  • docs/github-app-auth.md: new "Multiple organizations" section covering configuration, how routing works, and its limits.

Usage:

github-mcp-server stdio \
  --app-id 123456 \
  --app-private-key-path /secrets/github-app.pem

MCP impact

  • No tool or API changes

Authentication configuration only; no tool schemas or behavior change.

Prompts tested (tool changes only)

n/a — no tool changes.

Security / limits

  • Auth / permissions considered

A request that names no owner, or names an account the app is not installed on, is sent unauthenticated rather than falling back to another installation's token — a misrouted call fails visibly (401/404) instead of silently running against the wrong organization. The one deliberate trade-off is that endpoints which are not owner-scoped (/user, /rate_limit, /repositories/{id}) do not work in multi-installation mode; --app-installation-id remains available to authenticate as one specific installation. This is called out in the docs.

Installation tokens are still scoped by AllowedHosts, unchanged. The private key handling is untouched.

Tool renaming

  • I am not renaming tools as part of this PR

Lint & tests

  • Tested locally with go test ./... — full suite passes.
  • Linted locally with ./script/lint — the pinned golangci-lint v2.9.0 cannot read Go 1.27 export data on my machine (export data version 4 is greater than maximum supported version 2). I ran v2.13.2 instead: it reports no findings in any file this PR touches (the 3 remaining findings are pre-existing, in pkg/http/transport/etag_test.go and elsewhere, and surface only because that version is newer than the pinned one). gofmt -s is clean.

New tests cover owner extraction from REST paths and GraphQL bodies (including the body-not-consumed contract), per-owner token routing, case-insensitive account lookup, directory caching and refresh-on-miss, listing failures, and the transport's provider precedence.

Docs

  • Updated (README / docs / examples)

A GitHub App can be installed on several accounts, but each installation
has its own ID and mints its own access token. Today the server takes a
single installation ID, so an enterprise whose repositories are spread
across organizations needs one server process per organization.

Make --app-installation-id optional. Without it, the server lists the
app's installations, caches the account-to-installation map, and mints a
token per installation on demand, routing each API request to the
installation that owns the resource it addresses: REST requests by the
owner in the path, GraphQL requests by the owner or login variable.

Routing needs the request, which the existing func() string token
provider cannot see, so BearerAuthTransport gains an optional
RequestTokenProvider that takes precedence over it.

A request that names no owner, or names an account the app is not
installed on, is sent unauthenticated rather than falling back to
another installation's token, so a misrouted call fails visibly instead
of running against the wrong organization.

Passing --app-installation-id keeps the existing single-installation
behavior unchanged.
@vedant381
vedant381 requested a review from a team as a code owner September 1, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support GitHub App authentication across multiple organizations/installation IDs

1 participant