git-ecosystem/git-credential-manager · critical · Trace2OAuth2Exception
Invalid ' ' in response; does not match the request.
Error message
Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request. What it means
Companion to the missing-state check in GetAuthorizationCodeAsync: the response did contain a state parameter, but its value does not match the one sent in the authorization request (compared ordinally). Per OAuth2 this indicates a badly implemented authorization server, a CSRF attempt, or a replay — the client must terminate the flow.
Solutions
- Ensure the exact same state string passed to the authorization request is passed to GetAuthorizationCodeAsync.
- Always use the fresh redirect/callback URL from the current login attempt; never reuse URLs from earlier flows.
- If running concurrent logins, keep per-attempt state and match each callback to its request.
- Investigate the identity provider if it legitimately fails to echo state — it is non-compliant with OAuth2.
Example fix
// before
var state = Guid.NewGuid().ToString("N");
// ... browser flow ...
await client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, cachedCallbackUrl); // old state
// after
await client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, currentCallbackUrl); // same attempt's URL Defensive patterns
Strategy: try-catch
Validate before calling
var responseParams = ParseResponseParams(callbackUrl);
if (responseParams.TryGetValue("state", out var s) && !StringComparer.Ordinal.Equals(s, state))
throw new InvalidOperationException("State mismatch: possible CSRF/replay; aborting."); Try / catch
try
{
result = await client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, callbackUrl);
}
catch (Trace2OAuth2Exception ex) when (ex.Message.Contains("does not match the request"))
{
// state mismatch: CSRF or stale callback; discard and restart login
return AuthFailure.StateMismatch;
} Prevention
- Bind each callback URL to its login attempt; never reuse callback URLs across attempts.
- Generate fresh random state per attempt and compare ordinally before calling the API.
- Serialize concurrent sign-in attempts or match callbacks per-session.
- Treat any mismatch as a security event and abort — never proceed despite mismatch.
When it happens
Trigger: Calling GetAuthorizationCodeAsync with a response whose 'state' value differs from the state argument — e.g. reusing a redirect URL from a previous login attempt, hardcoding state in one place, or an IdP generating its own state.
Common situations: Replaying an old callback URL after the state was regenerated; multiple concurrent sign-in attempts where callbacks got crossed; a custom/stub IdP echoing a different or empty state; copy-pasting a callback URL from a prior session.
Related errors
- Missing ' ' in response.
- Unencrypted HTTP is not recommended for Bitbucket.org…
- 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/567b16a1f4a63c18.
Report an issue: GitHub.
Appendix: source
Thrown at src/Core/Authentication/OAuth/OAuth2Client.cs:186
Uri authorizationUri = authorizationUriBuilder.Uri;
// Open the browser at the request URI to start the authorization code grant flow, and
// intercept the response parameters delivered to the redirect URI.
IDictionary<string, string> responseParams =
await browser.GetAuthenticationResponseAsync(authorizationUri, redirectUri, _responseMode, ct);
// Check for errors serious enough we should terminate the flow, such as if the state value returned does
// not match the one we passed. This indicates a badly implemented Authorization Server, or worse, some
// form of failed MITM or replay attack.
if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState))
{
throw new Trace2OAuth2Exception(_trace2,
$"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.");
}
if (!StringComparer.Ordinal.Equals(state, replyState))
{
throw new Trace2OAuth2Exception(_trace2,
$"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request.");
}
// We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason)
if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode))
{
throw new Trace2OAuth2Exception(_trace2,
$"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response.");
}
return new OAuth2AuthorizationCodeResult(authCode, redirectUri, codeVerifier);
}
public async Task<OAuth2DeviceCodeResult> GetDeviceCodeAsync(IEnumerable<string> scopes, CancellationToken ct)
{
var label = "get device code";
using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);
View on GitHub (pinned to e8ce762cd0)