elsa-workflows/elsa-core · error · OpenIdConnectAuthenticationException
The identity provider callback issuer did not match the…
Error message
The identity provider callback issuer did not match the initiated request.
What it means
When the adapter state (from the protected transaction payload) exists, AuthenticateCallbackAsync compares adapterState.Issuer with the issuer from the resolved provider metadata using an ordinal comparison. A mismatch means the discovery document at callback time declares a different issuer than the one recorded when the flow started, so the callback may be from a different or spoofed provider.
Solutions
- Confirm the connection's DiscoveryUrl/Issuer settings were not edited between initiation and callback; restart the login flow after any change.
- Check the provider's discovery document issuer matches the configured issuer exactly (case and trailing slash matter — comparison is ordinal).
- In multi-tenant/realm setups, make sure the realm-specific discovery URL is used consistently for the whole flow.
Defensive patterns
Strategy: validation
Validate before calling
var meta = await ResolveMetadataAsync(settings); if (!string.Equals(savedIssuer, meta.Issuer, StringComparison.Ordinal)) logger.LogError("Issuer drift: {Saved} vs {Current}", savedIssuer, meta.Issuer); Try / catch
try { await adapter.AuthenticateCallbackAsync(context); } catch (OpenIdConnectAuthenticationException ex) when (ex.Message.Contains("issuer")) { logger.LogError(ex, "OIDC issuer mismatch"); throw; } Prevention
- Pin the issuer/discovery configuration per environment and avoid changing it mid-flight.
- Compare issuer strings with exact ordinal semantics (watch trailing slashes).
- In multi-tenant setups, keep realm-specific discovery URLs consistent per connection.
When it happens
Trigger: AuthenticateCallbackAsync where ReadAdapterState returns a non-null state whose Issuer differs from metadata.Issuer resolved via ResolveMetadataAsync.
Common situations: Discovery URL changed to a different provider or realm mid-flow; the provider's metadata issuer changed (e.g. trailing slash differences or realm rename); load balancer routes callback to an environment configured for another tenant.
Related errors
- The identity provider rejected the authentication request.
- The identity provider callback could not be correlated.
- The identity provider nonce did not match the initiated…
- The identity provider response did not contain a subject.
- 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/a58e9d53d8607617.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs:91
["code_challenge_method"] = "S256"
};
return new(WithQuery(metadata.AuthorizationEndpoint, query), state);
}
public async ValueTask<ExternalAuthenticationResult> AuthenticateCallbackAsync(ExternalCallbackContext context, CancellationToken cancellationToken = default)
{
if (TryGetParameter(context.Parameters, "error", out _))
throw new OpenIdConnectAuthenticationException("The identity provider rejected the authentication request.");
if (!TryGetParameter(context.Parameters, "state", out var state) || !FixedTimeEquals(state, context.CorrelationState))
throw new OpenIdConnectAuthenticationException("The identity provider callback could not be correlated.");
var settings = await GetSettingsAsync(context.Connection.Connection.AdapterSettings, cancellationToken);
var metadata = await ResolveMetadataAsync(settings, cancellationToken);
var adapterState = ReadAdapterState(context.Transaction.ProtectedPayload);
if (adapterState is not null && !string.Equals(adapterState.Issuer, metadata.Issuer, StringComparison.Ordinal))
throw new OpenIdConnectAuthenticationException("The identity provider callback issuer did not match the initiated request.");
var idToken = await ExchangeCodeAsync(settings, metadata, context, adapterState?.CodeVerifier, cancellationToken);
var principal = await ValidateIdTokenAsync(idToken, settings, metadata, cancellationToken);
var nonce = principal.FindFirst("nonce")?.Value;
var expectedNonce = context.Transaction.ProviderNonce ?? adapterState?.Nonce;
if (string.IsNullOrWhiteSpace(expectedNonce) || !FixedTimeEquals(nonce, expectedNonce))
throw new OpenIdConnectAuthenticationException("The identity provider nonce did not match the initiated request.");
var issuer = principal.FindFirst("iss")?.Value ?? metadata.Issuer;
var subject = principal.FindFirst("sub")?.Value;
if (string.IsNullOrWhiteSpace(subject))
throw new OpenIdConnectAuthenticationException("The identity provider response did not contain a subject.");
var projectedClaims = ProjectClaims(principal, context.Connection.Connection.ClaimProjection);
return new(new(issuer, subject, projectedClaims), projectedClaims, [], new(idToken));
}
public async ValueTask<ConnectionTestResult> TestAsync(ConnectionTestContext context, CancellationToken cancellationToken = default)View on GitHub (pinned to fe9217bdfa)