137 lines
5.6 KiB
C#
137 lines
5.6 KiB
C#
|
|
using Microsoft.Extensions.Configuration;
|
|
using StellaOps.Doctor.Models;
|
|
using StellaOps.Doctor.Plugins;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.Doctor.Plugins.Security.Checks;
|
|
|
|
/// <summary>
|
|
/// Validates JWT token configuration and security settings.
|
|
/// </summary>
|
|
public sealed class JwtConfigurationCheck : IDoctorCheck
|
|
{
|
|
/// <inheritdoc />
|
|
public string CheckId => "check.security.jwt.config";
|
|
|
|
/// <inheritdoc />
|
|
public string Name => "JWT Configuration";
|
|
|
|
/// <inheritdoc />
|
|
public string Description => "Validates JWT token signing and validation configuration";
|
|
|
|
/// <inheritdoc />
|
|
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> Tags => ["security", "jwt", "authentication"];
|
|
|
|
/// <inheritdoc />
|
|
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
|
|
|
/// <inheritdoc />
|
|
public bool CanRun(DoctorPluginContext context)
|
|
{
|
|
var jwtEnabled = context.Configuration.GetSection("Jwt").Exists()
|
|
|| context.Configuration.GetSection("Authentication:Jwt").Exists();
|
|
return jwtEnabled;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
|
{
|
|
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
|
|
|
|
var issues = new List<string>();
|
|
|
|
var signingKey = context.Configuration.GetValue<string>("Jwt:SigningKey")
|
|
?? context.Configuration.GetValue<string>("Authentication:Jwt:SigningKey");
|
|
var issuer = context.Configuration.GetValue<string>("Jwt:Issuer")
|
|
?? context.Configuration.GetValue<string>("Authentication:Jwt:Issuer");
|
|
var audience = context.Configuration.GetValue<string>("Jwt:Audience")
|
|
?? context.Configuration.GetValue<string>("Authentication:Jwt:Audience");
|
|
var expirationMinutes = context.Configuration.GetValue<int?>("Jwt:ExpirationMinutes")
|
|
?? context.Configuration.GetValue<int?>("Authentication:Jwt:ExpirationMinutes")
|
|
?? 60;
|
|
var algorithm = context.Configuration.GetValue<string>("Jwt:Algorithm")
|
|
?? context.Configuration.GetValue<string>("Authentication:Jwt:Algorithm")
|
|
?? "HS256";
|
|
|
|
if (string.IsNullOrWhiteSpace(signingKey))
|
|
{
|
|
issues.Add("JWT signing key is not configured");
|
|
}
|
|
else if (signingKey.Length < 32)
|
|
{
|
|
issues.Add($"JWT signing key is too short ({signingKey.Length} chars) - minimum 32 characters recommended");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(issuer))
|
|
{
|
|
issues.Add("JWT issuer is not configured");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(audience))
|
|
{
|
|
issues.Add("JWT audience is not configured");
|
|
}
|
|
|
|
if (expirationMinutes > 1440)
|
|
{
|
|
issues.Add($"JWT expiration ({expirationMinutes} minutes) is very long - consider shorter token lifetime");
|
|
}
|
|
|
|
var weakAlgorithms = new[] { "none", "HS256" };
|
|
if (weakAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
if (algorithm.Equals("none", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
issues.Add("JWT algorithm 'none' is insecure - use RS256 or ES256");
|
|
}
|
|
else
|
|
{
|
|
issues.Add($"JWT algorithm '{algorithm}' is acceptable but RS256/ES256 recommended for production");
|
|
}
|
|
}
|
|
|
|
if (issues.Count > 0)
|
|
{
|
|
var severity = issues.Any(i => i.Contains("not configured") || i.Contains("'none'"))
|
|
? DoctorSeverity.Fail
|
|
: DoctorSeverity.Warn;
|
|
|
|
return Task.FromResult(result
|
|
.WithSeverity(severity, $"{issues.Count} JWT configuration issue(s) found")
|
|
.WithEvidence("JWT configuration", e =>
|
|
{
|
|
e.Add("SigningKeyConfigured", (!string.IsNullOrWhiteSpace(signingKey)).ToString());
|
|
e.Add("SigningKeyLength", signingKey?.Length.ToString(CultureInfo.InvariantCulture) ?? "0");
|
|
e.Add("Issuer", issuer ?? "(not set)");
|
|
e.Add("Audience", audience ?? "(not set)");
|
|
e.Add("ExpirationMinutes", expirationMinutes.ToString(CultureInfo.InvariantCulture));
|
|
e.Add("Algorithm", algorithm);
|
|
})
|
|
.WithCauses(issues.ToArray())
|
|
.WithRemediation(r => r
|
|
.AddManualStep(1, "Configure JWT settings", "Set Jwt:SigningKey, Jwt:Issuer, and Jwt:Audience")
|
|
.AddManualStep(2, "Use strong key", "Ensure signing key is at least 32 characters")
|
|
.AddManualStep(3, "Consider RS256", "Use asymmetric algorithms for production"))
|
|
.WithVerification("stella doctor --check check.security.jwt.config")
|
|
.Build());
|
|
}
|
|
|
|
return Task.FromResult(result
|
|
.Pass("JWT configuration is secure")
|
|
.WithEvidence("JWT configuration", e =>
|
|
{
|
|
e.Add("SigningKeyConfigured", "true");
|
|
e.Add("SigningKeyLength", signingKey?.Length.ToString(CultureInfo.InvariantCulture) ?? "0");
|
|
e.Add("Issuer", issuer ?? "(not set)");
|
|
e.Add("Audience", audience ?? "(not set)");
|
|
e.Add("ExpirationMinutes", expirationMinutes.ToString(CultureInfo.InvariantCulture));
|
|
e.Add("Algorithm", algorithm);
|
|
})
|
|
.Build());
|
|
}
|
|
}
|