using Microsoft.Extensions.Configuration; using StellaOps.Doctor.Models; using StellaOps.Doctor.Plugins; using System.Globalization; namespace StellaOps.Doctor.Plugins.Security.Checks; /// /// Validates JWT token configuration and security settings. /// public sealed class JwtConfigurationCheck : IDoctorCheck { /// public string CheckId => "check.security.jwt.config"; /// public string Name => "JWT Configuration"; /// public string Description => "Validates JWT token signing and validation configuration"; /// public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail; /// public IReadOnlyList Tags => ["security", "jwt", "authentication"]; /// public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100); /// public bool CanRun(DoctorPluginContext context) { var jwtEnabled = context.Configuration.GetSection("Jwt").Exists() || context.Configuration.GetSection("Authentication:Jwt").Exists(); return jwtEnabled; } /// public Task RunAsync(DoctorPluginContext context, CancellationToken ct) { var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString()); var issues = new List(); var signingKey = context.Configuration.GetValue("Jwt:SigningKey") ?? context.Configuration.GetValue("Authentication:Jwt:SigningKey"); var issuer = context.Configuration.GetValue("Jwt:Issuer") ?? context.Configuration.GetValue("Authentication:Jwt:Issuer"); var audience = context.Configuration.GetValue("Jwt:Audience") ?? context.Configuration.GetValue("Authentication:Jwt:Audience"); var expirationMinutes = context.Configuration.GetValue("Jwt:ExpirationMinutes") ?? context.Configuration.GetValue("Authentication:Jwt:ExpirationMinutes") ?? 60; var algorithm = context.Configuration.GetValue("Jwt:Algorithm") ?? context.Configuration.GetValue("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()); } }