OAuth2 "invalid_grant" from the token endpoint in CI
The OAuth2 token endpoint rejected the credential you exchanged: an authorization code that expired or was already used, an invalid refresh token, or wrong resource-owner credentials. The provider returns HTTP 400 with "error":"invalid_grant".
What this error means
A step that exchanges a grant for an access token gets HTTP 400 with a JSON body containing "error":"invalid_grant". It often appears when a CI-stored refresh token has been revoked or an authorization code is replayed.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error":"invalid_grant","error_description":"The provided authorization grant is invalid, expired, revoked, or does not match the redirection URI."}Common causes
An expired or already-used authorization code
Authorization codes are single-use and short-lived. A retried or replayed code exchange returns invalid_grant.
A revoked or rotated refresh token
The refresh token stored as a CI secret was revoked, expired, or rotated by the provider, so the exchange fails.
How to fix it
Use a fresh grant per run
- Do not cache an authorization code across retries; each is single-use.
- For machine-to-machine flows, use client_credentials instead of a stored refresh token.
- If a refresh token is required, rotate the CI secret when the provider rotates it.
curl -s -X POST "$TOKEN_URL" \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET"Read error_description for the exact grant that failed
The error_description field names whether the code, refresh token, or redirect URI mismatched, so you fix the right input.
How to prevent it
- Prefer client_credentials for CI over long-lived refresh tokens.
- Never retry a single-use authorization code exchange.
- Rotate stored refresh tokens promptly when the provider rotates them.