Auth: Add setting to disable username based brute force login protection (#109152)

* Add setting to disable username based brute force login protection

* Use new DisableUsernameLoginProtection setting in tests where appropriate

* Update documentation for other brute force directives

* Avoid unecessary database calls

* Add test cases for username and IP protection settings
This commit is contained in:
Théo Brigitte
2025-08-06 14:08:49 +00:00
committed by GitHub
parent f82ea23061
commit 5c50fc6385
6 changed files with 156 additions and 48 deletions
+3
View File
@@ -374,6 +374,9 @@ disable_brute_force_login_protection = false
# max number of failed login attempts before user gets locked
brute_force_login_protection_max_attempts = 5
# disable protection against brute force login attempts by username
disable_username_login_protection = false
# disable protection against brute force login attempts by IP address
disable_ip_address_login_protection = true
+3
View File
@@ -371,6 +371,9 @@
# max number of failed login attempts before user gets locked
;brute_force_login_protection_max_attempts = 5
# disable protection against brute force login attempts by username
;disable_username_login_protection = false
# disable protection against brute force login attempts by IP address
; disable_ip_address_login_protection = true
@@ -703,13 +703,17 @@ PostgreSQL, MySQL, and MSSQL data sources don't use the proxy and are not affect
Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout).
Default is `false`.
An existing user's account is unable to login for five minutes if all login attempts are spent within a 5 minute window.
Login is blocked for five minutes if all login attempts are spent within a 5 minute window.
#### `brute_force_login_protection_max_attempts`
Configure how many login attempts a user can have within a five minute window before their account is locked.
Configure how many login attempts can be made within a five minute window before being blocked.
Default is `5`.
#### `disable_username_login_protection`
Set to `true` to disable [brute force login protection by username](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `false`. User will be unable to login for 5 minutes if all login attempts are spent within a 5 minute window.
#### `disable_ip_address_login_protection`
Set to `true` to disable [brute force login protection by IP address](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `true`. Anyone from the IP address will be unable to login for 5 minutes if all login attempts are spent within a 5 minute window.
@@ -47,7 +47,7 @@ func (s *Service) Run(ctx context.Context) error {
}
func (s *Service) Add(ctx context.Context, username, IPAddress string) error {
if s.cfg.DisableBruteForceLoginProtection {
if s.cfg.DisableBruteForceLoginProtection || (s.cfg.DisableUsernameLoginProtection && s.cfg.DisableIPAddressLoginProtection) {
return nil
}
@@ -63,7 +63,7 @@ func (s *Service) Reset(ctx context.Context, username string) error {
}
func (s *Service) Validate(ctx context.Context, username string) (bool, error) {
if s.cfg.DisableBruteForceLoginProtection {
if s.cfg.DisableBruteForceLoginProtection || s.cfg.DisableUsernameLoginProtection {
return true, nil
}
@@ -85,7 +85,7 @@ func (s *Service) Validate(ctx context.Context, username string) (bool, error) {
}
func (s *Service) ValidateIPAddress(ctx context.Context, IPAddress string) (bool, error) {
if s.cfg.DisableIPAddressLoginProtection {
if s.cfg.DisableBruteForceLoginProtection || s.cfg.DisableIPAddressLoginProtection {
return true, nil
}
@@ -18,11 +18,12 @@ func TestService_Validate(t *testing.T) {
const maxInvalidLoginAttempts = 5
testCases := []struct {
name string
loginAttempts int64
disabled bool
expected bool
expectedErr error
name string
loginAttempts int64
disableProtection bool
disableUsernameProtection bool
expected bool
expectedErr error
}{
{
name: "Should be valid when brute force protection enabled and user login attempt count is less than max",
@@ -44,25 +45,70 @@ func TestService_Validate(t *testing.T) {
},
{
name: "Should be valid when brute force protection disabled and user login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and user login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force protection disabled and user login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and user login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force protection disabled and user login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and user login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force username protection disabled and user login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force username protection disabled and user login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force username protection disabled and user login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and user login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableProtection: true,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and user login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableProtection: true,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and user login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableProtection: true,
disableUsernameProtection: true,
expected: true,
expectedErr: nil,
},
}
@@ -70,7 +116,8 @@ func TestService_Validate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts
cfg.DisableBruteForceLoginProtection = tt.disabled
cfg.DisableBruteForceLoginProtection = tt.disableProtection
cfg.DisableUsernameLoginProtection = tt.disableUsernameProtection
service := &Service{
store: fakeStore{
ExpectedCount: tt.loginAttempts,
@@ -93,6 +140,7 @@ func TestIntegrationUserLoginAttempts(t *testing.T) {
ctx := context.Background()
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = false
cfg.DisableUsernameLoginProtection = false
cfg.BruteForceLoginProtectionMaxAttempts = 5
db := db.InitTestDB(t)
service := ProvideService(db, cfg, nil)
@@ -119,11 +167,12 @@ func TestService_ValidateIPAddress(t *testing.T) {
const maxInvalidLoginAttempts = 5
testCases := []struct {
name string
loginAttempts int64
disabled bool
expected bool
expectedErr error
name string
loginAttempts int64
disableProtection bool
disableIPProtection bool
expected bool
expectedErr error
}{
{
name: "Should be valid when brute force protection enabled and IP address login attempt count is less than max",
@@ -143,27 +192,71 @@ func TestService_ValidateIPAddress(t *testing.T) {
expected: false,
expectedErr: nil,
},
{
name: "Should be valid when brute force protection disabled and IP address login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and IP address login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force protection disabled and IP address login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and IP address login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when brute force protection disabled and IP address login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disabled: true,
expected: true,
expectedErr: nil,
name: "Should be valid when brute force protection disabled and IP address login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when IP brute force protection disabled and IP address login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when IP brute force protection disabled and IP address login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when IP brute force protection disabled and IP address login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and IP address login attempt count is less than max",
loginAttempts: maxInvalidLoginAttempts - 1,
disableProtection: true,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and IP address login attempt count equals max",
loginAttempts: maxInvalidLoginAttempts,
disableProtection: true,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
{
name: "Should be valid when both brute force protections disabled and IP address login attempt count is greater than max",
loginAttempts: maxInvalidLoginAttempts + 1,
disableProtection: true,
disableIPProtection: true,
expected: true,
expectedErr: nil,
},
}
@@ -171,7 +264,8 @@ func TestService_ValidateIPAddress(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts
cfg.DisableIPAddressLoginProtection = tt.disabled
cfg.DisableBruteForceLoginProtection = tt.disableProtection
cfg.DisableIPAddressLoginProtection = tt.disableIPProtection
service := &Service{
store: fakeStore{
ExpectedCount: tt.loginAttempts,
@@ -193,6 +287,7 @@ func TestIntegrationIPLoginAttempts(t *testing.T) {
}
ctx := context.Background()
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = false
cfg.DisableIPAddressLoginProtection = false
cfg.BruteForceLoginProtectionMaxAttempts = 3
db := db.InitTestDB(t)
@@ -224,6 +319,7 @@ func TestIntegrationIPv6AddressSupport(t *testing.T) {
ctx := context.Background()
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = false
cfg.DisableIPAddressLoginProtection = false
cfg.BruteForceLoginProtectionMaxAttempts = 5
// Use controlled time like other tests to avoid timestamp conversion issues
+2
View File
@@ -157,6 +157,7 @@ type Cfg struct {
DisableInitAdminCreation bool
DisableBruteForceLoginProtection bool
BruteForceLoginProtectionMaxAttempts int64
DisableUsernameLoginProtection bool
DisableIPAddressLoginProtection bool
CookieSecure bool
CookieSameSiteDisabled bool
@@ -1587,6 +1588,7 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
cfg.BruteForceLoginProtectionMaxAttempts = security.Key("brute_force_login_protection_max_attempts").MustInt64(5)
cfg.DisableUsernameLoginProtection = security.Key("disable_username_login_protection").MustBool(false)
cfg.DisableIPAddressLoginProtection = security.Key("disable_ip_address_login_protection").MustBool(true)
// Ensure at least one login attempt can be performed.