{"record":{"id":"a5981b1fa6c348ba","repo":"elsa-workflows/elsa-core","slug":"the-identity-provider-callback-could-not-be-correlated","errorCode":null,"errorMessage":"The identity provider callback could not be correlated.","messagePattern":"The identity provider callback could not be correlated\\.","errorType":"exception","errorClass":"OpenIdConnectAuthenticationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs","lineNumber":85,"sourceCode":"            [\"client_id\"] = settings.ClientId,\n            [\"redirect_uri\"] = GetCallbackUri(context.Connection, context.Transaction.Purpose).AbsoluteUri,\n            [\"scope\"] = string.Join(' ', settings.Scopes),\n            [\"state\"] = context.CorrelationState,\n            [\"nonce\"] = nonce,\n            [\"code_challenge\"] = CreateCodeChallenge(verifier),\n            [\"code_challenge_method\"] = \"S256\"\n        };\n\n        return new(WithQuery(metadata.AuthorizationEndpoint, query), state);\n    }\n\n    public async ValueTask<ExternalAuthenticationResult> AuthenticateCallbackAsync(ExternalCallbackContext context, CancellationToken cancellationToken = default)\n    {\n        if (TryGetParameter(context.Parameters, \"error\", out _))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider rejected the authentication request.\");\n\n        if (!TryGetParameter(context.Parameters, \"state\", out var state) || !FixedTimeEquals(state, context.CorrelationState))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider callback could not be correlated.\");\n\n        var settings = await GetSettingsAsync(context.Connection.Connection.AdapterSettings, cancellationToken);\n        var metadata = await ResolveMetadataAsync(settings, cancellationToken);\n        var adapterState = ReadAdapterState(context.Transaction.ProtectedPayload);\n        if (adapterState is not null && !string.Equals(adapterState.Issuer, metadata.Issuer, StringComparison.Ordinal))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider callback issuer did not match the initiated request.\");\n\n        var idToken = await ExchangeCodeAsync(settings, metadata, context, adapterState?.CodeVerifier, cancellationToken);\n        var principal = await ValidateIdTokenAsync(idToken, settings, metadata, cancellationToken);\n        var nonce = principal.FindFirst(\"nonce\")?.Value;\n        var expectedNonce = context.Transaction.ProviderNonce ?? adapterState?.Nonce;\n        if (string.IsNullOrWhiteSpace(expectedNonce) || !FixedTimeEquals(nonce, expectedNonce))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider nonce did not match the initiated request.\");\n\n        var issuer = principal.FindFirst(\"iss\")?.Value ?? metadata.Issuer;\n        var subject = principal.FindFirst(\"sub\")?.Value;\n        if (string.IsNullOrWhiteSpace(subject))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider response did not contain a subject.\");","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs#L67-L103","documentation":"After confirming the callback carries no 'error' parameter, AuthenticateCallbackAsync verifies the 'state' query parameter with FixedTimeEquals against context.CorrelationState (the value generated when the login flow started). If state is absent or does not match, the callback cannot be tied to an initiated request and this exception is thrown, guarding against CSRF and forged callbacks.","triggerScenarios":"AuthenticateCallbackAsync receives no 'state' parameter, or a state value that does not byte-compare equal to the correlation state stored for the transaction.","commonSituations":"Multiple concurrent logins overwriting one another's correlation data; the correlation store is in-memory and restarted between the redirect and the callback; load-balanced instances without shared state; cookies/localStorage lost, so the client's state was not sent back.","solutions":["Ensure the correlation state survives between the initiation and the callback (shared/durable store in multi-instance deployments).","Verify the entire callback URL including the state parameter is passed through unchanged (no proxy stripping query values).","Restart the login flow: initiate a new authorization request and retry the callback with fresh state."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if (!Request.Query.ContainsKey(\"state\")) return Results.Problem(\"Missing state parameter; restart login.\");","typeGuard":null,"tryCatchPattern":"try { await adapter.AuthenticateCallbackAsync(context); } catch (OpenIdConnectAuthenticationException ex) when (ex.Message.Contains(\"correlated\")) { return Results.Redirect(\"/login?reason=state-mismatch\"); }","preventionTips":["Use a shared/durable correlation store across all server instances.","Never alter or truncate the state query parameter in proxies or URL rewriting rules.","Restart the whole authorization flow on any state mismatch instead of retrying the callback."],"tags":["openid-connect","authentication","csrf","state-mismatch"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}