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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user