fix(security): resolve Security-001/002/003 — reachable StartTLS path, Secure cookie, JWT signing key validation
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
26
src/ScadaLink.Security/LdapTransport.cs
Normal file
26
src/ScadaLink.Security/LdapTransport.cs
Normal 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
|
||||
}
|
||||
@@ -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 => LDAPS, false => 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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user