{"record":{"id":"746b5281c321085d","repo":"elsa-workflows/elsa-core","slug":"the-provider-client-secret-is-unavailable","errorCode":null,"errorMessage":"The provider client secret is unavailable.","messagePattern":"The provider client secret is unavailable\\.","errorType":"exception","errorClass":"OpenIdConnectAuthenticationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs","lineNumber":167,"sourceCode":"        var tokenEndpoint = GetRequiredHttpsUri(root, \"token_endpoint\");\n        return new(issuer, authorizationEndpoint, tokenEndpoint, GetOptionalHttpsUri(root, \"userinfo_endpoint\"), GetOptionalHttpsUri(root, \"end_session_endpoint\"), GetOptionalHttpsUri(root, \"jwks_uri\"), default);\n    }\n\n    private async Task<string> ExchangeCodeAsync(OpenIdConnectConnectionSettings settings, ProviderMetadata metadata, ExternalCallbackContext context, string? verifier, CancellationToken cancellationToken)\n    {\n        if (!TryGetParameter(context.Parameters, \"code\", out var code))\n            throw new OpenIdConnectAuthenticationException(\"The identity provider callback did not contain an authorization code.\");\n\n        var values = new Dictionary<string, string>\n        {\n            [\"grant_type\"] = \"authorization_code\",\n            [\"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","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs#L149-L185","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Configure a client secret on the OpenIdConnect connection so it is stored under the \"clientSecret\" key when the broker transaction is created.","Verify the code path that creates the broker transaction (authorize redirect) actually populates context.Secrets from the connection settings.","If using ClientSecretPost or Basic auth, confirm settings.ClientAuthenticationMethod matches a confidential client and that the secret is present.","Wrap the callback handling in a try-catch for OpenIdConnectAuthenticationException and surface a configuration error to the operator."],"exampleFix":"// before: connection without secret\nnew OpenIdConnectConnection { ClientId = \"my-app\", ClientAuthenticationMethod = OpenIdConnectClientAuthenticationMethod.ClientSecretPost };\n// after: secret configured\nnew OpenIdConnectConnection { ClientId = \"my-app\", ClientSecret = Secret.Create(\"s3cret\"), ClientAuthenticationMethod = OpenIdConnectClientAuthenticationMethod.ClientSecretPost };","handlingStrategy":"validation","validationCode":"if (!transaction.Secrets.TryGetValue(\"clientSecret\", out var _))\n    throw new InvalidOperationException(\"OpenIdConnect connection requires a configured client secret before starting the callback exchange.\");","typeGuard":"bool HasClientSecret(IReadOnlyDictionary<string, Secret> secrets) => secrets.ContainsKey(\"clientSecret\");","tryCatchPattern":"try { await broker.ExchangeCodeAsync(transaction, code, cancellationToken); }\ncatch (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); }","preventionTips":["Always configure ClientSecret on confidential-client OpenIdConnect connections","Validate connection completeness (clientId + clientSecret + issuer) when saving connection settings","Add a startup health check that inspects registered connections for required secrets"],"tags":["openid-connect","oauth","configuration","client-secret","authentication"],"backgroundTag":"missing-credentials","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"}