git-ecosystem/git-credential-manager · error
OAuth2 response error (from token endpoint response)
Error message
OAuth2 response error (from token endpoint response)
What it means
Thrown by OAuth2Client.GetTokenByAuthorizationCodeAsync when the token endpoint does not return a success response that deserializes into a TokenEndpointResponseJson (i.e. no access token). The response body is converted via CreateExceptionFromResponse into an OAuth2Exception with the server's error code (e.g. invalid_grant, invalid_client) when it is standard RFC 6749 error JSON, otherwise a generic Trace2OAuth2Exception. It means the authorization-code grant failed and no tokens were issued.
Solutions
- Read the OAuth2Exception.Error from the throw to identify the server's reason (invalid_grant, invalid_client, invalid_request, etc.).
- Ensure each authorization code is exchanged exactly once and immediately; restart the whole authorization flow if the code was already consumed or expired.
- Verify the redirect_uri passed to GetTokenByAuthorizationCodeAsync matches the one used in GetAuthorizationCodeAsync exactly, including trailing slashes.
- Check client_id/client_secret configuration and that the PKCE code_verifier comes from the same OAuth2AuthorizationCodeResult as the code.
- Inspect the raw body in 'Unknown OAuth error' messages for proxy/CDN interference returning non-JSON responses.
Example fix
// before: re-exchanging a stored code causes invalid_grant var token = await client.GetTokenByAuthorizationCodeAsync(cachedCodeResult, ct); // after: always use a fresh code from a new authorization request var codeResult = await client.GetAuthorizationCodeAsync(scopes, browser, ct); var token = await client.GetTokenByAuthorizationCodeAsync(codeResult, ct);
Defensive patterns
Strategy: try-catch
Validate before calling
// Before exchanging, ensure the code result is fresh and consistent
if (authorizationCodeResult is null || string.IsNullOrEmpty(authorizationCodeResult.Code))
throw new InvalidOperationException("No authorization code available");
// Ensure the code has not already been exchanged once (codes are single-use)
if (Interlocked.Exchange(ref exchanged, 1) == 1)
throw new InvalidOperationException("Authorization code already redeemed; start a new authorization flow"); Type guard
static bool HasServerError(Exception ex, string code) => ex is OAuth2Exception o && string.Equals(o.Error, code, StringComparison.Ordinal);
Try / catch
try
{
var token = await client.GetTokenByAuthorizationCodeAsync(codeResult, ct);
}
catch (OAuth2Exception ex) when (ex.Error == "invalid_grant")
{
// Code expired/reused or PKCE mismatch: restart the full authorization flow
await RestartAuthorizationAsync(ct);
} Prevention
- Never cache or re-use an authorization code; exchange it exactly once immediately after the redirect callback.
- Keep the redirect_uri byte-for-byte identical between the authorization request and the token exchange.
- Store client_id/client_secret in configuration, not code, and rotate them consistently.
- Pass the code_verifier from the same OAuth2AuthorizationCodeResult as the code — never regenerate it.
When it happens
Trigger: Calling GetTokenByAuthorizationCodeAsync with an authorization code that was already redeemed or expired (invalid_grant), a PKCE code_verifier that does not match the challenge, a redirect_uri that differs byte-for-byte from the one used in the authorization request, wrong client_id/client_secret (invalid_client), or a token endpoint returning non-JSON (proxy/HTML error page).
Common situations: Double-exchanging the same auth code (browser retry, double callback handling), redirect URI trailing-slash mismatch (the library compares redirect URLs byte-for-byte), rotated or missing client secret, clock skew invalidating the code within its short lifetime, or PKCE verifier regenerated between the authorization and token requests.
Related errors
- Extra query parameter
- Missing ' ' in response.
- Missing ' ' in response.
- Unknown PKCE code challenge method.
- OAuth2 response error (from device authorization endpoint…
AI-assisted analysis of git-ecosystem/git-credential-manager@e8ce762cd0 (2026-09-11).
Data as JSON: /api/errors/e5dd9286801062c0.
Report an issue: GitHub.
Appendix: source
Thrown at src/Core/Authentication/OAuth/OAuth2Client.cs:273
}
if (authorizationCodeResult.CodeVerifier != null)
{
formData[OAuth2Constants.TokenEndpoint.PkceVerifierParameter] = authorizationCodeResult.CodeVerifier;
}
using (HttpContent requestContent = new FormUrlEncodedContent(formData))
using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader))
using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))
{
string json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))
{
return result;
}
throw CreateExceptionFromResponse(json);
}
}
public async Task<OAuth2TokenResult> GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct)
{
var label = "get token by refresh token";
using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);
var formData = new Dictionary<string, string>
{
[OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.RefreshTokenGrantType,
[OAuth2Constants.TokenEndpoint.RefreshTokenParameter] = refreshToken,
[OAuth2Constants.ClientIdParameter] = _clientId,
[OAuth2Constants.ClientSecretParameter] = _clientSecret
};
if (_redirectUri != null)
{
View on GitHub (pinned to e8ce762cd0)