elsa-workflows/elsa-core · error · OpenIdConnectAuthenticationException
The identity provider token exchange failed.
Error message
The identity provider token exchange failed.
What it means
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.
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.
Example fix
// before: provider rejects due to redirect_uri mismatch
options.Value.Redirects.ExternalCallbackBaseUri = null; // falls back differently per request
// after: pin a stable callback base URI
options.Value.Redirects.ExternalCallbackBaseUri = new Uri("https://myapp.example.com"); Defensive patterns
Strategy: try-catch
Validate before calling
var metadata = await discovery.GetMetadataAsync(settings, ct);
if (string.IsNullOrEmpty(metadata.TokenEndpoint)) throw new InvalidOperationException("Provider discovery returned no token endpoint."); Try / catch
try { await broker.ExchangeCodeAsync(transaction, code, ct); }
catch (OpenIdConnectAuthenticationException ex) when (ex.Message == "The identity provider token exchange failed.")
{ logger.LogWarning(ex, "Token exchange rejected by {Issuer}", settings.Issuer); return Results.Problem("Sign-in failed at the identity provider.", statusCode: 502); } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- The identity provider token response did not contain an ID…
- The identity provider signing keys could not be resolved.
- The identity provider rejected the authentication request.
- The identity provider metadata could not be resolved.
- The identity provider callback did not contain an…
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/2d4b34f8d3c32389.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs:179
["code"] = code,
["redirect_uri"] = GetCallbackUri(context.Connection, context.Transaction.Purpose).AbsoluteUri
};
if (verifier is not null)
values["code_verifier"] = verifier;
if (!context.Secrets.TryGetValue("clientSecret", out var secret))
throw new OpenIdConnectAuthenticationException("The provider client secret is unavailable.");
IReadOnlyDictionary<string, string>? headers = null;
if (settings.ClientAuthenticationMethod == OpenIdConnectClientAuthenticationMethod.ClientSecretPost)
{
values["client_id"] = settings.ClientId;
values["client_secret"] = secret.Value.Reveal();
}
else
headers = new Dictionary<string, string> { ["Authorization"] = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{FormUrlEncode(settings.ClientId)}:{FormUrlEncode(secret.Value.Reveal())}"))}" };
var response = await providerHttpClient.PostFormAsync(metadata.TokenEndpoint, values, headers, ProviderResponseKind.Token, cancellationToken);
if (!response.IsSuccessStatusCode)
throw new OpenIdConnectAuthenticationException("The identity provider token exchange failed.");
using var payload = ParseProviderJson(response.Body, "The identity provider token response was invalid.");
if (!payload.RootElement.TryGetProperty("id_token", out var idToken) || idToken.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(idToken.GetString()))
throw new OpenIdConnectAuthenticationException("The identity provider token response did not contain an ID token.");
return idToken.GetString()!;
}
private static string FormUrlEncode(string value) => Uri.EscapeDataString(value).Replace("%20", "+", StringComparison.Ordinal);
private async Task<System.Security.Claims.ClaimsPrincipal> ValidateIdTokenAsync(string idToken, OpenIdConnectConnectionSettings settings, ProviderMetadata metadata, CancellationToken cancellationToken)
{
var signingKeys = metadata.SigningKeys.ValueKind == JsonValueKind.Object
? new JsonWebKeySet(metadata.SigningKeys.GetRawText()).Keys
: await GetSigningKeysAsync(metadata.JwksUri, cancellationToken);
var validation = await new JsonWebTokenHandler { MapInboundClaims = false }.ValidateTokenAsync(idToken, new()
{
ValidateIssuer = true,
ValidIssuer = metadata.Issuer,
ValidateAudience = true,View on GitHub (pinned to fe9217bdfa)