Skip to content

feat: add GitHub App authentication support#2562

Open
pyama86 wants to merge 3 commits into
github:mainfrom
pyama86:feat/github-app-auth
Open

feat: add GitHub App authentication support#2562
pyama86 wants to merge 3 commits into
github:mainfrom
pyama86:feat/github-app-auth

Conversation

@pyama86
Copy link
Copy Markdown

@pyama86 pyama86 commented May 27, 2026

Summary

Adds native GitHub App authentication as an alternative to Personal Access Tokens, addressing a common request for organization-managed, scoped, short-lived credentials.

  • Introduces pkg/github/appauth package that handles JWT generation (RS256) and installation token management using only the Go standard library (no new dependencies)
  • Automatically refreshes installation tokens before expiry (5-minute buffer on 1-hour tokens), thread-safe with sync.Mutex
  • Supports private key via environment variable (GITHUB_APP_PRIVATE_KEY, with literal \n handling) or file path (GITHUB_APP_PRIVATE_KEY_PATH)
  • Falls back gracefully: when App auth env vars are not set, PAT authentication works as before

Environment Variables

Variable Description
GITHUB_APP_ID The GitHub App ID
GITHUB_APP_INSTALLATION_ID The installation ID of the GitHub App
GITHUB_APP_PRIVATE_KEY PEM-encoded private key (inline, \n for newlines)
GITHUB_APP_PRIVATE_KEY_PATH Path to the private key file (alternative to inline)

Closes #1333

Test plan

  • 13 unit tests covering key parsing (PKCS1/PKCS8), JWT generation/verification, installation token fetch, caching, auto-refresh, HTTP round-trip, and error handling
  • Manual test with a real GitHub App installation
  • Verify PAT-based authentication still works unchanged when App auth env vars are not set
  • Verify incomplete App auth config (e.g. missing installation ID) returns a clear error
  • Test with GitHub Enterprise Server (--gh-host)

pyama86 added 2 commits May 28, 2026 08:43
Add native GitHub App authentication as an alternative to Personal
Access Tokens. The server can now authenticate using App ID, private
key, and installation ID to automatically generate and refresh
installation tokens.

- Add `pkg/github/appauth` package with JWT generation and installation
  token management using only the standard library
- Auto-refresh tokens before expiry (5-minute buffer on 1-hour tokens)
- Support private key via env var (GITHUB_APP_PRIVATE_KEY) or file path
  (GITHUB_APP_PRIVATE_KEY_PATH)
- Handle literal `\n` in env var PEM keys
- Add comprehensive tests (13 tests covering key parsing, JWT generation,
  token caching, refresh, round-trip, and error handling)

Closes github#1333
gogithub.Option is not an exported type, so we cannot store options
in a slice. Use separate NewClient calls for each auth path instead.
Copy link
Copy Markdown

@sharmaz11 sharmaz11 left a comment

Choose a reason for hiding this comment

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

Looks good!

Copy link
Copy Markdown

@sharmaz11 sharmaz11 left a comment

Choose a reason for hiding this comment

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

Security test - please ignore - bug bounty testing

@pyama86 pyama86 marked this pull request as ready for review May 28, 2026 04:22
@pyama86 pyama86 requested a review from a team as a code owner May 28, 2026 04:22
Copilot AI review requested due to automatic review settings May 28, 2026 04:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds GitHub App installation authentication as an alternative to Personal Access Tokens for the stdio MCP server. A new appauth package implements an http.RoundTripper that generates JWTs and fetches/caches installation tokens; the server wiring conditionally uses this transport when App credentials are supplied via environment variables.

Changes:

  • New pkg/github/appauth package (Transport + JWT/installation token logic + tests).
  • internal/ghmcp/server.go plumbs an optional authTransport through NewStdioMCPServer/createGitHubClients and skips PAT-only scope fetching when App auth is in use.
  • CLI (cmd/github-mcp-server/main.go) parses GITHUB_APP_ID/GITHUB_APP_INSTALLATION_ID/GITHUB_APP_PRIVATE_KEY[_PATH] env vars; README documents the feature.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pkg/github/appauth/appauth.go New transport that signs JWTs and exchanges them for installation tokens.
pkg/github/appauth/appauth_test.go Unit tests covering key parsing, JWT generation, token caching/refresh, and RoundTrip.
internal/ghmcp/server.go Threads optional App auth transport into REST/GraphQL client construction.
cmd/github-mcp-server/main.go Reads App auth config from env vars and passes to RunStdioServer.
go.sum Adds golang-jwt/jwt/v4 checksum entry.
README.md Documents GitHub App authentication usage.

Comment thread cmd/github-mcp-server/main.go Outdated
Comment on lines +280 to +281
// Environment variables often use literal "\n" instead of actual newlines
privateKey = []byte(strings.ReplaceAll(privateKeyStr, `\n`, "\n"))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Now only replaces literal \n when the value contains no actual newlines, so keys correctly passed via .env files or heredocs are not affected.

Comment on lines +253 to +257
func parseAppAuthConfig() (appID int64, privateKey []byte, installationID int64, err error) {
appIDStr := viper.GetString("app_id")
installationIDStr := viper.GetString("app_installation_id")
privateKeyStr := viper.GetString("app_private_key")
privateKeyPath := viper.GetString("app_private_key_path")
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These env vars are correctly picked up via viper.AutomaticEnv() with the GITHUB_ prefix. For example, viper.GetString("app_id") reads GITHUB_APP_ID because the env prefix is set to github and the key replacer converts - to _. No explicit BindEnv is needed for this case.

Comment on lines +265 to +267
if appIDStr == "" || installationIDStr == "" || (privateKeyStr == "" && privateKeyPath == "") {
return 0, nil, 0, errors.New("incomplete GitHub App auth config: GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_PATH are all required")
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Now returns an error when both GITHUB_APP_PRIVATE_KEY and GITHUB_APP_PRIVATE_KEY_PATH are set. README also updated to state they are mutually exclusive.

Comment thread internal/ghmcp/server.go Outdated
Comment on lines +98 to +100
// Auth transport already sets the Authorization header
gqlTransport = &transport.GraphQLFeaturesTransport{
Transport: authTransport,
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Wrapped the GraphQL transport in UserAgentTransport when using App auth, matching the REST path behavior.

req.Header.Set("Authorization", "Bearer "+jwtToken)
req.Header.Set("Accept", "application/vnd.github+json")

resp, err := t.base.RoundTrip(req)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a doc comment on NewTransport clarifying that the base transport must not inject its own Authorization header.

Comment on lines +93 to +95
func (t *Transport) installationToken(ctx context.Context) (string, error) {
t.mu.Lock()
defer t.mu.Unlock()
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Switched to sync.RWMutex with a double-check pattern: cached-token reads use RLock (concurrent), and only the refresh path acquires Lock. This avoids serializing all requests behind a slow token refresh.

Comment thread pkg/github/appauth/appauth.go Outdated
Comment on lines +205 to +207
// VerifyJWT parses and verifies a JWT token using the given RSA public key.
// Returns the claims map. This is used only for testing.
func VerifyJWT(tokenString string, pubKey *rsa.PublicKey) (map[string]any, error) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Moved VerifyJWT to the test file as unexported verifyJWT.

Comment thread pkg/github/appauth/appauth.go Outdated
Comment on lines +119 to +127
now := time.Now().Add(-30 * time.Second) // allow 30s clock drift

header := map[string]string{
"alg": "RS256",
"typ": "JWT",
}
payload := map[string]any{
"iat": now.Unix(),
"exp": now.Add(10 * time.Minute).Unix(),
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Changed to iat = now - 30s, exp = now + 9m to stay well within the 10-minute maximum.

- Move VerifyJWT to test file to avoid exporting test-only helpers
- Use RWMutex with double-check pattern to avoid blocking reads during
  token refresh
- Add UserAgentTransport to GraphQL path when using App auth for
  consistency with REST
- Make GITHUB_APP_PRIVATE_KEY and GITHUB_APP_PRIVATE_KEY_PATH mutually
  exclusive (return error when both are set)
- Only replace literal \n when the private key has no real newlines to
  avoid corrupting correctly-passed keys
- Use safer JWT lifetime (iat=now-30s, exp=now+9m) to stay well within
  GitHub's 10-minute maximum
- Document that base transport must not inject its own Authorization
  header
Copy link
Copy Markdown

@ah-gh2 ah-gh2 left a comment

Choose a reason for hiding this comment

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

Changes in this PR Details ..

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

4 participants