PAT vs GitHub App auth — when to use which
GitHub offers five token types: classic PAT, fine-grained PAT, App installation token, App user token, and the per-job GITHUB_TOKEN. App installation tokens win for org automation thanks to org-owned identity, 1-hour auto-rotation, scoped permissions, and scalable rate limits; fine-grained PATs suit personal scripts.
article technology en GitHub offers five token types: classic PAT, fine-grained PAT, App installation token, App user token, and the per-job GITHUB_TOKEN. App installation tokens win for org automation thanks to org-owned identity, 1-hour auto-rotation, scoped permissions, and scalable rate limits; fine-grained PATs suit personal scripts.PAT vs GitHub App auth — when to use which
GitHub authentication for automation is a choice between five credential types — classic PAT (ghp_…), fine-grained PAT (github_pat_…), GitHub App installation token (ghs_…), GitHub App user token, and the per-job GITHUB_TOKEN minted inside Actions. The choice is not just about scopes: it determines whose identity acts on the API, who owns the credential when staff turnover happens, how rotation is automated, what rate-limit bucket is consumed, and how clearly the audit log distinguishes the bot from a human. For any org-level automation the right answer is a GitHub App installation token; fine-grained PATs are the pragmatic fallback for personal scripts; classic PATs and bot-user PATs are legacy patterns to migrate away from.
Token types at a glance
GitHub exposes five distinct credential shapes for the REST and GraphQL APIs, each with its own identity model and lifetime ceiling. The classic PAT (ghp_…) is tied to a user account and grants coarse scopes such as repo, which covers full read/write across every repository the user can access; expiry is optional, so unrotated classic PATs routinely become long-lived blast-radius secrets. The fine-grained PAT (github_pat_…) is also user-owned but selects specific repositories and per-resource permissions (for example contents: write on one repo, no access elsewhere) and enforces a maximum 366-day expiry that org admins can shorten further. The GitHub App installation token (ghs_…) is minted by exchanging a 10-minute JWT — signed with the App’s private key — for a 1-hour access token via POST /app/installations/{id}/access_tokens; the App itself is a first-class principal independent of any human account. The GitHub App user token is the OAuth-on-behalf-of-user variant, primarily for SaaS integrations that need to act as the authorising user rather than as the bot. Finally, GITHUB_TOKEN is automatically created for each Actions job, lives only for the job’s duration, and is locked to the triggering repository unless explicitly swapped for another credential.
Trust model and identity ownership
The decisive question is who the API call belongs to when something goes wrong. A PAT — classic or fine-grained — is owned by an individual user, so any automation it powers is permanently entangled with that human’s account: when they leave the organisation, access continues until the PAT expires or is manually revoked, and the audit log shows the human’s username as the actor for every machine-driven request. A GitHub App is an organisation-owned principal that survives staff turnover, can be installed and uninstalled by org admins as a control surface, and produces audit-log entries attributed to app-name[bot] and commits authored by app-id+app-name@users.noreply.github.com. Blast radius differs accordingly: a leaked classic PAT exposes every repository and permission its owner can reach, whereas a leaked installation token only grants the App’s declared permissions on the repositories the App is installed on, and the token expires within an hour regardless. Bot user accounts holding PATs — a common shortcut — violate GitHub’s one-account-per-person Terms of Service and reproduce the user-coupling problem without solving it.
Scopes versus permissions
Classic PAT scopes are coarse strings such as repo, workflow, and admin:org, and a classic PAT cannot be narrowed to fewer repositories than the user can access — it is all-or-nothing per scope. Fine-grained PATs replace scopes with a per-repository selection plus per-resource permission levels (read, write, or no access for resources like contents, pull_requests, issues, actions), so the same PAT can grant write to one repo’s contents and read-only on another’s pull requests. GitHub Apps declare the maximum permission set in the App manifest, and each installation can further restrict the actual grant; at token-creation time the JWT exchange can additionally restrict the issued installation token to a subset of installed repositories, enabling least-privilege per workflow step. The practical consequence is that a classic PAT with repo is structurally over-privileged for almost every modern automation, whereas an App can be sized exactly to the resources the bot must touch.
Rate limits per token type
Rate limits are bucketed by principal, not by call site, which is why token choice has direct throughput implications. Classic and fine-grained PATs share the authenticated-user bucket of 5,000 requests per hour — and this bucket is shared with every other tool the user runs, including their interactive web session, gh CLI, IDE integrations, and other scripts. GitHub App installation tokens start at a 5,000 req/h baseline that scales with installation size: GitHub Enterprise Cloud organisations get up to 15,000 req/h, and other installations gain +50 req/h per repository and +50 req/h per organisation user, capped at 12,500 req/h. The Actions GITHUB_TOKEN has the most restrictive bucket — 1,000 write requests and 500 read requests per hour per repository, not per user — so any high-volume CI workload that bumps into those limits should swap GITHUB_TOKEN for an App installation token. Because PAT buckets are shared with the human owner, a CI job running on a developer’s personal PAT can starve their own interactive workflow during peak load, which is another reason org-level automation should not depend on user-owned tokens.
Lifetime, rotation, and audit attribution
Token lifetime determines how much rotation work the operator has to do. A classic PAT with no expiry persists until manually revoked; a fine-grained PAT must be rotated within 366 days at the latest (often 90 days under org policy). An App installation token expires at one hour with no extension, but rotation is effectively zero-burden because only the App’s long-lived private key is stored as a secret and the JWT-to-installation-token exchange runs at the start of every workflow execution; the short token never has to be persisted. The Actions GITHUB_TOKEN is automatic and minted per job. Audit-log attribution mirrors this lifetime model: any PAT-based action shows the human owner as the actor, indistinguishable from that user clicking through the web UI, while an App installation token surfaces app-name[bot] as the actor and a clearly machine-flavoured commit author. For organisations subject to SOC 2, ISO 27001, or other auditability regimes, the ability to separate bot actions from human actions in a single log query is itself a compliance feature.
Recommendation matrix by use case
The token choice falls out of three properties of the workload: who owns the work, how many repositories it touches, and whether it runs inside or outside Actions. Personal scripts on personal repos should use a fine-grained PAT with the shortest practical expiry — no App registration overhead, scoped to specific repos, and revocable from the user’s own settings. CI bots in a single org repository should use a GitHub App installation token because the App registration buys org-owned identity, audit clarity, and scalable rate limits. Multi-repo automation across an organisation also wants a GitHub App, with one App installed on N repositories instead of N PATs to manage. A reusable public GitHub Action should accept the caller’s GITHUB_TOKEN (with explicit permissions: declarations) and optionally support an App-token input so consumers can opt into higher rate limits. Third-party SaaS integrations should ship as a GitHub App with the OAuth user-token flow, since that exposes a user-consented permission prompt familiar to admins. Workflows that act only inside the triggering repository should use the built-in GITHUB_TOKEN; workflows that push or read across repositories must swap it for an App installation token because GITHUB_TOKEN is locked to the source repo. Legacy automation still on classic PATs should migrate either to fine-grained PATs (small scripts) or to a GitHub App (org-level work).
Migration path from PAT to App
The cheapest migration is from classic PAT to fine-grained PAT: enumerate the repositories and API operations the script actually touches, create a fine-grained PAT bound to exactly those repositories with exactly those resource permissions, set expiry to no more than 90 days, replace the secret in the relevant secrets store, and delete the classic PAT. This buys per-repo and per-permission scoping plus enforced rotation without any infrastructure change. The fuller migration to a GitHub App takes more setup but is straightforward: register a GitHub App in the organisation under Settings → Developer settings → GitHub Apps, declare only the permissions the bot needs in the manifest, install the App on the target repositories, store APP_ID and the App’s PEM-formatted APP_PRIVATE_KEY as Actions secrets, and use actions/create-github-app-token@v2 at the top of each workflow to mint a fresh installation token. The action handles JWT signing, the installation-token exchange, and the 1-hour scoping automatically. Once the App flow is verified end-to-end, revoke the legacy PAT to close the migration.
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- run: gh pr create --title "from-app" --body "..."
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Security pitfalls and operational guardrails
The recurring failure modes are predictable and have known mitigations. Treat classic and fine-grained PATs like passwords: enable GitHub Secret Scanning (default for public repositories, enabled via Advanced Security for private ones) so leaked PATs are auto-revoked on push, and turn on push protection so commits containing detected secrets are blocked before they ever reach main. Audit repo-scoped classic PATs because the scope grants substantially more than typical scripts need; replacement with a fine-grained PAT or App scoped to the actual API surface is almost always possible. Avoid the bot-user-with-a-PAT pattern — it violates GitHub’s Terms of Service and gives the same blast radius and turnover risk as a real user account. Always declare an explicit permissions: block in Actions workflows: older repositories may default to write-all, which is the Actions equivalent of running every job as root. Rotate App private keys on a defined cadence (annually is a reasonable baseline) and store them in a secrets manager such as 1Password or AWS Secrets Manager rather than relying on plain Actions secrets when the organisation has stricter controls. Finally, remember that GITHUB_TOKEN cannot push or read across repositories — workflows that fan out to multiple repos must use an App installation token, not a workflow-scoped token, even if the source repo’s GITHUB_TOKEN happens to share the same owner.
Alternatives considered and deprecated paths
A few authentication mechanisms exist outside the five-token taxonomy and should be understood mainly to rule them out. OAuth Apps were the predecessor to GitHub Apps for third-party integrations, but they offer only user-tokens — no machine-to-machine installation token — and GitHub now steers all new third-party integrations to GitHub Apps; new work should not adopt OAuth Apps. Deploy keys are SSH keys scoped to a single repository with read or read/write access for git push/pull only, with no REST API surface beyond that; they remain useful for narrow git-only deployment hooks but cannot drive API automation such as PR creation or label management. Service-account-style PATs on a dedicated machine user account are a recurring temptation because they appear simpler than registering an App, but they violate the one-account-per-person Terms of Service, leave the same audit-attribution gap as any other PAT, and reproduce the turnover problem the App model is designed to eliminate.
Sources
- GitHub Docs — About creating GitHub Apps: https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps
- GitHub Docs — Authenticating to the REST API: https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api
- GitHub Docs — Primary rate limits per token type: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api
- GitHub Docs — Rate-limit scaling for App installations: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- GitHub Docs — Managing your personal access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
- GitHub Docs — Keeping your API credentials secure: https://docs.github.com/en/rest/authentication/keeping-your-api-credentials-secure
- GitHub Docs — Apps REST API (installation access token, 1-hour expiry): https://docs.github.com/en/rest/apps/apps
- GitHub Docs — Org fine-grained PAT approval API: https://docs.github.com/en/rest/orgs/personal-access-tokens
- GitHub Docs — Automatic token authentication (GITHUB_TOKEN permissions): https://docs.github.com/en/actions/security-guides/automatic-token-authentication
Backlinks
No backlinks yet