Add option to ban user by ip instead of username

customizations
Jonas Letzbor 2023-06-23 22:07:23 +02:00
parent c13e0e12ea
commit e9a383be0c
Signed by: RPJosh
GPG Key ID: 46D72F589702E55A
13 changed files with 72 additions and 32 deletions

View File

@ -58,6 +58,15 @@ Wenn die Konfiguration geändert wurde, müssen die Keys zur Validierung wieder
go run ./cmd/authelia-gen code keys
```
## Mocks abgeändert
Wenn interfaces von den Mocks geändert werden, muss folgendes wieder ausgeführt werden:
```
export PATH=$PATH:$(go env GOPATH)/bin
go generate ./...
```
## Bauen
Um ein Docker Image für authelia zu bauen, müssen die folgenden Befehle ausgeführt werden.

View File

@ -85,6 +85,8 @@ server:
# Even if TLS is configured in the server setting (under server.tls), the grcp server won't use TLS
disableTLS: false
# By default the ban is issued for the user. With this options the IP instead of the user will be banned
use_ip_for_ban: true
## Server headers configuration/customization.
headers:

View File

@ -265,6 +265,7 @@ var Keys = []string{
"server.asset_path",
"server.disable_healthcheck",
"server.disable_autho_https_redirect",
"server.use_ip_for_ban",
"server.tls.certificate",
"server.tls.key",
"server.tls.client_certificates",

View File

@ -11,6 +11,7 @@ type ServerConfiguration struct {
AssetPath string `koanf:"asset_path"`
DisableHealthcheck bool `koanf:"disable_healthcheck"`
DisableAutoHttpsRedirect bool `koanf:"disable_autho_https_redirect"`
UseIPInsteadOfUserForBan bool `koanf:"use_ip_for_ban"`
TLS ServerTLS `koanf:"tls"`
Headers ServerHeaders `koanf:"headers"`

View File

@ -33,7 +33,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
return
}
if bannedUntil, err := ctx.Providers.Regulator.Regulate(ctx, bodyJSON.Username); err != nil {
if bannedUntil, err := ctx.Providers.Regulator.Regulate(ctx, bodyJSON.Username, ctx.RemoteIP().String()); err != nil {
if errors.Is(err, regulation.ErrUserIsBanned) {
_ = markAuthenticationAttempt(ctx, false, &bannedUntil, bodyJSON.Username, regulation.AuthType1FA, nil)

View File

@ -277,13 +277,18 @@ func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, ba
if successful {
ctx.Logger.Debugf("Successful %s authentication attempt made by user '%s'", authType, username)
} else {
reasonPhrase := "by user '" + username + "'"
if ctx.Configuration.Server.UseIPInsteadOfUserForBan {
reasonPhrase = fmt.Sprintf("by ip %q (user %q)", ctx.RemoteIP().String(), username)
}
switch {
case errAuth != nil:
ctx.Logger.Errorf("Unsuccessful %s authentication attempt by user '%s': %+v", authType, username, errAuth)
ctx.Logger.Errorf("Unsuccessful %s authentication attempt %s: %+v", authType, reasonPhrase, errAuth)
case bannedUntil != nil:
ctx.Logger.Errorf("Unsuccessful %s authentication attempt by user '%s' and they are banned until %s", authType, username, bannedUntil)
ctx.Logger.Errorf("Unsuccessful %s authentication attempt %s and they are banned until %s", authType, reasonPhrase, bannedUntil)
default:
ctx.Logger.Errorf("Unsuccessful %s authentication attempt by user '%s'", authType, username)
ctx.Logger.Errorf("Unsuccessful %s authentication attempt %s", authType, reasonPhrase)
}
}

View File

@ -210,18 +210,18 @@ func (mr *MockStorageMockRecorder) FindIdentityVerification(arg0, arg1 interface
}
// LoadAuthenticationLogs mocks base method.
func (m *MockStorage) LoadAuthenticationLogs(arg0 context.Context, arg1 string, arg2 time.Time, arg3, arg4 int) ([]model.AuthenticationAttempt, error) {
func (m *MockStorage) LoadAuthenticationLogs(arg0 context.Context, arg1, arg2 string, arg3 time.Time, arg4, arg5 int) ([]model.AuthenticationAttempt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadAuthenticationLogs", arg0, arg1, arg2, arg3, arg4)
ret := m.ctrl.Call(m, "LoadAuthenticationLogs", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].([]model.AuthenticationAttempt)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadAuthenticationLogs indicates an expected call of LoadAuthenticationLogs.
func (mr *MockStorageMockRecorder) LoadAuthenticationLogs(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) LoadAuthenticationLogs(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAuthenticationLogs", reflect.TypeOf((*MockStorage)(nil).LoadAuthenticationLogs), arg0, arg1, arg2, arg3, arg4)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAuthenticationLogs", reflect.TypeOf((*MockStorage)(nil).LoadAuthenticationLogs), arg0, arg1, arg2, arg3, arg4, arg5)
}
// LoadOAuth2BlacklistedJTI mocks base method.

View File

@ -40,13 +40,13 @@ func (r *Regulator) Mark(ctx Context, successful, banned bool, username, request
// Regulate the authentication attempts for a given user.
// This method returns ErrUserIsBanned if the user is banned along with the time until when the user is banned.
func (r *Regulator) Regulate(ctx context.Context, username string) (time.Time, error) {
func (r *Regulator) Regulate(ctx context.Context, username string, remoteIp string) (time.Time, error) {
// If there is regulation configuration, no regulation applies.
if !r.enabled {
return time.Time{}, nil
}
attempts, err := r.store.LoadAuthenticationLogs(ctx, username, r.clock.Now().Add(-r.config.BanTime), 10, 0)
attempts, err := r.store.LoadAuthenticationLogs(ctx, username, remoteIp, r.clock.Now().Add(-r.config.BanTime), 10, 0)
if err != nil {
return time.Time{}, nil
}

View File

@ -23,6 +23,8 @@ type RegulatorSuite struct {
mock *mocks.MockAutheliaCtx
}
// @TODO
// Extend this test for IP ban :)
func (s *RegulatorSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
s.mock.Ctx.Configuration.Regulation = schema.RegulationConfiguration{
@ -58,9 +60,9 @@ func (s *RegulatorSuite) TestShouldMark() {
func (s *RegulatorSuite) TestShouldHandleRegulateError() {
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
s.mock.StorageMock.EXPECT().LoadAuthenticationLogs(s.mock.Ctx, "john", s.mock.Clock.Now().Add(-s.mock.Ctx.Configuration.Regulation.BanTime), 10, 0).Return(nil, fmt.Errorf("failed"))
s.mock.StorageMock.EXPECT().LoadAuthenticationLogs(s.mock.Ctx, "john", "127.0.0.1", s.mock.Clock.Now().Add(-s.mock.Ctx.Configuration.Regulation.BanTime), 10, 0).Return(nil, fmt.Errorf("failed"))
until, err := regulator.Regulate(s.mock.Ctx, "john")
until, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
s.NoError(err)
s.Equal(time.Time{}, until)
@ -76,12 +78,12 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
}
@ -107,12 +109,12 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenFailedAuthenticationNotInFindTime
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
}
@ -143,12 +145,12 @@ func (s *RegulatorSuite) TestShouldBanUserIfLatestAttemptsAreWithinFinTime() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
}
@ -176,12 +178,12 @@ func (s *RegulatorSuite) TestShouldCheckUserIsStillBanned() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
}
@ -200,12 +202,12 @@ func (s *RegulatorSuite) TestShouldCheckUserIsNotYetBanned() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
}
@ -232,12 +234,12 @@ func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
}
@ -268,12 +270,12 @@ func (s *RegulatorSuite) TestShouldCheckRegulationHasBeenResetOnSuccessfulAttemp
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
}
@ -303,7 +305,7 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
}
s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), "127.0.0.1", gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil)
// Check Disabled Functionality.
@ -314,7 +316,7 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
}
regulator := regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.mock.Ctx, "john")
_, err := regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.NoError(s.T(), err)
// Check Enabled Functionality.
@ -325,6 +327,6 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
}
regulator = regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock)
_, err = regulator.Regulate(s.mock.Ctx, "john")
_, err = regulator.Regulate(s.mock.Ctx, "john", "127.0.0.1")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
}

View File

@ -90,5 +90,5 @@ type Provider interface {
// RegulatorProvider is an interface providing storage capabilities for persisting any kind of data related to the regulator.
type RegulatorProvider interface {
AppendAuthenticationLog(ctx context.Context, attempt model.AuthenticationAttempt) (err error)
LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []model.AuthenticationAttempt, err error)
LoadAuthenticationLogs(ctx context.Context, username string, ip string, fromDate time.Time, limit, page int) (attempts []model.AuthenticationAttempt, err error)
}

View File

@ -33,6 +33,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs),
sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs),
sqlSelectAuthenticationAttemptyByIP: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByIP, tableAuthenticationLogs),
sqlInsertIdentityVerification: fmt.Sprintf(queryFmtInsertIdentityVerification, tableIdentityVerification),
sqlConsumeIdentityVerification: fmt.Sprintf(queryFmtConsumeIdentityVerification, tableIdentityVerification),
@ -149,6 +150,7 @@ type SQLProvider struct {
// Table: authentication_logs.
sqlInsertAuthenticationAttempt string
sqlSelectAuthenticationAttemptsByUsername string
sqlSelectAuthenticationAttemptyByIP string
// Table: identity_verification.
sqlInsertIdentityVerification string
@ -1021,10 +1023,18 @@ func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt model
}
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []model.AuthenticationAttempt, err error) {
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, ip string, fromDate time.Time, limit, page int) (attempts []model.AuthenticationAttempt, err error) {
attempts = make([]model.AuthenticationAttempt, 0, limit)
if err = p.db.SelectContext(ctx, &attempts, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page); err != nil {
// Dynmaic values based on ip / username ban
query := p.sqlSelectAuthenticationAttemptsByUsername
placeholder := username
if p.config.Server.UseIPInsteadOfUserForBan {
query = p.sqlSelectAuthenticationAttemptyByIP
placeholder = ip
}
if err = p.db.SelectContext(ctx, &attempts, query, fromDate, placeholder, limit, limit*page); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoAuthenticationLogs
}
@ -1033,4 +1043,5 @@ func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username strin
}
return attempts, nil
}

View File

@ -71,6 +71,7 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
provider.sqlSelectAuthenticationAttemptyByIP = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptyByIP)
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
provider.sqlSelectMigrations = provider.db.Rebind(provider.sqlSelectMigrations)

View File

@ -211,6 +211,14 @@ const (
ORDER BY time DESC
LIMIT ?
OFFSET ?;`
queryFmtSelect1FAAuthenticationLogEntryByIP = `
SELECT time, successful, username
FROM %s
WHERE time > ? AND remote_ip = ? AND auth_type = '1FA' AND banned = FALSE
ORDER BY time DESC
LIMIT ?
OFFSET ?;`
)
const (