{"record":{"id":"2d4b34f8d3c32389","repo":"elsa-workflows/elsa-core","slug":"the-identity-provider-token-exchange-failed","errorCode":null,"errorMessage":"The identity provider token exchange failed.","messagePattern":"The identity provider token exchange failed\\.","errorType":"exception","errorClass":"OpenIdConnectAuthenticationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs","lineNumber":179,"sourceCode":"            [\"code\"] = code,\n            [\"redirect_uri\"] = GetCallbackUri(context.Connection, context.Transaction.Purpose).AbsoluteUri\n        };\n        if (verifier is not null)\n            values[\"code_verifier\"] = verifier;\n        if (!context.Secrets.TryGetValue(\"clientSecret\", out var secret))\n            throw new OpenIdConnectAuthenticationException(\"The provider client secret is unavailable.\");\n        IReadOnlyDictionary<string, string>? headers = null;\n        if (settings.ClientAuthenticationMethod == OpenIdConnectClientAuthenticationMethod.ClientSecretPost)\n        {\n            values[\"client_id\"] = settings.ClientId;\n            values[\"client_secret\"] = secret.Value.Reveal();\n        }\n        else\n            headers = new Dictionary<string, string> { [\"Authorization\"] = $\"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($\"{FormUrlEncode(settings.ClientId)}:{FormUrlEncode(secret.Value.Reveal())}\"))}\" };\n\n        var response = await providerHttpClient.PostFormAsync(metadata.TokenEndpoint, values, headers, ProviderResponseKind.Token, cancellationToken);\n        if (!response.IsSuccessStatusCode)\n            throw new OpenIdConnectAuthenticationException(\"The identity provider token exchange failed.\");\n        using var payload = ParseProviderJson(response.Body, \"The identity provider token response was invalid.\");\n        if (!payload.RootElement.TryGetProperty(\"id_token\", out var idToken) || idToken.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(idToken.GetString()))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider token response did not contain an ID token.\");\n        return idToken.GetString()!;\n    }\n\n    private static string FormUrlEncode(string value) => Uri.EscapeDataString(value).Replace(\"%20\", \"+\", StringComparison.Ordinal);\n\n    private async Task<System.Security.Claims.ClaimsPrincipal> ValidateIdTokenAsync(string idToken, OpenIdConnectConnectionSettings settings, ProviderMetadata metadata, CancellationToken cancellationToken)\n    {\n        var signingKeys = metadata.SigningKeys.ValueKind == JsonValueKind.Object\n            ? new JsonWebKeySet(metadata.SigningKeys.GetRawText()).Keys\n            : await GetSigningKeysAsync(metadata.JwksUri, cancellationToken);\n        var validation = await new JsonWebTokenHandler { MapInboundClaims = false }.ValidateTokenAsync(idToken, new()\n        {\n            ValidateIssuer = true,\n            ValidIssuer = metadata.Issuer,\n            ValidateAudience = true,","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs#L161-L197","documentation":"After posting the authorization code (and optional PKCE verifier) to the identity provider's token endpoint, the adapter checks response.IsSuccessStatusCode. Any non-success HTTP response - invalid code, expired code, wrong redirect_uri, bad client credentials, provider outage - results in this exception. The provider's response body is intentionally not surfaced, so you must inspect provider logs for the underlying reason.","triggerScenarios":"ExchangeCodeAsync's PostFormAsync to metadata.TokenEndpoint returns a non-2xx status: authorization code already redeemed or expired, redirect_uri mismatch, client_id/client_secret rejected, token endpoint unreachable/5xx, or unsupported grant.","commonSituations":"Callback base URI changed between authorize and token requests so redirect_uri no longer matches; clock skew making the code appear expired; replaying a code after a browser refresh of the callback; provider requiring client auth method (Basic vs POST body) that doesn't match settings.ClientAuthenticationMethod.","solutions":["Confirm the redirect_uri used in the authorize request exactly matches the one sent to the token endpoint (check Redirects.ExternalCallbackBaseUri and the provider's allowed redirect URIs).","Ensure the authorization code is redeemed exactly once - handle browser refreshes/retries of the callback gracefully.","Verify the client secret and client authentication method (ClientSecretPost vs Basic) match what the provider expects.","Check provider-side logs or temporarily log the token endpoint response status/body to identify the provider's error code (e.g. invalid_grant).","Add retry-with-backoff only for transient 5xx responses; never retry on 4xx."],"exampleFix":"// before: provider rejects due to redirect_uri mismatch\noptions.Value.Redirects.ExternalCallbackBaseUri = null; // falls back differently per request\n// after: pin a stable callback base URI\noptions.Value.Redirects.ExternalCallbackBaseUri = new Uri(\"https://myapp.example.com\");","handlingStrategy":"try-catch","validationCode":"var metadata = await discovery.GetMetadataAsync(settings, ct);\nif (string.IsNullOrEmpty(metadata.TokenEndpoint)) throw new InvalidOperationException(\"Provider discovery returned no token endpoint.\");","typeGuard":null,"tryCatchPattern":"try { await broker.ExchangeCodeAsync(transaction, code, ct); }\ncatch (OpenIdConnectAuthenticationException ex) when (ex.Message == \"The identity provider token exchange failed.\")\n{ logger.LogWarning(ex, \"Token exchange rejected by {Issuer}\", settings.Issuer); return Results.Problem(\"Sign-in failed at the identity provider.\", statusCode: 502); }","preventionTips":["Pin Redirects.ExternalCallbackBaseUri so redirect_uri is identical in authorize and token requests","Never let callback pages be refreshed/replayed without idempotent handling","Match ClientAuthenticationMethod to the provider's expected client auth style","Monitor provider status during key rotations and outages"],"tags":["openid-connect","oauth","token-exchange","http","identity-provider"],"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"}