{"record":{"id":"e5dd9286801062c0","repo":"git-ecosystem/git-credential-manager","slug":"oauth2-response-error-from-token-endpoint-respons","errorCode":null,"errorMessage":"OAuth2 response error (from token endpoint response)","messagePattern":"OAuth2 response error \\(from token endpoint response\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/Core/Authentication/OAuth/OAuth2Client.cs","lineNumber":273,"sourceCode":"            }\r\n\r\n            if (authorizationCodeResult.CodeVerifier != null)\r\n            {\r\n                formData[OAuth2Constants.TokenEndpoint.PkceVerifierParameter] = authorizationCodeResult.CodeVerifier;\r\n            }\r\n\r\n            using (HttpContent requestContent = new FormUrlEncodedContent(formData))\r\n            using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader))\r\n            using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct))\r\n            {\r\n                string json = await response.Content.ReadAsStringAsync();\r\n\r\n                if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))\r\n                {\r\n                    return result;\r\n                }\r\n\r\n                throw CreateExceptionFromResponse(json);\r\n            }\r\n        }\r\n\r\n        public async Task<OAuth2TokenResult> GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct)\r\n        {\r\n            var label = \"get token by refresh token\";\r\n            using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);\r\n\r\n            var formData = new Dictionary<string, string>\r\n            {\r\n                [OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.RefreshTokenGrantType,\r\n                [OAuth2Constants.TokenEndpoint.RefreshTokenParameter] = refreshToken,\r\n                [OAuth2Constants.ClientIdParameter] = _clientId,\r\n                [OAuth2Constants.ClientSecretParameter] = _clientSecret\r\n            };\r\n\r\n            if (_redirectUri != null)\r\n            {\r","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/git-ecosystem/git-credential-manager/blob/e8ce762cd04b4100ae637b5fbf39ef9d0a96561e/src/Core/Authentication/OAuth/OAuth2Client.cs#L255-L291","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: re-exchanging a stored code causes invalid_grant\nvar token = await client.GetTokenByAuthorizationCodeAsync(cachedCodeResult, ct);\n// after: always use a fresh code from a new authorization request\nvar codeResult = await client.GetAuthorizationCodeAsync(scopes, browser, ct);\nvar token = await client.GetTokenByAuthorizationCodeAsync(codeResult, ct);","handlingStrategy":"try-catch","validationCode":"// Before exchanging, ensure the code result is fresh and consistent\nif (authorizationCodeResult is null || string.IsNullOrEmpty(authorizationCodeResult.Code))\n    throw new InvalidOperationException(\"No authorization code available\");\n// Ensure the code has not already been exchanged once (codes are single-use)\nif (Interlocked.Exchange(ref exchanged, 1) == 1)\n    throw new InvalidOperationException(\"Authorization code already redeemed; start a new authorization flow\");","typeGuard":"static bool HasServerError(Exception ex, string code) => ex is OAuth2Exception o && string.Equals(o.Error, code, StringComparison.Ordinal);","tryCatchPattern":"try\n{\n    var token = await client.GetTokenByAuthorizationCodeAsync(codeResult, ct);\n}\ncatch (OAuth2Exception ex) when (ex.Error == \"invalid_grant\")\n{\n    // Code expired/reused or PKCE mismatch: restart the full authorization flow\n    await RestartAuthorizationAsync(ct);\n}","preventionTips":["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."],"tags":["oauth2","token-exchange","authorization-code","pkce","http-error-response"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"e8ce762cd04b4100ae637b5fbf39ef9d0a96561e","analyzedAt":"2026-09-11T17:15:08.753Z","contentChangedAt":"2026-09-11T17:15:08.753Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}