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());
}
}