mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
ad8e844af6
Allow users to configure the TOTP Algorithm and Digits. This should be used with caution as many TOTP applications do not support it. Some will also fail to notify the user that there is an issue. i.e. if the algorithm in the QR code is sha512, they continue to generate one time passwords with sha1. In addition this drastically refactors TOTP in general to be more user friendly by not forcing them to register a new device if the administrator changes the period (or algorithm). Fixes #1226.
307 lines
8.4 KiB
Go
307 lines
8.4 KiB
Go
package regulation_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/mocks"
|
|
"github.com/authelia/authelia/v4/internal/models"
|
|
"github.com/authelia/authelia/v4/internal/regulation"
|
|
)
|
|
|
|
type RegulatorSuite struct {
|
|
suite.Suite
|
|
|
|
ctx context.Context
|
|
ctrl *gomock.Controller
|
|
storageMock *mocks.MockStorage
|
|
configuration schema.RegulationConfiguration
|
|
clock mocks.TestingClock
|
|
}
|
|
|
|
func (s *RegulatorSuite) SetupTest() {
|
|
s.ctrl = gomock.NewController(s.T())
|
|
s.storageMock = mocks.NewMockStorage(s.ctrl)
|
|
s.ctx = context.Background()
|
|
|
|
s.configuration = schema.RegulationConfiguration{
|
|
MaxRetries: 3,
|
|
BanTime: "180",
|
|
FindTime: "30",
|
|
}
|
|
s.clock.Set(time.Now())
|
|
}
|
|
|
|
func (s *RegulatorSuite) TearDownTest() {
|
|
s.ctrl.Finish()
|
|
}
|
|
|
|
func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: true,
|
|
Time: s.clock.Now().Add(-4 * time.Minute),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
}
|
|
|
|
// This test checks the case in which a user failed to authenticate many times but always
|
|
// with a certain amount of time larger than FindTime. Meaning the user should not be banned.
|
|
func (s *RegulatorSuite) TestShouldNotThrowWhenFailedAuthenticationNotInFindTime() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-1 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-90 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-180 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
}
|
|
|
|
// This test checks the case in which a user failed to authenticate many times only a few
|
|
// seconds ago (meaning we are checking from now back to now-FindTime).
|
|
func (s *RegulatorSuite) TestShouldBanUserIfLatestAttemptsAreWithinFinTime() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-1 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-4 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-6 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-180 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
|
|
}
|
|
|
|
// This test checks the case in which a user failed to authenticate many times only a few
|
|
// seconds ago (meaning we are checking from now-FindTime+X back to now-2FindTime+X knowing that
|
|
// we are within now and now-BanTime). It means the user has been banned some time ago and is still
|
|
// banned right now.
|
|
func (s *RegulatorSuite) TestShouldCheckUserIsStillBanned() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-31 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-34 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-36 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
|
|
}
|
|
|
|
func (s *RegulatorSuite) TestShouldCheckUserIsNotYetBanned() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-34 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-36 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
}
|
|
|
|
func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-14 * time.Second),
|
|
},
|
|
// more than 30 seconds elapsed between this auth and the preceding one.
|
|
// In that case we don't need to regulate the user even though the number
|
|
// of retrieved attempts is 3.
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-94 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-96 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
}
|
|
|
|
func (s *RegulatorSuite) TestShouldCheckRegulationHasBeenResetOnSuccessfulAttempt() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-90 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: true,
|
|
Time: s.clock.Now().Add(-93 * time.Second),
|
|
},
|
|
// The user was almost banned but he did a successful attempt. Therefore, even if the next
|
|
// failure happens within FindTime, he should not be banned.
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-94 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-96 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
regulator := regulation.NewRegulator(&s.configuration, s.storageMock, &s.clock)
|
|
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
}
|
|
|
|
func TestRunRegulatorSuite(t *testing.T) {
|
|
s := new(RegulatorSuite)
|
|
suite.Run(t, s)
|
|
}
|
|
|
|
// This test checks that the regulator is disabled when configuration is set to 0.
|
|
func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
|
|
attemptsInDB := []models.AuthenticationAttempt{
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-31 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-34 * time.Second),
|
|
},
|
|
{
|
|
Username: "john",
|
|
Successful: false,
|
|
Time: s.clock.Now().Add(-36 * time.Second),
|
|
},
|
|
}
|
|
|
|
s.storageMock.EXPECT().
|
|
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
|
|
Return(attemptsInDB, nil)
|
|
|
|
// Check Disabled Functionality
|
|
configuration := schema.RegulationConfiguration{
|
|
MaxRetries: 0,
|
|
FindTime: "180",
|
|
BanTime: "180",
|
|
}
|
|
|
|
regulator := regulation.NewRegulator(&configuration, s.storageMock, &s.clock)
|
|
_, err := regulator.Regulate(s.ctx, "john")
|
|
assert.NoError(s.T(), err)
|
|
|
|
// Check Enabled Functionality
|
|
configuration = schema.RegulationConfiguration{
|
|
MaxRetries: 1,
|
|
FindTime: "180",
|
|
BanTime: "180",
|
|
}
|
|
|
|
regulator = regulation.NewRegulator(&configuration, s.storageMock, &s.clock)
|
|
_, err = regulator.Regulate(s.ctx, "john")
|
|
assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
|
|
}
|