GitHub App OAuth BAD_REFRESH_TOKEN — What it means & how to fix it
How to fix GitHub App OAuth refresh token issues
"error": "bad_refresh_token", "error_description": "The refresh token passed is incorrect or expired."
If you use the GitHub App OAuth flow to access user data, your refresh calls can fail with a bad_refresh_token error. Many OAuth clients surface the same failure as invalid_grant. It can halt background syncs, user-triggered actions, and webhook retries until you fix the root cause. This guide is specific to GitHub App user access tokens (not classic OAuth Apps).
For broader context on why refresh errors show up even in mature OAuth flows, see API Auth Is Deeper Than It Looks and Why OAuth Is Still Hard.
Spot the error
When your backend POSTs to https://github.com/login/oauth/access_token with grant_type=refresh_token to swap a refresh token for a new user access token, GitHub can answer with HTTP error code 400:
That single JSON payload means the refresh token is unusable. Retrying the exact same request will keep failing until the underlying issue is addressed.
Sometimes GitHub clients surface the same failure as invalid_grant, but the remediation is the same: re-auth or fix token lifecycle problems.
Why was the refresh token revoked?
GitHub App refresh tokens are not permanent. They expire, rotate, and can be revoked by the user or by GitHub. The most common causes are below.
Most common: You refreshed successfully but did not store the new refresh token
GitHub rotates refresh tokens. When you refresh, GitHub returns a new refresh token and invalidates the old one. If you keep using the previous token (or overwrite the new value in a race), your next refresh fails with bad_refresh_token (often surfaced as invalid_grant).
Refresh token expired (about six-month lifetime)
GitHub refresh tokens expire after a fixed lifetime (about six months). If a refresh token sits unused for too long, GitHub invalidates it and refresh is no longer possible. Once expired, the only fix is re-auth.
User or org revoked the GitHub App authorization
If the user disconnects your GitHub App or an organization admin revokes the app's access, all refresh tokens tied to that authorization become invalid immediately.
- The user removes the GitHub App from their account settings
- An org admin removes the app or changes access policies
The refresh token was used twice (race condition)
Because refresh tokens rotate, they are effectively single-use. If two workers refresh at the same time:
- Worker A refreshes and gets a new refresh token
- Worker B refreshes with the now-stale token and gets
bad_refresh_token
This problem shows up under load and is easy to miss until you have multiple workers or retries running in parallel.
Security revocation (token exposed or flagged)
GitHub can revoke tokens for security reasons, such as detecting token exposure in a public repo or other suspicious activity. When that happens, refresh fails permanently and you must re-authorize.
How to fix it
1. Confirm you are using the latest refresh token
After every successful refresh, persist the new refresh_token. Treat this as a required write, not a best-effort update.
2. Eliminate refresh races
If multiple workers can refresh the same connection, you need a single-flight guard:
- Only one refresh per connection at a time
- Atomic writes of
(access_token, refresh_token, expires_at) - Other workers wait for the refreshed token instead of racing
If you want a deeper dive on single-flight patterns, see How to handle concurrency with OAuth token refresh concurrency.
3. Detect revocation and re-auth fast
If bad_refresh_token (or invalid_grant) happens twice in a row, the refresh token is almost certainly gone. Mark the connection as re-auth required, pause background jobs, and prompt the user to reconnect your GitHub App.
How to prevent refresh token issues
A few engineering habits go a long way:
- Always store the rotated refresh token
Persist the newest refresh token after each refresh. - Refresh on a cadence
Keep the refresh token active so it does not age out silently. - Make refresh single-flight
Prevent race conditions across workers and retries. - Monitor
bad_refresh_tokenspikes
Retry once, then mark re-auth required if it fails again. - Build a first-class reconnect flow
Make it easy for users to re-authorize the GitHub App.
Skip the headache, let Nango refresh for you
Nango is an open-source auth layer that handles OAuth token lifecycle issues for you:
- Secure storage for access and refresh tokens
- Automatic access-token refresh
- Safe handling of rotated refresh tokens
- Concurrency-safe refresh logic
- Clear signals when a connection needs re-auth
If you are building a GitHub App API integration and tired of token lifecycle edge cases, Nango can manage the refresh pipeline so you can focus on product.




