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

  1. Read the OAuth2Exception.Error from the throw to identify the server's reason (invalid_grant, invalid_client, invalid_request, etc.).
  2. Ensure each authorization code is exchanged exactly once and immediately; restart the whole authorization flow if the code was already consumed or expired.
  3. Verify the redirect_uri passed to GetTokenByAuthorizationCodeAsync matches the one used in GetAuthorizationCodeAsync exactly, including trailing slashes.
  4. Check client_id/client_secret configuration and that the PKCE code_verifier comes from the same OAuth2AuthorizationCodeResult as the code.
  5. 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

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


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)