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
- 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.
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
- 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.
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
- OAuth2 response error (from device authorization endpoint…
- OAuth2 response error (from token endpoint response)
- Bitbucket DC OAuth Client ID must be defined
- Bitbucket DC OAuth Client Secret must be defined
- RemoteUri must be defined to generate Bitbucket DC OAuth2…
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)