diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 1ae05b558..87a34cc97 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -17,7 +17,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { err := ctx.ParseBody(&bodyJSON) if err != nil { - ctx.Error(err, authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, authenticationFailedMessage) return } @@ -25,11 +25,11 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { if err != nil { if err == regulation.ErrUserIsBanned { - ctx.Error(fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage) return } - ctx.Error(fmt.Errorf("Unable to regulate authentication: %s", err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regulate authentication: %s", err.Error()), authenticationFailedMessage) return } @@ -40,7 +40,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { 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. - ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) return } @@ -49,6 +49,8 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { 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 @@ -60,7 +62,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true) if err != nil { - ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to mark authentication: %s", err.Error()), authenticationFailedMessage) return } @@ -68,14 +70,14 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { err = ctx.SaveSession(session.NewDefaultUserSession()) if err != nil { - ctx.Error(fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage) + 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 { - ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) return } @@ -86,7 +88,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { if keepMeLoggedIn { err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) if err != nil { - ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) return } } @@ -95,7 +97,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username) if err != nil { - ctx.Error(fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage) return } @@ -118,7 +120,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage) return } diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index c27c8e5da..f052f041b 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -34,7 +34,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() { // No body assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message) - s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() { @@ -45,7 +45,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() { FirstFactorPost(s.mock.Ctx) assert.Equal(s.T(), "Unable to validate body: password: non zero value required", s.mock.Hook.LastEntry().Message) - s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() { @@ -70,7 +70,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() { FirstFactorPost(s.mock.Ctx) assert.Equal(s.T(), "Error while checking password for user test: Failed", s.mock.Hook.LastEntry().Message) - s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() { @@ -120,7 +120,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() { FirstFactorPost(s.mock.Ctx) assert.Equal(s.T(), "Error while retrieving details from user test: Failed", s.mock.Hook.LastEntry().Message) - s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() { @@ -142,7 +142,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() { FirstFactorPost(s.mock.Ctx) assert.Equal(s.T(), "Unable to mark authentication: failed", s.mock.Hook.LastEntry().Message) - s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") + s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.") } func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() { diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go index 6962ce9a8..2eaedb1b8 100644 --- a/internal/handlers/handler_sign_duo.go +++ b/internal/handlers/handler_sign_duo.go @@ -16,7 +16,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err := ctx.ParseBody(&requestBody) if err != nil { - ctx.Error(err, mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) return } @@ -38,7 +38,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { duoResponse, err := duoAPI.Call(values, ctx) if err != nil { - ctx.Error(fmt.Errorf("Duo API errored: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Duo API errored: %s", err), mfaValidationFailedMessage) return } @@ -61,7 +61,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) return } @@ -69,7 +69,7 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler { err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage) return } diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go index 8a2db7873..157548e8d 100644 --- a/internal/handlers/handler_sign_duo_test.go +++ b/internal/handlers/handler_sign_duo_test.go @@ -92,7 +92,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() { SecondFactorDuoPost(duoMock)(s.mock.Ctx) - s.mock.Assert200KO(s.T(), "Authentication failed, please retry later.") + s.mock.Assert401KO(s.T(), "Authentication failed, please retry later.") } func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() { diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go index c06181af0..3425b68b6 100644 --- a/internal/handlers/handler_sign_totp.go +++ b/internal/handlers/handler_sign_totp.go @@ -14,7 +14,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler err := ctx.ParseBody(&bodyJSON) if err != nil { - ctx.Error(err, mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage) return } @@ -22,25 +22,25 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler secret, err := ctx.Providers.StorageProvider.LoadTOTPSecret(userSession.Username) if err != nil { - ctx.Error(fmt.Errorf("Unable to load TOTP secret: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to load TOTP secret: %s", err), mfaValidationFailedMessage) return } isValid, err := totpVerifier.Verify(bodyJSON.Token, secret) if err != nil { - ctx.Error(fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) return } if !isValid { - ctx.Error(fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Wrong passcode during TOTP validation for user %s", userSession.Username), mfaValidationFailedMessage) return } err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) return } @@ -48,7 +48,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage) return } diff --git a/internal/handlers/handler_sign_u2f_step1.go b/internal/handlers/handler_sign_u2f_step1.go index 640298961..caf660a25 100644 --- a/internal/handlers/handler_sign_u2f_step1.go +++ b/internal/handlers/handler_sign_u2f_step1.go @@ -14,12 +14,12 @@ import ( // SecondFactorU2FSignGet handler for initiating a signing request. func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { if ctx.XForwardedProto() == nil { - ctx.Error(errMissingXForwardedProto, operationFailedMessage) + ctx.Error(errMissingXForwardedProto, mfaValidationFailedMessage) return } if ctx.XForwardedHost() == nil { - ctx.Error(errMissingXForwardedHost, operationFailedMessage) + ctx.Error(errMissingXForwardedHost, mfaValidationFailedMessage) return } @@ -29,7 +29,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { challenge, err := u2f.NewChallenge(appID, trustedFacets) if err != nil { - ctx.Error(fmt.Errorf("Unable to create U2F challenge: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to create U2F challenge: %s", err), mfaValidationFailedMessage) return } @@ -38,11 +38,11 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { if err != nil { if err == storage.ErrNoU2FDeviceHandle { - ctx.Error(fmt.Errorf("No device handle found for user %s", userSession.Username), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("No device handle found for user %s", userSession.Username), mfaValidationFailedMessage) return } - ctx.Error(fmt.Errorf("Unable to retrieve U2F device handle: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to retrieve U2F device handle: %s", err), mfaValidationFailedMessage) return } @@ -63,7 +63,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to save U2F challenge and registration in session: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save U2F challenge and registration in session: %s", err), mfaValidationFailedMessage) return } @@ -71,7 +71,7 @@ func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) { err = ctx.SetJSONBody(signRequest) if err != nil { - ctx.Error(fmt.Errorf("Unable to set sign request in body: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to set sign request in body: %s", err), mfaValidationFailedMessage) return } } diff --git a/internal/handlers/handler_sign_u2f_step2.go b/internal/handlers/handler_sign_u2f_step2.go index 00dd853c1..5a51722fc 100644 --- a/internal/handlers/handler_sign_u2f_step2.go +++ b/internal/handlers/handler_sign_u2f_step2.go @@ -20,12 +20,12 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler userSession := ctx.GetSession() if userSession.U2FChallenge == nil { - ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no challenge)"), mfaValidationFailedMessage) return } if userSession.U2FRegistration == nil { - ctx.Error(fmt.Errorf("U2F signing has not been initiated yet (no registration)"), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("U2F signing has not been initiated yet (no registration)"), mfaValidationFailedMessage) return } @@ -43,7 +43,7 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) if err != nil { - ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), mfaValidationFailedMessage) return } @@ -51,7 +51,7 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler err = ctx.SaveSession(userSession) if err != nil { - ctx.Error(fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage) + handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage) return } diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go index 4ee4afb76..f42815493 100644 --- a/internal/handlers/handler_verify.go +++ b/internal/handlers/handler_verify.go @@ -104,9 +104,10 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U IP: clientIP, }, targetURL) - if level == authorization.Bypass { + switch { + case level == authorization.Bypass: return Authorized - } else if username != "" && level == authorization.Denied { + case level == authorization.Denied && username != "": // If the user is not anonymous, it means that we went through // all the rules related to that user and knowing who he is we can // deduce the access is forbidden @@ -114,14 +115,9 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U // could not be granted the rights to access the resource. Consequently // for anonymous users we send Unauthorized instead of Forbidden return Forbidden - } else { - if level == authorization.OneFactor && - authLevel >= authentication.OneFactor { - return Authorized - } else if level == authorization.TwoFactor && - authLevel >= authentication.TwoFactor { - return Authorized - } + case level == authorization.OneFactor && authLevel >= authentication.OneFactor, + level == authorization.TwoFactor && authLevel >= authentication.TwoFactor: + return Authorized } return NotAuthorized @@ -186,7 +182,8 @@ func hasUserBeenInactiveTooLong(ctx *middlewares.AutheliaCtx) (bool, error) { // } // verifySessionCookie verifies if a user is identified by a cookie. -func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool, refreshProfileInterval time.Duration) (username string, groups []string, authLevel authentication.Level, err error) { +func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool, + refreshProfileInterval time.Duration) (username string, groups []string, authLevel authentication.Level, err error) { // No username in the session means the user is anonymous. isUserAnonymous := userSession.Username == "" @@ -326,6 +323,15 @@ func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *ur if !groupsDiff && !emailsDiff { ctx.Logger.Tracef("Updated profile not detected for %s.", userSession.Username) + // Only update TTL if the user has a interval set. + // We get to this check when there were no changes. + // Also make sure to update the session even if no difference was found. + // This is so that we don't check every subsequent request after this one. + if refreshProfileInterval != schema.RefreshIntervalAlways { + // Update RefreshTTL and save session if refresh is not set to always. + userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval) + return ctx.SaveSession(*userSession) + } } else { ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username) if ctx.Logger.Level.String() == "trace" { @@ -338,17 +344,12 @@ func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *ur if refreshProfileInterval != schema.RefreshIntervalAlways { userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval) } - return ctx.SaveSession(*userSession) - } - // Only update TTL if the user has a interval set. - // Also make sure to update the session even if no difference was found. - // This is so that we don't check every subsequent request after this one. - if refreshProfileInterval != schema.RefreshIntervalAlways { - userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval) + // Return the result of save session if there were changes. return ctx.SaveSession(*userSession) } } + // Return nil if disabled or if no changes and refresh interval set to always. return nil } @@ -431,12 +432,13 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques authorization := isTargetURLAuthorized(ctx.Providers.Authorizer, *targetURL, username, groups, ctx.RemoteIP(), authLevel) - if authorization == Forbidden { + switch authorization { + case Forbidden: ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username) ctx.ReplyForbidden() - } else if authorization == NotAuthorized { + case NotAuthorized: handleUnauthorized(ctx, targetURL, username) - } else if authorization == Authorized { + case Authorized: setForwardedHeaders(&ctx.Response.Header, username, groups) } diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 25d4961cc..93f15dcdf 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -4,6 +4,8 @@ import ( "fmt" "net/url" + "github.com/valyala/fasthttp" + "github.com/authelia/authelia/internal/authorization" "github.com/authelia/authelia/internal/middlewares" "github.com/authelia/authelia/internal/utils" @@ -85,3 +87,9 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) { ctx.ReplyOK() } } + +// handleAuthenticationUnauthorized provides harmonized response codes for 1FA. +func handleAuthenticationUnauthorized(ctx *middlewares.AutheliaCtx, err error, message string) { + ctx.SetStatusCode(fasthttp.StatusUnauthorized) + ctx.Error(err, message) +} diff --git a/internal/mocks/mock_authelia_ctx.go b/internal/mocks/mock_authelia_ctx.go index 9a54d681a..08cfb6530 100644 --- a/internal/mocks/mock_authelia_ctx.go +++ b/internal/mocks/mock_authelia_ctx.go @@ -133,6 +133,12 @@ func (m *MockAutheliaCtx) Close() { m.Ctrl.Finish() } +// Assert401KO assert an error response from the service. +func (m *MockAutheliaCtx) Assert401KO(t *testing.T, message string) { + assert.Equal(t, 401, m.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf("{\"status\":\"KO\",\"message\":\"%s\"}", message), string(m.Ctx.Response.Body())) +} + // Assert200KO assert an error response from the service. func (m *MockAutheliaCtx) Assert200KO(t *testing.T, message string) { assert.Equal(t, 200, m.Ctx.Response.StatusCode())