elsa-core/src/modules/Elsa.Identity/OptionConfigurators/ValidateIdentityTokenOptions.cs
Sipke Schoorstra e9d59bc5b1
[codex] Fail fast on default JWT signing keys (#7496)
* Fail fast on default JWT signing keys

* Address JWT signing key review feedback

* Refine JWT signing key validation feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Reject JWT signing keys with surrounding whitespace

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-05-20 22:30:49 +02:00

74 lines
3.3 KiB
C#

using Elsa.Identity.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Validates the <see cref="IdentityTokenOptions"/>.
/// </summary>
public class ValidateIdentityTokenOptions : IValidateOptions<IdentityTokenOptions>
{
private const int MinimumSigningKeyByteLength = 32;
private const string DemoEnvironmentName = "Demo";
private static readonly HashSet<string> KnownDefaultSigningKeys = new(StringComparer.OrdinalIgnoreCase)
{
"sufficiently-large-secret-signing-key",
"CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
};
private readonly IHostEnvironment? _environment;
/// <summary>
/// Initializes a new instance of the <see cref="ValidateIdentityTokenOptions"/> class.
/// </summary>
public ValidateIdentityTokenOptions()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ValidateIdentityTokenOptions"/> class.
/// </summary>
public ValidateIdentityTokenOptions(IHostEnvironment environment)
{
_environment = environment;
}
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, IdentityTokenOptions options)
{
if (string.IsNullOrWhiteSpace(options.SigningKey))
return ValidateOptionsResult.Fail("SigningKey is required. Configure a secure random JWT signing key through configuration, environment variables, or a secrets manager.");
var signingKey = options.SigningKey.Trim();
if (!string.Equals(signingKey, options.SigningKey, StringComparison.Ordinal))
return ValidateOptionsResult.Fail("SigningKey must not contain leading or trailing whitespace. Configure the exact secure random JWT signing key through configuration, environment variables, or a secrets manager.");
if (KnownDefaultSigningKeys.Contains(signingKey))
{
if (!IsDemoOrDevelopment())
return ValidateOptionsResult.Fail("SigningKey uses a known public default value. Replace it with a secure random JWT signing key through configuration, environment variables, or a secrets manager. Known defaults are allowed only in the Development or Demo environment.");
return ValidateOptionsResult.Success;
}
if (!IsPrintableAscii(signingKey))
return ValidateOptionsResult.Fail("SigningKey contains non-printable or non-ASCII characters. Configure a secure random JWT signing key using only printable ASCII characters (0x20-0x7E) through configuration, environment variables, or a secrets manager.");
if (signingKey.Length < MinimumSigningKeyByteLength)
return ValidateOptionsResult.Fail($"SigningKey must be at least {MinimumSigningKeyByteLength} ASCII characters long. Configure a secure random JWT signing key through configuration, environment variables, or a secrets manager.");
return ValidateOptionsResult.Success;
}
private bool IsDemoOrDevelopment()
{
return _environment is not null && (_environment.IsDevelopment() || string.Equals(_environment.EnvironmentName, DemoEnvironmentName, StringComparison.OrdinalIgnoreCase));
}
private static bool IsPrintableAscii(string value) => value.All(x => x is >= ' ' and <= '~');
}