elsa-workflows/elsa-core · error · OpenIdConnectAuthenticationException

The provider client secret is unavailable.

Error message

The provider client secret is unavailable.

What it means

ExchangeCodeAsync builds the token-endpoint form body to swap the authorization code for tokens, but requires the configured client secret from the broker transaction's Secrets dictionary. When the transaction context does not carry an entry under the key "clientSecret", the adapter cannot authenticate the token request and throws OpenIdConnectAuthenticationException. This is a configuration/transaction-state error thrown before any HTTP call to the identity provider.

Solutions

  1. Configure a client secret on the OpenIdConnect connection so it is stored under the "clientSecret" key when the broker transaction is created.
  2. Verify the code path that creates the broker transaction (authorize redirect) actually populates context.Secrets from the connection settings.
  3. If using ClientSecretPost or Basic auth, confirm settings.ClientAuthenticationMethod matches a confidential client and that the secret is present.
  4. Wrap the callback handling in a try-catch for OpenIdConnectAuthenticationException and surface a configuration error to the operator.

Example fix

// before: connection without secret
new OpenIdConnectConnection { ClientId = "my-app", ClientAuthenticationMethod = OpenIdConnectClientAuthenticationMethod.ClientSecretPost };
// after: secret configured
new OpenIdConnectConnection { ClientId = "my-app", ClientSecret = Secret.Create("s3cret"), ClientAuthenticationMethod = OpenIdConnectClientAuthenticationMethod.ClientSecretPost };
Defensive patterns

Strategy: validation

Validate before calling

if (!transaction.Secrets.TryGetValue("clientSecret", out var _))
    throw new InvalidOperationException("OpenIdConnect connection requires a configured client secret before starting the callback exchange.");

Type guard

bool HasClientSecret(IReadOnlyDictionary<string, Secret> secrets) => secrets.ContainsKey("clientSecret");

Try / catch

try { await broker.ExchangeCodeAsync(transaction, code, cancellationToken); }
catch (OpenIdConnectAuthenticationException ex) when (ex.Message.Contains("client secret")) { logger.LogError(ex, "Connection {Key} is missing its client secret", connectionKey); return Results.Problem("Authentication is misconfigured.", statusCode: 503); }

Prevention

When it happens

Trigger: Calling ExchangeCodeAsync (via the external authentication broker's idToken path) when context.Secrets lacks the "clientSecret" entry - e.g. the transaction was created without storing the connection's client secret, or the connection has no secret configured.

Common situations: OpenIdConnect connection configured without a client secret (public-client-style config used with a confidential token exchange); PKCE-only setup where the deployment assumes code_verifier alone suffices; secrets stored under a different key by a custom connection source; transaction state lost between the authorize redirect and the callback.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/746b5281c321085d. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs:167

        var tokenEndpoint = GetRequiredHttpsUri(root, "token_endpoint");
        return new(issuer, authorizationEndpoint, tokenEndpoint, GetOptionalHttpsUri(root, "userinfo_endpoint"), GetOptionalHttpsUri(root, "end_session_endpoint"), GetOptionalHttpsUri(root, "jwks_uri"), default);
    }

    private async Task<string> ExchangeCodeAsync(OpenIdConnectConnectionSettings settings, ProviderMetadata metadata, ExternalCallbackContext context, string? verifier, CancellationToken cancellationToken)
    {
        if (!TryGetParameter(context.Parameters, "code", out var code))
            throw new OpenIdConnectAuthenticationException("The identity provider callback did not contain an authorization code.");

        var values = new Dictionary<string, string>
        {
            ["grant_type"] = "authorization_code",
            ["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()!;
    }

View on GitHub (pinned to fe9217bdfa)