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

  1. 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).
  2. Ensure the authorization code is redeemed exactly once - handle browser refreshes/retries of the callback gracefully.
  3. Verify the client secret and client authentication method (ClientSecretPost vs Basic) match what the provider expects.
  4. Check provider-side logs or temporarily log the token endpoint response status/body to identify the provider's error code (e.g. invalid_grant).
  5. 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

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


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)