{"record":{"id":"2830ca84b5b4cabc","repo":"git-ecosystem/git-credential-manager","slug":"oauth2-response-error-from-device-code-token-resp","errorCode":null,"errorMessage":"OAuth2 response error (from device code token response)","messagePattern":"OAuth2 response error \\(from device code token response\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/Core/Authentication/OAuth/OAuth2Client.cs","lineNumber":350,"sourceCode":"                        if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))\r\n                        {\r\n                            return result;\r\n                        }\r\n\r\n                        TryDeserializeJson(json, OAuthJsonContext.Default.ErrorResponseJson, out ErrorResponseJson error);\r\n\r\n                        switch (error?.Error)\r\n                        {\r\n                            case OAuth2Constants.DeviceAuthorization.Errors.AuthorizationPending:\r\n                                // Retry with the current polling interval value\r\n                                break;\r\n                            case OAuth2Constants.DeviceAuthorization.Errors.SlowDown:\r\n                                // We must increase the polling interval by 5 seconds\r\n                                retryInterval = retryInterval.Add(TimeSpan.FromSeconds(5));\r\n                                break;\r\n                            default:\r\n                                // For all other errors do not retry\r\n                                throw CreateExceptionFromResponse(json);\r\n                        }\r\n                    }\r\n                }\r\n                catch (TimeoutException)\r\n                {\r\n                    // Back-off exponentially (2 * x = x + x)\r\n                    retryInterval += retryInterval;\r\n                }\r\n\r\n                // Wait the polling interval before retrying\r\n                await Task.Delay(retryInterval, ct);\r\n            }\r\n        }\r\n\r\n        #endregion\r\n\r\n        #region Extension Points\r\n\r","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/git-ecosystem/git-credential-manager/blob/e8ce762cd04b4100ae637b5fbf39ef9d0a96561e/src/Core/Authentication/OAuth/OAuth2Client.cs#L332-L368","documentation":"Thrown by OAuth2Client.GetTokenByDeviceCodeAsync when the token endpoint returns a terminal error while polling for the device-code grant. authorization_pending and slow_down are retried automatically, but any other error (default case in the switch) is converted via CreateExceptionFromResponse into an OAuth2Exception — e.g. expired_token (user took too long), access_denied (user refused), or invalid_client. The polling loop stops and no token is issued.","triggerScenarios":"Polling GetTokenByDeviceCodeAsync when the device code expires before the user completes sign-in (expired_token), the user denies the request (access_denied), the client_id is wrong (invalid_client/unauthorized_client), or the server returns any error other than authorization_pending/slow_down during the poll loop.","commonSituations":"Users abandoning the browser sign-in until the device code's ~10-15 minute lifetime lapses, users clicking 'Cancel/No' on the consent page, device flow disabled for the app registration, or sharing a stale OAuth2DeviceCodeResult across sessions after it already expired.","solutions":["Handle expired_token by restarting the flow: call GetDeviceCodeAsync again to get a fresh device/user code and prompt the user to retry.","Surface access_denied to the user as 'authorization was denied' and offer to restart; do not retry — it is terminal.","Prompt the user promptly and show the user code immediately to avoid the code expiring before completion.","Verify client_id correctness and that device flow is enabled for the application if invalid_client/unauthorized_client is reported.","Catch OAuth2Exception around the polling loop and branch on the Error property rather than treating all failures as transient."],"exampleFix":"// before: assuming all failures are transient and retrying\nvar token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);\n// after: restart the device flow on terminal errors\ntry\n{\n    token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);\n}\ncatch (OAuth2Exception ex) when (ex.Error is \"expired_token\" or \"access_denied\")\n{\n    deviceCodeResult = await client.GetDeviceCodeAsync(scopes, ct); // fresh code, re-prompt user\n}","handlingStrategy":"try-catch","validationCode":"// Before starting the poll, ensure the device code result is fresh\nif (deviceCodeResult is null || deviceCodeResult.ExpiresIn <= 0)\n    throw new InvalidOperationException(\"Device code result missing or already expired; call GetDeviceCodeAsync again\");","typeGuard":"static bool IsTerminalDeviceError(Exception ex) => ex is OAuth2Exception o && o.Error is not (\"authorization_pending\" or \"slow_down\");","tryCatchPattern":"try\n{\n    token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);\n}\ncatch (OAuth2Exception ex) when (ex.Error == \"expired_token\")\n{\n    deviceCodeResult = await client.GetDeviceCodeAsync(scopes, ct); // fresh code, re-prompt user\n}\ncatch (OAuth2Exception ex) when (ex.Error == \"access_denied\")\n{\n    ShowUserDeniedMessage(); // terminal, do not retry\n}","preventionTips":["Display the user code and verification URI immediately and keep them visible so users finish sign-in before expiry.","Never reuse a device code result across sessions or after a terminal error.","Branch on OAuth2Exception.Error: only expired_token/access_denied-style errors are terminal; the library already handles pending/slow_down.","Honor the polling interval returned by the device authorization endpoint to avoid slow_down escalation."],"tags":["oauth2","device-flow","polling","access-denied","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"}