GitHub App works for about an hour, then every API call returns 401 "Bad credentials #211715
🏷️ Discussion TypeQuestion 💬 Feature/Topic AreaApps BodyI built a GitHub App that comments on pull requests. On startup I generate a JWT from the app’s private key, call POST /app/installations/{id}/access_tokens, and store the returned token in a variable. Everything works, but after roughly an hour all calls start failing with 401 Bad credentials. Restarting the service fixes it until it happens again. I’m also occasionally seeing 403 Resource not accessible by integration on POST /repos/{owner}/{repo}/issues/{n}/comments on one specific repo, even though the same code works on others. Do installation tokens expire? How are people handling this in long-running services? |
Replies: 1 comment
|
Yes — two different lifetimes are involved, and you’re caching the wrong one for too long: The app JWT (signed with your private key, RS256) is valid for at most 10 minutes. It’s only used to talk about the app itself and to mint installation tokens. So the fix is to treat installation tokens as short-lived credentials: cache them per installation with their expires_at, and mint a new one when they’re within a minute or two of expiry. Don’t store them at process start. Minimal version with Octokit, which does the caching/refresh for you: import { App } from "octokit"; const app = new App({ // Per request: get an Octokit bound to that installation. If you’re hand-rolling it, generate the JWT with iat set ~60 s in the past (clock drift) and exp ≤ iat + 600, then call POST /app/installations/{installation_id}/access_tokens and keep the token until expires_at minus a safety margin. The 403 Resource not accessible by integration on one repo is a permissions problem, not an auth one. Common causes: You added a permission (e.g. Pull requests: write or Issues: write) to the app after it was installed there. Permission changes aren’t applied until the org/user owner accepts them — check the installation page under Settings → GitHub Apps. |
Yes — two different lifetimes are involved, and you’re caching the wrong one for too long:
The app JWT (signed with your private key, RS256) is valid for at most 10 minutes. It’s only used to talk about the app itself and to mint installation tokens.
The installation access token is valid for 1 hour. That’s the one you use for repo API calls, and it’s what’s expiring in your service.
So the fix is to treat installation tokens as short-lived credentials: cache them per installation with their expires_at, and mint a new one when they’re within a minute or two of expiry. Don’t store them at process start.
Minimal version with Octokit, which does the caching/refresh for you:
import { App } fr…