authelia/internal/handlers/handler_sign_totp_test.go

850 lines
27 KiB
Go

package handlers
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"go.uber.org/mock/gomock"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/storage"
)
type HandlerSignTOTPSuite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *HandlerSignTOTPSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
userSession.AuthenticationLevel = authentication.OneFactor
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
s.mock.Clock.Set(time.Unix(1701295903, 0))
s.mock.Ctx.Clock = &s.mock.Clock
s.mock.Ctx.Configuration.TOTP = schema.DefaultTOTPConfiguration
}
func (s *HandlerSignTOTPSuite) TearDownTest() {
s.mock.Close()
}
func (s *HandlerSignTOTPSuite) AssertLastLogMessage(message string, err string) {
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), message, err)
}
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
s.mock.Ctx.Configuration.Session.Cookies[0].DefaultRedirectionURL = testRedirectionURL
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: testRedirectionURLString,
})
}
func (s *HandlerSignTOTPSuite) TestShouldFailWhenTOTPSignInInfoFailsToUpdate() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(errors.New("failed to perform update")),
)
s.mock.Ctx.Configuration.Session.Cookies[0].DefaultRedirectionURL = testRedirectionURL
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
}
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})
}
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookie{
{
Domain: "example.com",
},
{
Domain: "mydomain.local",
},
}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
TargetURL: "https://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: "https://mydomain.example.com",
})
}
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURLDisableReusePolicy() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookie{
{
Domain: "example.com",
},
{
Domain: "mydomain.local",
},
}
s.mock.Ctx.Configuration.TOTP.DisableReuseSecurityPolicy = true
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
TargetURL: "https://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: "https://mydomain.example.com",
})
}
func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, testUsername).
Return(&model.TOTPConfiguration{Secret: []byte("secret"), Digits: 6, Period: 30}, nil),
s.mock.TOTPMock.EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&model.TOTPConfiguration{Secret: []byte("secret"), Digits: 6, Period: 30})).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
TargetURL: "http://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
}
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}
func (s *HandlerSignTOTPSuite) TestShouldHandleErrorSaveHistory() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(fmt.Errorf("bad stuff")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.AssertLastLogMessage("Error occurred validating a TOTP authentication for user 'john': error occurred saving the TOTP history to the storage backend", "bad stuff")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleErrorExistsHistory() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, fmt.Errorf("oh my")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.AssertLastLogMessage("Error occurred validating a TOTP authentication for user 'john': error occurred checking the TOTP history", "oh my")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleExistsHistory() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(true, nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.AssertLastLogMessage("Error occurred validating a TOTP authentication for user 'john': error occurred satisfying security policies", "the user has already used this code recently and will not be permitted to reuse it")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleAnonymous() {
us, err := s.mock.Ctx.GetSession()
s.Require().NoError(err)
us.Username = ""
us.AuthenticationLevel = authentication.NotAuthenticated
s.Require().NoError(s.mock.Ctx.SaveSession(us))
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "abc",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.AssertLastLogMessage("Error occurred validating a TOTP authentication", "user is anonymous")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleGETAnonymous() {
us, err := s.mock.Ctx.GetSession()
s.Require().NoError(err)
us.Username = ""
us.AuthenticationLevel = authentication.NotAuthenticated
s.Require().NoError(s.mock.Ctx.SaveSession(us))
TimeBasedOneTimePasswordGET(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.AssertLastLogMessage("Error occurred retrieving TOTP configuration", "user is anonymous")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleGETErrorLoadConfiguration() {
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, testUsername).
Return(nil, fmt.Errorf("nah")),
)
TimeBasedOneTimePasswordGET(s.mock.Ctx)
s.mock.Assert500KO(s.T(), "Could not find TOTP Configuration for user.")
s.AssertLastLogMessage("Error occurred retrieving TOTP configuration for user 'john': error occurred retrieving the configuration from the storage backend", "nah")
}
func (s *HandlerSignTOTPSuite) TestShouldHandleGETErrorLoadConfigurationNotFound() {
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, testUsername).
Return(nil, storage.ErrNoTOTPConfiguration),
)
TimeBasedOneTimePasswordGET(s.mock.Ctx)
s.mock.Assert404KO(s.T(), "Could not find TOTP Configuration for user.")
s.AssertLastLogMessage("Error occurred retrieving TOTP configuration for user 'john': error occurred retrieving the configuration from the storage backend", "no TOTP configuration for user")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidValue() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(false, uint64(0), fmt.Errorf("invalid")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred validating a TOTP authentication for user 'john': error occurred validating the user input", "invalid")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidBoolean() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.
EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.
EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(false, uint64(0), nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: false,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).
Return(nil),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), MustGetLogLastSeq(s.T(), s.mock.Hook, 1), "Error occurred validating a TOTP authentication for user 'john': error occurred validating the user input", "the user input wasn't valid")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidBooleanMarkErr() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(false, uint64(0), nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
Username: testUsername,
Successful: false,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
})).Return(fmt.Errorf("failed to insert")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unable to mark TOTP authentication attempt by user 'john'", "failed to insert")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidJSON() {
s.mock.Ctx.Request.SetBody([]byte(`{"token:"1234"}`))
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred validating a TOTP authentication for user 'john': error parsing the request body", "unable to parse body: invalid character '1' after object key")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidBooleanMarkErrSuccess() {
config := model.TOTPConfiguration{ID: 1, Username: testUsername, Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
gomock.InOrder(
s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil),
s.mock.TOTPMock.EXPECT().
Validate(s.mock.Ctx, gomock.Eq("123456"), gomock.Eq(&config)).
Return(true, getStepTOTP(s.mock.Ctx, -1), nil),
s.mock.StorageMock.
EXPECT().
ExistsTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(false, nil),
s.mock.StorageMock.
EXPECT().
SaveTOTPHistory(s.mock.Ctx, testUsername, uint64(1701295890)).
Return(nil),
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, model.AuthenticationAttempt{
Username: testUsername,
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: model.NewNullIPFromString("0.0.0.0"),
}).
Return(fmt.Errorf("failed to insert")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unable to mark TOTP authentication attempt by user 'john'", "failed to insert")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidConfig() {
gomock.InOrder(
s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(nil, fmt.Errorf("not found")),
)
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123456",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert403KO(s.T(), "Authentication failed, please retry later.")
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred validating a TOTP authentication for user 'john': error occurred retreiving the configuration from the storage backend", "not found")
}
func (s *HandlerSignTOTPSuite) TestShouldReturnErrorOnInvalidTokenLength() {
bodyBytes, err := json.Marshal(bodySignTOTPRequest{
Token: "123",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.AssertKO(s.T(), "Authentication failed, please retry later.", fasthttp.StatusBadRequest)
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred validating a TOTP authentication for user 'john': expected code length is 6 or 8 but the user provided code was 3 characters in length", "")
}
func TestRunHandlerSignTOTPSuite(t *testing.T) {
suite.Run(t, new(HandlerSignTOTPSuite))
}
func TestSignTOTPHandleGetSessionError(t *testing.T) {
testCases := []struct {
name string
handler middlewares.RequestHandler
message string
expected string
}{
{
"ShouldHandleGET",
TimeBasedOneTimePasswordGET,
"Authentication failed, please retry later.",
"Error occurred retrieving TOTP configuration: error occurred retrieving the user session data",
},
{
"ShouldHandlePOST",
TimeBasedOneTimePasswordPOST,
"Authentication failed, please retry later.",
"Error occurred validating a TOTP authentication: error occurred retrieving the user session data",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
mock.Clock.Set(time.Unix(1701295903, 0))
mock.Ctx.Clock = &mock.Clock
mock.Ctx.Configuration.TOTP = schema.DefaultTOTPConfiguration
mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
tc.handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode())
assert.Equal(t, fmt.Sprintf(`{"status":"KO","message":"%s"}`, tc.message), string(mock.Ctx.Response.Body()))
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), tc.expected, "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
})
}
}