{"record":{"id":"567b16a1f4a63c18","repo":"git-ecosystem/git-credential-manager","slug":"invalid-oauth2constants-authorizationgrantrespon","errorCode":null,"errorMessage":"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request.","messagePattern":"Invalid '(.+?)' in response; does not match the request\\.","errorType":"exception","errorClass":"Trace2OAuth2Exception","httpStatus":null,"severity":"critical","filePath":"src/Core/Authentication/OAuth/OAuth2Client.cs","lineNumber":186,"sourceCode":"\r\n            Uri authorizationUri = authorizationUriBuilder.Uri;\r\n\r\n            // Open the browser at the request URI to start the authorization code grant flow, and\r\n            // intercept the response parameters delivered to the redirect URI.\r\n            IDictionary<string, string> responseParams =\r\n                await browser.GetAuthenticationResponseAsync(authorizationUri, redirectUri, _responseMode, ct);\r\n\r\n            // Check for errors serious enough we should terminate the flow, such as if the state value returned does\r\n            // not match the one we passed. This indicates a badly implemented Authorization Server, or worse, some\r\n            // form of failed MITM or replay attack.\r\n            if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2,\r\n                    $\"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response.\");\r\n            }\r\n            if (!StringComparer.Ordinal.Equals(state, replyState))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2,\r\n                    $\"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request.\");\r\n            }\r\n\r\n            // We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason)\r\n            if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode))\r\n            {\r\n                throw new Trace2OAuth2Exception(_trace2,\r\n                    $\"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response.\");\r\n            }\r\n\r\n            return new OAuth2AuthorizationCodeResult(authCode, redirectUri, codeVerifier);\r\n        }\r\n\r\n        public async Task<OAuth2DeviceCodeResult> GetDeviceCodeAsync(IEnumerable<string> scopes, CancellationToken ct)\r\n        {\r\n            var label = \"get device code\";\r\n            using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label);\r\n\r","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/git-ecosystem/git-credential-manager/blob/e8ce762cd04b4100ae637b5fbf39ef9d0a96561e/src/Core/Authentication/OAuth/OAuth2Client.cs#L168-L204","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvar state = Guid.NewGuid().ToString(\"N\");\n// ... browser flow ...\nawait client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, cachedCallbackUrl); // old state\n// after\nawait client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, currentCallbackUrl); // same attempt's URL","handlingStrategy":"try-catch","validationCode":"var responseParams = ParseResponseParams(callbackUrl);\nif (responseParams.TryGetValue(\"state\", out var s) && !StringComparer.Ordinal.Equals(s, state))\n    throw new InvalidOperationException(\"State mismatch: possible CSRF/replay; aborting.\");","typeGuard":null,"tryCatchPattern":"try\n{\n    result = await client.GetAuthorizationCodeAsync(endpoints, clientId, redirectUri, scopes, state, verifier, callbackUrl);\n}\ncatch (Trace2OAuth2Exception ex) when (ex.Message.Contains(\"does not match the request\"))\n{\n    // state mismatch: CSRF or stale callback; discard and restart login\n    return AuthFailure.StateMismatch;\n}","preventionTips":["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."],"tags":["oauth2","csrf","state-mismatch","replay-attack","security"],"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"}