fix(security): resolve Security-001/002/003 — reachable StartTLS path, Secure cookie, JWT signing key validation
This commit is contained in:
@@ -357,6 +357,124 @@ public class RoleMapperTests : IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
#region Code Review Regression Tests
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Security-001 (StartTLS dead code), Security-002 (cookie not
|
||||
/// marked Secure), and Security-003 (JWT signing key length never validated).
|
||||
/// </summary>
|
||||
public class SecurityReviewRegressionTests
|
||||
{
|
||||
// --- Security-003: JWT signing key length validation ---
|
||||
|
||||
private static SecurityOptions JwtOptions(string signingKey) => new()
|
||||
{
|
||||
JwtSigningKey = signingKey,
|
||||
JwtExpiryMinutes = 15,
|
||||
IdleTimeoutMinutes = 30,
|
||||
JwtRefreshThresholdMinutes = 5
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void JwtTokenService_EmptySigningKey_ThrowsAtConstruction()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
new JwtTokenService(Options.Create(JwtOptions("")), NullLogger<JwtTokenService>.Instance));
|
||||
Assert.Contains("JwtSigningKey", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtTokenService_ShortSigningKey_ThrowsAtConstruction()
|
||||
{
|
||||
// 31 bytes — one short of the 256-bit HMAC-SHA256 minimum.
|
||||
var shortKey = new string('k', 31);
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
new JwtTokenService(Options.Create(JwtOptions(shortKey)), NullLogger<JwtTokenService>.Instance));
|
||||
Assert.Contains("32", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtTokenService_AdequateSigningKey_ConstructsSuccessfully()
|
||||
{
|
||||
var key = new string('k', 32);
|
||||
var service = new JwtTokenService(Options.Create(JwtOptions(key)), NullLogger<JwtTokenService>.Instance);
|
||||
var token = service.GenerateToken("User", "user", new[] { "Admin" }, null);
|
||||
Assert.False(string.IsNullOrEmpty(token));
|
||||
}
|
||||
|
||||
// --- Security-002: authentication cookie must be marked Secure ---
|
||||
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_IsMarkedSecureAlways()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddDataProtection();
|
||||
services.AddSecurity();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var cookieOptions = provider
|
||||
.GetRequiredService<IOptionsMonitor<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>>()
|
||||
.Get(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
Assert.Equal(
|
||||
Microsoft.AspNetCore.Http.CookieSecurePolicy.Always,
|
||||
cookieOptions.Cookie.SecurePolicy);
|
||||
Assert.True(cookieOptions.Cookie.HttpOnly);
|
||||
}
|
||||
|
||||
// --- Security-001: StartTLS transport must be reachable ---
|
||||
|
||||
[Fact]
|
||||
public void SecurityOptions_LdapTransport_DefaultsToLdaps()
|
||||
{
|
||||
var options = new SecurityOptions();
|
||||
Assert.Equal(LdapTransport.Ldaps, options.LdapTransport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_StartTlsTransport_AttemptsConnection()
|
||||
{
|
||||
// With StartTLS selected the service must not be blocked by the insecure-LDAP
|
||||
// guard and must reach the connection stage (which fails against a dead host).
|
||||
// This proves the StartTLS path is reachable rather than dead code.
|
||||
var options = new SecurityOptions
|
||||
{
|
||||
LdapServer = "nonexistent.invalid",
|
||||
LdapPort = 389,
|
||||
LdapTransport = LdapTransport.StartTls,
|
||||
LdapSearchBase = "dc=example,dc=com"
|
||||
};
|
||||
var service = new LdapAuthService(Options.Create(options), NullLogger<LdapAuthService>.Instance);
|
||||
|
||||
var result = await service.AuthenticateAsync("user", "password");
|
||||
|
||||
// Connection fails (host invalid) — but crucially NOT with the insecure-LDAP message.
|
||||
Assert.False(result.Success);
|
||||
Assert.DoesNotContain("Insecure LDAP", result.ErrorMessage ?? string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_NoTlsTransport_RejectedWithoutAllowInsecure()
|
||||
{
|
||||
var options = new SecurityOptions
|
||||
{
|
||||
LdapServer = "ldap.example.com",
|
||||
LdapPort = 389,
|
||||
LdapTransport = LdapTransport.None,
|
||||
AllowInsecureLdap = false
|
||||
};
|
||||
var service = new LdapAuthService(Options.Create(options), NullLogger<LdapAuthService>.Instance);
|
||||
|
||||
var result = await service.AuthenticateAsync("user", "password");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Insecure LDAP", result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WP-9: Authorization Policy Tests
|
||||
|
||||
public class AuthorizationPolicyTests
|
||||
|
||||
Reference in New Issue
Block a user