git-ecosystem/git-credential-manager · error

OAuth2 response error (from device code token response)

Error message

OAuth2 response error (from device code token response)

What it means

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.

Solutions

  1. Handle expired_token by restarting the flow: call GetDeviceCodeAsync again to get a fresh device/user code and prompt the user to retry.
  2. Surface access_denied to the user as 'authorization was denied' and offer to restart; do not retry — it is terminal.
  3. Prompt the user promptly and show the user code immediately to avoid the code expiring before completion.
  4. Verify client_id correctness and that device flow is enabled for the application if invalid_client/unauthorized_client is reported.
  5. Catch OAuth2Exception around the polling loop and branch on the Error property rather than treating all failures as transient.

Example fix

// before: assuming all failures are transient and retrying
var token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);
// after: restart the device flow on terminal errors
try
{
    token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);
}
catch (OAuth2Exception ex) when (ex.Error is "expired_token" or "access_denied")
{
    deviceCodeResult = await client.GetDeviceCodeAsync(scopes, ct); // fresh code, re-prompt user
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting the poll, ensure the device code result is fresh
if (deviceCodeResult is null || deviceCodeResult.ExpiresIn <= 0)
    throw new InvalidOperationException("Device code result missing or already expired; call GetDeviceCodeAsync again");

Type guard

static bool IsTerminalDeviceError(Exception ex) => ex is OAuth2Exception o && o.Error is not ("authorization_pending" or "slow_down");

Try / catch

try
{
    token = await client.GetTokenByDeviceCodeAsync(deviceCodeResult, ct);
}
catch (OAuth2Exception ex) when (ex.Error == "expired_token")
{
    deviceCodeResult = await client.GetDeviceCodeAsync(scopes, ct); // fresh code, re-prompt user
}
catch (OAuth2Exception ex) when (ex.Error == "access_denied")
{
    ShowUserDeniedMessage(); // terminal, do not retry
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of git-ecosystem/git-credential-manager@e8ce762cd0 (2026-09-11). Data as JSON: /api/errors/2830ca84b5b4cabc. Report an issue: GitHub.

Appendix: source

Thrown at src/Core/Authentication/OAuth/OAuth2Client.cs:350

                        if (response.IsSuccessStatusCode && TryCreateTokenEndpointResult(json, out OAuth2TokenResult result))
                        {
                            return result;
                        }

                        TryDeserializeJson(json, OAuthJsonContext.Default.ErrorResponseJson, out ErrorResponseJson error);

                        switch (error?.Error)
                        {
                            case OAuth2Constants.DeviceAuthorization.Errors.AuthorizationPending:
                                // Retry with the current polling interval value
                                break;
                            case OAuth2Constants.DeviceAuthorization.Errors.SlowDown:
                                // We must increase the polling interval by 5 seconds
                                retryInterval = retryInterval.Add(TimeSpan.FromSeconds(5));
                                break;
                            default:
                                // For all other errors do not retry
                                throw CreateExceptionFromResponse(json);
                        }
                    }
                }
                catch (TimeoutException)
                {
                    // Back-off exponentially (2 * x = x + x)
                    retryInterval += retryInterval;
                }

                // Wait the polling interval before retrying
                await Task.Delay(retryInterval, ct);
            }
        }

        #endregion

        #region Extension Points

View on GitHub (pinned to e8ce762cd0)