fix(security): resolve Security-001/002/003 — reachable StartTLS path, Secure cookie, JWT signing key validation

This commit is contained in:
Joseph Doherty
2026-05-16 19:47:17 -04:00
parent 393172f169
commit 0d9363766d
7 changed files with 222 additions and 11 deletions

View File

@@ -22,6 +22,19 @@ public class JwtTokenService
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// Fail fast: a missing or short signing key produces trivially forgeable tokens.
// HMAC-SHA256 requires a key of at least 256 bits (32 bytes).
var keyByteLength = string.IsNullOrEmpty(_options.JwtSigningKey)
? 0
: Encoding.UTF8.GetByteCount(_options.JwtSigningKey);
if (keyByteLength < SecurityOptions.MinJwtSigningKeyBytes)
{
throw new InvalidOperationException(
$"SecurityOptions.JwtSigningKey must be at least {SecurityOptions.MinJwtSigningKeyBytes} bytes " +
$"(256 bits) for HMAC-SHA256; the configured key is {keyByteLength} byte(s). " +
"Configure a strong signing key before starting the service.");
}
}
public string GenerateToken(

View File

@@ -24,7 +24,7 @@ public class LdapAuthService
return new LdapAuthResult(false, null, null, null, "Password is required.");
// Enforce TLS unless explicitly allowed for dev/test
if (!_options.LdapUseTls && !_options.AllowInsecureLdap)
if (_options.LdapTransport == LdapTransport.None && !_options.AllowInsecureLdap)
{
return new LdapAuthResult(false, null, null, null,
"Insecure LDAP connections are not allowed. Enable TLS or set AllowInsecureLdap for dev/test.");
@@ -34,16 +34,24 @@ public class LdapAuthService
{
using var connection = new LdapConnection();
if (_options.LdapUseTls)
// LDAPS: TLS negotiated at connection time. StartTLS: connect plaintext,
// then upgrade the session before any credentials are sent.
if (_options.LdapTransport == LdapTransport.Ldaps)
{
connection.SecureSocketLayer = true;
}
await Task.Run(() => connection.Connect(_options.LdapServer, _options.LdapPort), ct);
if (_options.LdapUseTls && !connection.SecureSocketLayer)
if (_options.LdapTransport == LdapTransport.StartTls)
{
await Task.Run(() => connection.StartTls(), ct);
if (!connection.Tls)
{
return new LdapAuthResult(false, null, null, null,
"StartTLS upgrade did not produce an encrypted session.");
}
}
// Resolve the user's actual DN, then bind with their credentials

View File

@@ -0,0 +1,26 @@
namespace ScadaLink.Security;
/// <summary>
/// Transport security mode for the LDAP connection. The design requires either
/// LDAPS or StartTLS in production; <see cref="None"/> is for dev/test only and
/// must be paired with <see cref="SecurityOptions.AllowInsecureLdap"/>.
/// </summary>
public enum LdapTransport
{
/// <summary>
/// LDAPS — TLS negotiated at connection time (typically port 636).
/// </summary>
Ldaps,
/// <summary>
/// StartTLS — connect in plaintext (typically port 389), then upgrade the
/// session to TLS before binding.
/// </summary>
StartTls,
/// <summary>
/// No transport security. Dev/test only — requires
/// <see cref="SecurityOptions.AllowInsecureLdap"/> to be true.
/// </summary>
None
}

View File

@@ -4,7 +4,24 @@ public class SecurityOptions
{
public string LdapServer { get; set; } = string.Empty;
public int LdapPort { get; set; } = 389;
public bool LdapUseTls { get; set; } = true;
/// <summary>
/// Transport security mode for the LDAP connection. Defaults to LDAPS.
/// Use <see cref="LdapTransport.StartTls"/> to connect on the plaintext port
/// and upgrade the session before binding.
/// </summary>
public LdapTransport LdapTransport { get; set; } = LdapTransport.Ldaps;
/// <summary>
/// True when the configured transport provides encryption (LDAPS or StartTLS).
/// Retained for backward compatibility: assigning a value maps onto
/// <see cref="LdapTransport"/> (true =&gt; LDAPS, false =&gt; None).
/// </summary>
public bool LdapUseTls
{
get => LdapTransport != LdapTransport.None;
set => LdapTransport = value ? LdapTransport.Ldaps : LdapTransport.None;
}
/// <summary>
/// Allow insecure (non-TLS) LDAP connections. ONLY for dev/test with GLAuth.
@@ -39,7 +56,16 @@ public class SecurityOptions
/// </summary>
public string LdapGroupAttribute { get; set; } = "memberOf";
/// <summary>
/// Symmetric HMAC-SHA256 signing key for cookie-embedded JWTs. Must be at least
/// 32 bytes (256 bits) — validated at <see cref="JwtTokenService"/> construction.
/// </summary>
public string JwtSigningKey { get; set; } = string.Empty;
/// <summary>
/// Minimum signing-key length in bytes required for HMAC-SHA256 (256 bits).
/// </summary>
public const int MinJwtSigningKeyBytes = 32;
public int JwtExpiryMinutes { get; set; } = 15;
public int IdleTimeoutMinutes { get; set; } = 30;

View File

@@ -20,6 +20,9 @@ public static class ServiceCollectionExtensions
options.Cookie.Name = "ScadaLink.Auth";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
// The cookie carries the embedded JWT (a bearer credential); never
// transmit it over plain HTTP. Design: "HttpOnly and Secure (requires HTTPS)".
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
});
services.AddScadaLinkAuthorization();