diff --git a/docs/security/measures.md b/docs/security/measures.md index e97c4cba..275292ab 100644 --- a/docs/security/measures.md +++ b/docs/security/measures.md @@ -28,6 +28,19 @@ that the attacker must also require the certificate to retrieve the cookies. Note that using [HSTS] has consequences. That's why you should read the blog post nginx has written on [HSTS]. +## Protection against username enumeration + +Authelia adaptively delays authentication attempts based on the mean (average) of the +previous 10 successful attempts, and a small random interval to make it even harder to +determine if the attempt was successful. On start it is assumed that the last 10 attempts +took 1000ms, this quickly grows or shrinks to the correct value over time regardless of the +authentication backend. + +The cost of this is low since in the instance of a user not existing it just sleeps to delay +the login. Lastly the absolute minimum time authentication can take is 250ms. Both of these measures +also have the added effect of creating an additional delay for all authentication attempts reducing +the likelihood a password can be brute-forced even if regulation settings are too permissive. + ## Protections against password cracking (File authentication provider) Authelia implements a variety of measures to prevent an attacker cracking passwords if they diff --git a/internal/handlers/const.go b/internal/handlers/const.go index 137dabb8..eabeab4c 100644 --- a/internal/handlers/const.go +++ b/internal/handlers/const.go @@ -39,3 +39,7 @@ const testInactivity = "10" const testRedirectionURL = "http://redirection.local" const testResultAllow = "allow" const testUsername = "john" + +const movingAverageWindow = 10 +const msMinimumDelay1FA = float64(250) +const msMaximumRandomDelay = int64(85) diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 87a34cc9..978cb8a6 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -2,6 +2,9 @@ package handlers import ( "fmt" + "math" + "math/rand" + "sync" "time" "github.com/authelia/authelia/internal/authentication" @@ -10,119 +13,176 @@ import ( "github.com/authelia/authelia/internal/session" ) +func movingAverageIteration(value time.Duration, successful bool, movingAverageCursor *int, execDurationMovingAverage *[]time.Duration, mutex sync.Locker) float64 { + mutex.Lock() + if successful { + (*execDurationMovingAverage)[*movingAverageCursor] = value + *movingAverageCursor = (*movingAverageCursor + 1) % movingAverageWindow + } + + var sum int64 + + for _, v := range *execDurationMovingAverage { + sum += v.Milliseconds() + } + mutex.Unlock() + + return float64(sum / movingAverageWindow) +} + +func calculateActualDelay(ctx *middlewares.AutheliaCtx, execDuration time.Duration, avgExecDurationMs float64, successful *bool) float64 { + randomDelayMs := float64(rand.Int63n(msMaximumRandomDelay)) + totalDelayMs := math.Max(avgExecDurationMs, msMinimumDelay1FA) + randomDelayMs + actualDelayMs := math.Max(totalDelayMs-float64(execDuration.Milliseconds()), 1.0) + ctx.Logger.Tracef("attempt successful: %t, exec duration: %d, avg execution duration: %d, random delay ms: %d, total delay ms: %d, actual delay ms: %d", *successful, execDuration.Milliseconds(), int64(avgExecDurationMs), int64(randomDelayMs), int64(totalDelayMs), int64(actualDelayMs)) + + return actualDelayMs +} + +func delayToPreventTimingAttacks(ctx *middlewares.AutheliaCtx, requestTime time.Time, successful *bool, movingAverageCursor *int, execDurationMovingAverage *[]time.Duration, mutex sync.Locker) { + execDuration := time.Since(requestTime) + avgExecDurationMs := movingAverageIteration(execDuration, *successful, movingAverageCursor, execDurationMovingAverage, mutex) + actualDelayMs := calculateActualDelay(ctx, execDuration, avgExecDurationMs, successful) + time.Sleep(time.Duration(actualDelayMs) * time.Millisecond) +} + // FirstFactorPost is the handler performing the first factory. //nolint:gocyclo // TODO: Consider refactoring time permitting. -func FirstFactorPost(ctx *middlewares.AutheliaCtx) { - bodyJSON := firstFactorRequestBody{} - err := ctx.ParseBody(&bodyJSON) +func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middlewares.RequestHandler { + var execDurationMovingAverage = make([]time.Duration, movingAverageWindow) - if err != nil { - handleAuthenticationUnauthorized(ctx, err, authenticationFailedMessage) - return + var movingAverageCursor = 0 + + var mutex = &sync.Mutex{} + + for i := range execDurationMovingAverage { + execDurationMovingAverage[i] = msInitialDelay * time.Millisecond } - bannedUntil, err := ctx.Providers.Regulator.Regulate(bodyJSON.Username) + rand.Seed(time.Now().UnixNano()) - if err != nil { - if err == regulation.ErrUserIsBanned { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage) - return + return func(ctx *middlewares.AutheliaCtx) { + var successful bool + + requestTime := time.Now() + + if delayEnabled { + defer delayToPreventTimingAttacks(ctx, requestTime, &successful, &movingAverageCursor, &execDurationMovingAverage, mutex) } - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regulate authentication: %s", err.Error()), authenticationFailedMessage) + bodyJSON := firstFactorRequestBody{} + err := ctx.ParseBody(&bodyJSON) - return - } - - userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password) - - if err != nil { - ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) - ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) - - return - } - - if !userPasswordOk { - ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) - ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) - - ctx.ReplyError(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) - - return - } - - ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username) - - ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) - err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true) - - if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to mark authentication: %s", err.Error()), authenticationFailedMessage) - return - } - - // Reset all values from previous session before regenerating the cookie. - err = ctx.SaveSession(session.NewDefaultUserSession()) - - if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) - return - } - - err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) - - if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) - return - } - - // Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data - keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != 0 && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn - - // Set the cookie to expire if remember me is enabled and the user has asked us to - if keepMeLoggedIn { - err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, authenticationFailedMessage) return } + + bannedUntil, err := ctx.Providers.Regulator.Regulate(bodyJSON.Username) + + if err != nil { + if err == regulation.ErrUserIsBanned { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage) + return + } + + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regulate authentication: %s", err.Error()), authenticationFailedMessage) + + return + } + + userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password) + + if err != nil { + ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) + ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + + return + } + + if !userPasswordOk { + ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) + ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) + + ctx.ReplyError(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) + + return + } + + ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username) + + ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username) + err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true) + + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to mark authentication: %s", err.Error()), authenticationFailedMessage) + return + } + + // Reset all values from previous session before regenerating the cookie. + err = ctx.SaveSession(session.NewDefaultUserSession()) + + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + return + } + + err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) + + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + return + } + + // Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data + keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != 0 && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn + + // Set the cookie to expire if remember me is enabled and the user has asked us to + if keepMeLoggedIn { + err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + return + } + } + + // Get the details of the given user from the user provider. + userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username) + + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + return + } + + ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails) + + // And set those information in the new session. + userSession := ctx.GetSession() + userSession.Username = userDetails.Username + userSession.Groups = userDetails.Groups + userSession.Emails = userDetails.Emails + userSession.AuthenticationLevel = authentication.OneFactor + userSession.LastActivity = time.Now().Unix() + userSession.KeepMeLoggedIn = keepMeLoggedIn + refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend) + + if refresh { + userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval) + } + + err = ctx.SaveSession(userSession) + + if err != nil { + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage) + return + } + + successful = true + + Handle1FAResponse(ctx, bodyJSON.TargetURL, userSession.Username, userSession.Groups) } - - // Get the details of the given user from the user provider. - userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username) - - if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) - return - } - - ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails) - - // And set those information in the new session. - userSession := ctx.GetSession() - userSession.Username = userDetails.Username - userSession.Groups = userDetails.Groups - userSession.Emails = userDetails.Emails - userSession.AuthenticationLevel = authentication.OneFactor - userSession.LastActivity = time.Now().Unix() - userSession.KeepMeLoggedIn = keepMeLoggedIn - refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend) - - if refresh { - userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval) - } - - err = ctx.SaveSession(userSession) - - if err != nil { - handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage) - return - } - - Handle1FAResponse(ctx, bodyJSON.TargetURL, userSession.Username, userSession.Groups) } diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index f052f041..4e10b4a6 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)) + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 02413e58..f3c290ba 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -47,7 +47,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) router.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) - router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost)) + router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost(1000, true))) router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost)) // Only register endpoints if forgot password is not disabled. diff --git a/internal/suites/scenario_regulation_test.go b/internal/suites/scenario_regulation_test.go index 120f8e5c..827e71e9 100644 --- a/internal/suites/scenario_regulation_test.go +++ b/internal/suites/scenario_regulation_test.go @@ -47,7 +47,7 @@ func (s *RegulationScenario) SetupTest() { } func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() s.doVisitLoginPage(ctx, s.T(), "") diff --git a/internal/suites/suite_ldap.go b/internal/suites/suite_ldap.go index be2d2161..caf7fb69 100644 --- a/internal/suites/suite_ldap.go +++ b/internal/suites/suite_ldap.go @@ -57,7 +57,7 @@ func init() { SetUp: setup, SetUpTimeout: 5 * time.Minute, OnSetupTimeout: displayAutheliaLogs, - TestTimeout: 1 * time.Minute, + TestTimeout: 80 * time.Second, TearDown: teardown, TearDownTimeout: 2 * time.Minute, OnError: displayAutheliaLogs,