Skip to content

feat(auth): add GitHub App server-to-server authentication for stdio#2797

Open
SamMorrowDrums wants to merge 1 commit into
mainfrom
sammorrowdrums-github-app-s2s-auth
Open

feat(auth): add GitHub App server-to-server authentication for stdio#2797
SamMorrowDrums wants to merge 1 commit into
mainfrom
sammorrowdrums-github-app-s2s-auth

Conversation

@SamMorrowDrums

Copy link
Copy Markdown
Collaborator

Summary

Adds non-interactive GitHub App installation authentication to the stdio server, so headless deployments (CI, background agents, the Claude Agent SDK, a self-hosted image spawned by Copilot's cloud agent) can authenticate with no browser, device code, or elicitation.

This is the outstanding follow-up tracked in #1333: stdio OAuth (#2704) shipped the interactive user-to-server flows, but PEM-based server-to-server (s2s) auth was still needed to remove the human-in-the-loop requirement.

Builds on the prior art in #2562 (thanks @pyama86) — same goal, reworked for a tighter security posture and a stdlib-only implementation.

How it works

internal/githubapp signs a short-lived RS256 JWT with the app's private key, exchanges it for an installation access token, and refreshes it transparently ~5 minutes before expiry. The token is held in memory only. It exposes a Provider whose AccessToken() mirrors oauth.Manager, so it slots into the existing BearerAuthTransport token provider with no new transport code. Only the standard library and golang.org/x/oauth2 are used (no new dependencies).

Safe key injection

  • --app-id / GITHUB_APP_ID, --app-installation-id / GITHUB_APP_INSTALLATION_ID
  • --app-private-key-path / GITHUB_APP_PRIVATE_KEY_PATHpreferred: mount as a secret file; keeps the key off argv and out of the environment
  • GITHUB_APP_PRIVATE_KEY — inline fallback for env-only secret stores

There is intentionally no flag for the key contents, which would otherwise leak via the process command line. App auth is mutually exclusive with a PAT and with OAuth login.

Why stdio only (by design)

This is not available for the http server, and that's deliberate. An HTTP server authenticating with a server-wide app identity would let any client that can reach its endpoint act as the app with full permissions — turning a network-reachable port into ambient, unauthenticated access to the whole installation. The http server keeps requiring per-request Authorization tokens so every caller's identity stays explicit. (Distinct from #2236, which is about per-request multi-identity tokens.)

Security

Per popular demand, but dangerous: this injects a long-lived, high-privilege credential alongside an AI agent, and minted tokens act across every repo the app is installed on. A loud startup warning and a dedicated docs page (docs/github-app-auth.md, with Docker + Kubernetes secret-mount examples, least-privilege guidance, and the stdio-only rationale) make this explicit. Not recommended without an independent security review.

Test plan

  • internal/githubapp unit tests: PKCS#1/fix: added reason argument to get_me function #8 key parsing, JWT round-trip + claim/lifetime verification, installation-token fetch (success / error status / expiry) against httptest, caching, early refresh, and error-once logging.
  • internal/ghmcp mutual-exclusivity guard extended to cover all three modes (token / OAuth / App).
  • script/lint (0 issues) and script/test (race) green; generate-docs produces no drift.

Closes #1333


🤖 Generated with GitHub Copilot CLI

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Add non-interactive GitHub App installation authentication to the stdio
server, so headless deployments (CI, Kubernetes, background agents) can
authenticate without a browser, device code, or elicitation. This is the
outstanding follow-up tracked in #1333: OAuth login shipped the interactive
user-to-server flows, but PEM-based server-to-server auth was still needed to
remove the interactive requirement.

The new internal/githubapp package signs a short-lived RS256 JWT with the
app's private key, exchanges it for an installation access token, and refreshes
it transparently before expiry. It exposes a Provider whose AccessToken method
mirrors oauth.Manager so it plugs into the existing BearerAuthTransport token
provider. Only the standard library and golang.org/x/oauth2 are used.

The private key is injected safely: a file path (GITHUB_APP_PRIVATE_KEY_PATH,
preferred — mountable as a secret and kept off argv and out of the environment)
or an inline GITHUB_APP_PRIVATE_KEY env var. There is intentionally no flag for
the key contents, which would otherwise leak via the process command line.

App auth is mutually exclusive with a PAT and with OAuth login. A loud startup
warning and a dedicated docs page (docs/github-app-auth.md, with Docker and
Kubernetes examples) cover the security considerations: this injects a
high-privilege credential alongside the agent and is not recommended without an
independent security review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SamMorrowDrums SamMorrowDrums requested a review from a team as a code owner June 29, 2026 22:06
Copilot AI review requested due to automatic review settings June 29, 2026 22:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GitHub App server-to-server (installation token) authentication to the stdio GitHub MCP server, enabling fully headless deployments to authenticate without interactive OAuth/PAT provisioning.

Changes:

  • Introduces internal/githubapp to mint RS256 app JWTs, exchange them for installation tokens, and refresh/cache tokens via an AccessToken() provider.
  • Wires stdio config to support an App-auth mode with explicit mutual exclusivity vs PAT and OAuth, plus a loud startup warning about credential risk.
  • Adds documentation for GitHub App auth usage patterns (Docker/Kubernetes) and updates README / OAuth docs to point to it.
Show a summary per file
File Description
README.md Adds a headless-deployment pointer to the new GitHub App auth docs and security warning.
internal/githubapp/githubapp.go New GitHub App s2s auth implementation (JWT minting, installation-token fetch, caching/refresh provider).
internal/githubapp/githubapp_test.go Unit tests for key parsing, JWT correctness, token fetching, caching/refresh, and error logging behavior.
internal/ghmcp/server.go Adds AppAuth config, enforces auth-mode exclusivity, wires provider into token transport, and logs a startup warning.
internal/ghmcp/oauth_test.go Extends exclusivity tests to cover PAT vs OAuth vs App auth combinations.
docs/oauth-login.md Notes that OAuth is still human-mediated and points headless users to GitHub App auth.
docs/github-app-auth.md New end-to-end documentation for stdio GitHub App auth, including security guidance and deployment examples.
cmd/github-mcp-server/main.go Adds stdio flags/env handling for GitHub App auth, host→REST base resolution, and exclusivity checks.

Review details

  • Files reviewed: 8/8 changed files
  • Comments generated: 5
  • Review effort level: Low

Comment on lines +67 to +69
// AppID is the GitHub App's App ID or client ID; it becomes the JWT issuer
// (iss). Both forms are accepted by GitHub.
AppID string
Comment thread docs/github-app-auth.md
Comment on lines +95 to +99
3. Note three values:
- the **App ID** (or the app's **client ID** — either works as the JWT issuer),
- the **installation ID** (visible in the installation's settings URL, or via
the [installations API](https://docs.github.com/en/rest/apps/apps#list-installations-for-the-authenticated-app)),
- the path to the **private key** `.pem`.
Comment thread docs/github-app-auth.md
Comment on lines +109 to +112
| `--app-id` | `GITHUB_APP_ID` | GitHub App ID or client ID. Becomes the JWT issuer. |
| `--app-installation-id` | `GITHUB_APP_INSTALLATION_ID` | Installation ID whose token is minted. |
| `--app-private-key-path` | `GITHUB_APP_PRIVATE_KEY_PATH` | Path to the private key PEM file. **Preferred** way to supply the key. |
| _(no flag)_ | `GITHUB_APP_PRIVATE_KEY` | The PEM contents inline. Use only where a file can't be mounted. Literal `\n` sequences are accepted so the key can live in a single-line variable. |
Comment on lines +68 to +70
if token == "" && !appAuthRequested && oauthClientID == "" {
return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth (GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID and GITHUB_APP_PRIVATE_KEY_PATH), or pass --oauth-client-id to log in via OAuth")
}
Comment on lines +266 to +273
// stdio-specific GitHub App (server-to-server) flags. Provide an app ID,
// installation ID, and private key to authenticate non-interactively — no
// browser, device code, or elicitation. Intended for headless deployments.
// The private key itself has no flag (only GITHUB_APP_PRIVATE_KEY): a flag
// would place the key in the process arguments. Prefer the key file path.
stdioCmd.Flags().String("app-id", "", "GitHub App ID or client ID, enabling non-interactive server-to-server authentication")
stdioCmd.Flags().String("app-installation-id", "", "GitHub App installation ID to mint installation access tokens for")
stdioCmd.Flags().String("app-private-key-path", "", "Path to the GitHub App private key (PEM). Preferred over GITHUB_APP_PRIVATE_KEY: keeps the key off the command line and out of the environment")
@pyama86

pyama86 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Thank you for the excellent proposal. Even with HTTP, it is possible to listen with a high degree of isolation in environments like Kubernetes. Therefore, instead of restricting it to stdio, I would like you to leave the decision up to the user.

@SamMorrowDrums

Copy link
Copy Markdown
Collaborator Author

@pyama86 I understand it can be done safely, but I do have to be careful about what GitHub provides because users may use it in inappropriate contexts also, and the risk with http version is massively higher. With port binding to 0.0.0.0 a user could easily expose themselves to even website direct access. You are of course welcome to do whatever you like with a fork, while we iron out what's possible here.

I will not even merge this until I discuss with security/auth folks in GitHub also.

@go-kazuhiko-yamashita

Copy link
Copy Markdown

@SamMorrowDrums
I also agree that we should be cautious about GitHub's responsibility as a platform and the baseline it needs to maintain.
By the way, the other PR of mine that got merged #2655 was intended to make it listen on 127.0.0.1. I completely respect your concerns, so I'm happy to wait for the outcome.

@SamMorrowDrums

Copy link
Copy Markdown
Collaborator Author

@SamMorrowDrums

I also agree that we should be cautious about GitHub's responsibility as a platform and the baseline it needs to maintain.

By the way, the other PR of mine that got merged #2655 was intended to make it listen on 127.0.0.1. I completely respect your concerns, so I'm happy to wait for the outcome.

I was super happy to receive and merge that PR! Makes sense in this context especially. 🙏

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 with client credentials

5 participants