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:
- App-level JWT – signed with the App's RSA private key (RS256), valid
for ≤10 minutes. Used only to call the installations endpoint.
- 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.
Summary
gitgrabcurrently authenticates only via a Personal Access Token (PAT) inGITHUB_TOKEN. PATs are tied to individual user accounts, never expireautomatically, 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:
for ≤10 minutes. Used only to call the installations endpoint.
POST /app/installations/{id}/access_tokensusing 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
GITHUB_APP_IDGITHUB_APP_PRIVATE_KEYGITHUB_APP_INSTALLATION_IDWhen all three are present,
gitgrabuses the App flow to obtain aninstallation token. When only
GITHUB_TOKENis present, behaviour isunchanged. If neither is configured, the tool exits with a clear error.
New library file:
app_auth.goJWT signing uses only the Go standard library (
crypto/rsa,crypto/x509,encoding/pem) — no new external dependencies required.Auth resolution in
cmd/gitgrab/main.goWhat does NOT change
GitHubTokentype and itsAuthHeader()method (installation tokens usethe same
token <value>format)GitHubClient,makeRequest(),FetchAllRepos(),CloneRepo()Testing
GetInstallationToken()with a mockHTTPClient(consistent withthe existing test pattern using
httptest.NewRecorder())crypto/rsa.VerifyPKCS1v15)resolveToken()covering: App-only, PAT-only, bothset (App takes precedence), neither set
Out of scope / follow-ups
this may matter;
GetInstallationToken's signature makes it easy to add arefresh wrapper later.
GET /app/installationsfiltered byorg name — avoids requiring users to look up the installation ID manually.
var (useful in some CI systems) instead of a file path.