summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_firstfactor_test.go
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2020-05-21 08:03:15 +1000
committerGitHub <noreply@github.com>2020-05-21 00:03:15 +0200
commit469daedd36e03b6ff9a432255e6a27548a5631e4 (patch)
tree6fa14d935457ed7ead203f71c0a3cf080b4b865c /internal/handlers/handler_firstfactor_test.go
parent147d0879e392ae2f55e1efe7f38673951347f638 (diff)
[FEATURE] Delay 1FA Authentication (#993)
* adaptively delay 1FA by the actual execution time of authentication * should grow and shrink over time as successful attempts are made * uses the average of the last 10 successful attempts to calculate * starts at an average of 1000ms * minimum is 250ms * a random delay is added to the largest of avg or minimum * the random delay is between 0ms and 85ms * bump LDAP suite to 80s timeout * bump regulation scenario to 45s * add mutex locking * amend logging * add docs * add tests Co-authored-by: Clément Michaud <clement.michaud34@gmail.com>
Diffstat (limited to 'internal/handlers/handler_firstfactor_test.go')
-rw-r--r--internal/handlers/handler_firstfactor_test.go82
1 files changed, 69 insertions, 13 deletions
diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go
index f052f041b..4e10b4a64 100644
--- a/internal/handlers/handler_firstfactor_test.go
+++ b/internal/handlers/handler_firstfactor_test.go
@@ -2,7 +2,9 @@ package handlers
import (
"fmt"
+ "sync"
"testing"
+ "time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
@@ -30,7 +32,7 @@ func (s *FirstFactorSuite) TearDownTest() {
}
func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() {
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// No body
assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
@@ -42,7 +44,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() {
s.mock.Ctx.Request.SetBodyString(`{
"username": "test"
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
assert.Equal(s.T(), "Unable to validate body: password: non zero value required", s.mock.Hook.LastEntry().Message)
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
@@ -67,7 +69,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
"password": "hello",
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
assert.Equal(s.T(), "Error while checking password for user test: Failed", s.mock.Hook.LastEntry().Message)
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
@@ -93,7 +95,7 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
}
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
@@ -117,7 +119,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
"password": "hello",
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
assert.Equal(s.T(), "Error while retrieving details from user test: Failed", s.mock.Hook.LastEntry().Message)
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
@@ -139,7 +141,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
"password": "hello",
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
assert.Equal(s.T(), "Unable to mark authentication: failed", s.mock.Hook.LastEntry().Message)
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
@@ -170,7 +172,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
"password": "hello",
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
@@ -210,7 +212,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
"password": "hello",
"keepMeLoggedIn": false
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
@@ -253,7 +255,7 @@ func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSess
"password": "hello",
"keepMeLoggedIn": true
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
@@ -323,7 +325,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenNoTarget
"password": "hello",
"keepMeLoggedIn": false
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"})
@@ -343,7 +345,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUns
"targetURL": "http://notsafe.local"
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"})
@@ -363,7 +365,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenNoTargetURLProvidedA
"keepMeLoggedIn": false
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
s.mock.Assert200OK(s.T(), nil)
@@ -393,7 +395,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvi
"keepMeLoggedIn": false
}`)
- FirstFactorPost(s.mock.Ctx)
+ FirstFactorPost(0, false)(s.mock.Ctx)
// Respond with 200.
s.mock.Assert200OK(s.T(), nil)
@@ -403,3 +405,57 @@ func TestFirstFactorSuite(t *testing.T) {
suite.Run(t, new(FirstFactorSuite))
suite.Run(t, new(FirstFactorRedirectionSuite))
}
+
+func TestFirstFactorDelayAverages(t *testing.T) {
+ execDuration := time.Millisecond * 500
+ oneSecond := time.Millisecond * 1000
+ durations := []time.Duration{oneSecond, oneSecond, oneSecond, oneSecond, oneSecond, oneSecond, oneSecond, oneSecond, oneSecond, oneSecond}
+ cursor := 0
+ mutex := &sync.Mutex{}
+ avgExecDuration := movingAverageIteration(execDuration, false, &cursor, &durations, mutex)
+ assert.Equal(t, avgExecDuration, float64(1000))
+
+ execDurations := []time.Duration{
+ time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500,
+ time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500,
+ time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500, time.Millisecond * 500,
+ }
+
+ current := float64(1000)
+
+ // Execute at 500ms for 12 requests.
+ for _, execDuration = range execDurations {
+ // Should not dip below 500, and should decrease in value by 50 each iteration.
+ if current > 500 {
+ current -= 50
+ }
+
+ avgExecDuration := movingAverageIteration(execDuration, true, &cursor, &durations, mutex)
+ assert.Equal(t, avgExecDuration, current)
+ }
+}
+
+func TestFirstFactorDelayCalculations(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ successful := false
+
+ execDuration := 500 * time.Millisecond
+ avgExecDurationMs := 1000.0
+ expectedMinimumDelayMs := avgExecDurationMs - float64(execDuration.Milliseconds())
+
+ for i := 0; i < 100; i++ {
+ delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful)
+ assert.True(t, delay >= expectedMinimumDelayMs)
+ assert.True(t, delay <= expectedMinimumDelayMs+float64(msMaximumRandomDelay))
+ }
+
+ execDuration = 5 * time.Millisecond
+ avgExecDurationMs = 5.0
+ expectedMinimumDelayMs = msMinimumDelay1FA - float64(execDuration.Milliseconds())
+
+ for i := 0; i < 100; i++ {
+ delay := calculateActualDelay(mock.Ctx, execDuration, avgExecDurationMs, &successful)
+ assert.True(t, delay >= expectedMinimumDelayMs)
+ assert.True(t, delay <= expectedMinimumDelayMs+float64(msMaximumRandomDelay))
+ }
+}