feat(security): wire LoginThrottle into management Basic-Auth and /auth/login|token — password-spray guard (arch-review P1/UA5)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -104,15 +104,30 @@ public static class ManagementAuthenticator
|
||||
new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
// 1a. Login throttle: refuse a bind while this username+IP key is locked out
|
||||
// (fixed-window password-spray guard, arch-review P1/UA5). Consulted BELOW the
|
||||
// DisableLogin bypass — a login-disabled deployment never binds, so it never throttles.
|
||||
var throttle = context.RequestServices.GetRequiredService<LoginThrottle>();
|
||||
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
|
||||
if (throttle.IsLockedOut(username, remoteIp!))
|
||||
{
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = "Too many failed authentication attempts. Try again later.", code = "AUTH_THROTTLED" },
|
||||
statusCode: 429));
|
||||
}
|
||||
|
||||
// 2. LDAP authentication
|
||||
var ldapAuth = context.RequestServices.GetRequiredService<ILdapAuthService>();
|
||||
var authResult = await ldapAuth.AuthenticateAsync(username, password, context.RequestAborted);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throttle.RecordFailure(username, remoteIp!);
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = LdapAuthFailureMessages.ToMessage(authResult.Failure), code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
throttle.RecordSuccess(username, remoteIp!);
|
||||
|
||||
// 3. Role resolution
|
||||
var roleMapper = context.RequestServices.GetRequiredService<RoleMapper>();
|
||||
var mappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups, context.RequestAborted);
|
||||
|
||||
Reference in New Issue
Block a user