Skip to content

feat: support GitHub App token authentication #13

@scottbrown

Description

@scottbrown

Summary

gitgrab currently authenticates only via a Personal Access Token (PAT) in
GITHUB_TOKEN. PATs are tied to individual user accounts, never expire
automatically, and are poorly suited for CI/CD pipelines and shared
infrastructure. This issue tracks adding support for GitHub App installation
tokens as a more secure, scoped, machine-identity alternative.

Background

GitHub App authentication works in two steps:

  1. App-level JWT – signed with the App's RSA private key (RS256), valid
    for ≤10 minutes. Used only to call the installations endpoint.
  2. Installation access token – retrieved from
    POST /app/installations/{id}/access_tokens using the JWT. Short-lived
    (1 hour), scoped to specific repositories/permissions. Functions identically
    to a PAT for all subsequent API calls (Authorization: token ghs_xxx).

Because the installation token uses the same header format as a PAT, the
impact on existing code is minimal.

Proposed Design

New environment variables

Variable Description
GITHUB_APP_ID Numeric GitHub App ID
GITHUB_APP_PRIVATE_KEY Filesystem path to the PEM-encoded RSA private key
GITHUB_APP_INSTALLATION_ID Numeric installation ID scoped to the target org

When all three are present, gitgrab uses the App flow to obtain an
installation token. When only GITHUB_TOKEN is present, behaviour is
unchanged. If neither is configured, the tool exits with a clear error.

New library file: app_auth.go

type GitHubAppCredentials struct {
    AppID          string
    PrivateKeyPath string
    InstallationID string
}

// GetInstallationToken builds a JWT, calls the GitHub API, and returns
// a short-lived installation token as a GitHubToken.
func GetInstallationToken(creds GitHubAppCredentials, client HTTPClient) (GitHubToken, error)

JWT signing uses only the Go standard library (crypto/rsa, crypto/x509,
encoding/pem) — no new external dependencies required.

Auth resolution in cmd/gitgrab/main.go

func resolveToken(httpClient HTTPClient) (gitgrab.GitHubToken, error) {
    appID     := os.Getenv("GITHUB_APP_ID")
    keyPath   := os.Getenv("GITHUB_APP_PRIVATE_KEY")
    installID := os.Getenv("GITHUB_APP_INSTALLATION_ID")

    if appID != "" && keyPath != "" && installID != "" {
        creds := gitgrab.GitHubAppCredentials{
            AppID: appID, PrivateKeyPath: keyPath, InstallationID: installID,
        }
        return gitgrab.GetInstallationToken(creds, httpClient)
    }

    pat := os.Getenv("GITHUB_TOKEN")
    if pat != "" {
        return gitgrab.GitHubToken(pat), nil
    }

    return "", errors.New(
        "no GitHub credentials configured: set GITHUB_TOKEN or all three GITHUB_APP_* variables",
    )
}

What does NOT change

  • GitHubToken type and its AuthHeader() method (installation tokens use
    the same token <value> format)
  • GitHubClient, makeRequest(), FetchAllRepos(), CloneRepo()
  • All existing tests
  • SSH/HTTP clone method logic and URL-embedding behaviour

Testing

  • Unit test GetInstallationToken() with a mock HTTPClient (consistent with
    the existing test pattern using httptest.NewRecorder())
  • Unit test the JWT builder (verifiable with crypto/rsa.VerifyPKCS1v15)
  • Table-driven tests for resolveToken() covering: App-only, PAT-only, both
    set (App takes precedence), neither set

Out of scope / follow-ups

  • Token refresh: installation tokens expire in 1 hour. For very large orgs
    this may matter; GetInstallationToken's signature makes it easy to add a
    refresh wrapper later.
  • Auto-discover installation ID: via GET /app/installations filtered by
    org name — avoids requiring users to look up the installation ID manually.
  • Inline private key: support passing the PEM content directly as an env
    var (useful in some CI systems) instead of a file path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions