feat(security): LoginThrottle — per-username+IP LDAP-bind failure lockout (arch-review P1)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:01:05 -04:00
parent fb491a5342
commit bd6c310825
4 changed files with 331 additions and 0 deletions
@@ -104,6 +104,26 @@ public class SecurityOptions
/// <see cref="DefaultCookieName"/>. Changing this invalidates existing sessions on next deploy.
/// </summary>
public string CookieName { get; set; } = DefaultCookieName;
/// <summary>
/// Maximum failed LDAP binds tolerated per <see cref="LoginThrottle"/> fixed window for a
/// single <c>{username}|{ip}</c> pair before the pair is locked out. Default: <b>5</b>.
/// Set to <b>0</b> to disable login throttling entirely.
/// </summary>
public int MaxLoginFailuresPerWindow { get; set; } = 5;
/// <summary>
/// The <see cref="LoginThrottle"/> fixed-window length in minutes. Failures older than this
/// (measured from the window's first failure) no longer count toward the lockout threshold.
/// Default: <b>5</b>.
/// </summary>
public int LoginFailureWindowMinutes { get; set; } = 5;
/// <summary>
/// How long, in minutes, a <c>{username}|{ip}</c> pair stays locked out once it reaches
/// <see cref="MaxLoginFailuresPerWindow"/> failures within the window. Default: <b>5</b>.
/// </summary>
public int LoginLockoutMinutes { get; set; } = 5;
}
/// <summary>
@@ -137,6 +157,32 @@ public sealed class SecurityOptionsValidator : Microsoft.Extensions.Options.IVal
$"enforcement is defeated.");
}
// SECURITY: login-throttle thresholds must be non-negative. 0 for
// MaxLoginFailuresPerWindow is the documented "disable throttling" escape hatch;
// a negative value (or a negative window/lockout) is a misconfiguration that would
// make the fixed-window math ill-defined, so it fails fast at boot.
if (options.MaxLoginFailuresPerWindow < 0)
{
return Microsoft.Extensions.Options.ValidateOptionsResult.Fail(
$"{nameof(SecurityOptions.MaxLoginFailuresPerWindow)} " +
$"({options.MaxLoginFailuresPerWindow}) must be non-negative " +
$"(0 disables login throttling).");
}
if (options.LoginFailureWindowMinutes < 0)
{
return Microsoft.Extensions.Options.ValidateOptionsResult.Fail(
$"{nameof(SecurityOptions.LoginFailureWindowMinutes)} " +
$"({options.LoginFailureWindowMinutes}) must be non-negative.");
}
if (options.LoginLockoutMinutes < 0)
{
return Microsoft.Extensions.Options.ValidateOptionsResult.Fail(
$"{nameof(SecurityOptions.LoginLockoutMinutes)} " +
$"({options.LoginLockoutMinutes}) must be non-negative.");
}
return Microsoft.Extensions.Options.ValidateOptionsResult.Success;
}
}