mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
cc6650dbcd
* [BUGFIX] Set username retrieved from authentication backend in session. In some setups, binding is case insensitive but Authelia is case sensitive and therefore need the actual username as stored in the authentication backend in order for Authelia to work correctly. Fixes #561. * Use uid attribute as unique user identifier in suites. * Fix the integration tests. * Update config.template.yml * Compute user filter based on username attribute and users_filter. The filter provided in users_filter is now combined with a filter based on the username attribute to perform the LDAP search query finding a user object from the username. * Fix LDAP based integration tests. * Update `users_filter` reference examples
411 lines
12 KiB
Go
411 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/authelia/authelia/internal/authorization"
|
|
"github.com/authelia/authelia/internal/configuration/schema"
|
|
"github.com/authelia/authelia/internal/mocks"
|
|
"github.com/authelia/authelia/internal/models"
|
|
|
|
"github.com/authelia/authelia/internal/authentication"
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type FirstFactorSuite struct {
|
|
suite.Suite
|
|
|
|
mock *mocks.MockAutheliaCtx
|
|
}
|
|
|
|
func (s *FirstFactorSuite) SetupTest() {
|
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TearDownTest() {
|
|
s.mock.Close()
|
|
}
|
|
|
|
func (s *FirstFactorSuite) assertError500(err string) {
|
|
assert.Equal(s.T(), 500, s.mock.Ctx.Response.StatusCode())
|
|
assert.Equal(s.T(), []byte(InternalError), s.mock.Ctx.Response.Body())
|
|
assert.Equal(s.T(), err, s.mock.Hook.LastEntry().Message)
|
|
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() {
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// 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.")
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() {
|
|
// Missing password
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test"
|
|
}`)
|
|
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.")
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(false, fmt.Errorf("Failed"))
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Eq(models.AuthenticationAttempt{
|
|
Username: "test",
|
|
Successful: false,
|
|
Time: s.mock.Clock.Now(),
|
|
}))
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
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.")
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(false, fmt.Errorf("Invalid credentials"))
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Eq(models.AuthenticationAttempt{
|
|
Username: "test",
|
|
Successful: false,
|
|
Time: s.mock.Clock.Now(),
|
|
}))
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
|
|
FirstFactorPost(s.mock.Ctx)
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(nil)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
GetDetails(gomock.Eq("test")).
|
|
Return(nil, fmt.Errorf("Failed"))
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
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.")
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(fmt.Errorf("failed"))
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
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.")
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
GetDetails(gomock.Eq("test")).
|
|
Return(&authentication.UserDetails{
|
|
Username: "test",
|
|
Emails: []string{"test@example.com"},
|
|
Groups: []string{"dev", "admins"},
|
|
}, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(nil)
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
|
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
|
|
|
|
// And store authentication in session.
|
|
session := s.mock.Ctx.GetSession()
|
|
assert.Equal(s.T(), "test", session.Username)
|
|
assert.Equal(s.T(), true, session.KeepMeLoggedIn)
|
|
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
|
|
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
|
|
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
GetDetails(gomock.Eq("test")).
|
|
Return(&authentication.UserDetails{
|
|
Username: "test",
|
|
Emails: []string{"test@example.com"},
|
|
Groups: []string{"dev", "admins"},
|
|
}, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(nil)
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": false
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
|
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
|
|
|
|
// And store authentication in session.
|
|
session := s.mock.Ctx.GetSession()
|
|
assert.Equal(s.T(), "test", session.Username)
|
|
assert.Equal(s.T(), false, session.KeepMeLoggedIn)
|
|
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
|
|
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
|
|
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
|
}
|
|
|
|
func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSession() {
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
GetDetails(gomock.Eq("test")).
|
|
Return(&authentication.UserDetails{
|
|
// This is the name in authentication backend, in some setups the binding is
|
|
// case insensitive but the user ID in session must match the user in LDAP
|
|
// for the other modules of Authelia to be coherent.
|
|
Username: "Test",
|
|
Emails: []string{"test@example.com"},
|
|
Groups: []string{"dev", "admins"},
|
|
}, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(nil)
|
|
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": true
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
|
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
|
|
|
|
// And store authentication in session.
|
|
session := s.mock.Ctx.GetSession()
|
|
assert.Equal(s.T(), "Test", session.Username)
|
|
assert.Equal(s.T(), true, session.KeepMeLoggedIn)
|
|
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
|
|
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
|
|
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
|
|
}
|
|
|
|
type FirstFactorRedirectionSuite struct {
|
|
suite.Suite
|
|
|
|
mock *mocks.MockAutheliaCtx
|
|
}
|
|
|
|
func (s *FirstFactorRedirectionSuite) SetupTest() {
|
|
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
|
s.mock.Ctx.Configuration.DefaultRedirectionURL = "https://default.local"
|
|
s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = "bypass"
|
|
s.mock.Ctx.Configuration.AccessControl.Rules = []schema.ACLRule{
|
|
schema.ACLRule{
|
|
Domain: "default.local",
|
|
Policy: "one_factor",
|
|
},
|
|
}
|
|
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
|
|
s.mock.Ctx.Configuration.AccessControl)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
|
Return(true, nil)
|
|
|
|
s.mock.UserProviderMock.
|
|
EXPECT().
|
|
GetDetails(gomock.Eq("test")).
|
|
Return(&authentication.UserDetails{
|
|
Username: "test",
|
|
Emails: []string{"test@example.com"},
|
|
Groups: []string{"dev", "admins"},
|
|
}, nil)
|
|
|
|
s.mock.StorageProviderMock.
|
|
EXPECT().
|
|
AppendAuthenticationLog(gomock.Any()).
|
|
Return(nil)
|
|
}
|
|
|
|
func (s *FirstFactorRedirectionSuite) TearDownTest() {
|
|
s.mock.Close()
|
|
}
|
|
|
|
// When:
|
|
// 1/ the target url is unknown
|
|
// 2/ two_factor is disabled (no policy is set to two_factor)
|
|
// 3/ default_redirect_url is provided
|
|
// Then:
|
|
// the user should be redirected to the default url.
|
|
func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenNoTargetURLProvidedAndTwoFactorDisabled() {
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": false
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"})
|
|
}
|
|
|
|
// When:
|
|
// 1/ the target url is unsafe
|
|
// 2/ two_factor is disabled (no policy is set to two_factor)
|
|
// 3/ default_redirect_url is provided
|
|
// Then:
|
|
// the user should be redirected to the default url.
|
|
func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUnsafeAndTwoFactorDisabled() {
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": false,
|
|
"targetURL": "http://notsafe.local"
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"})
|
|
}
|
|
|
|
// When:
|
|
// 1/ two_factor is enabled (default policy)
|
|
// Then:
|
|
// the user should receive 200 without redirection URL.
|
|
func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenNoTargetURLProvidedAndTwoFactorEnabled() {
|
|
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
|
DefaultPolicy: "two_factor",
|
|
})
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": false
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
s.mock.Assert200OK(s.T(), nil)
|
|
}
|
|
|
|
// When:
|
|
// 1/ two_factor is enabled (some rule)
|
|
// Then:
|
|
// the user should receive 200 without redirection URL.
|
|
func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvidedAndTwoFactorEnabled() {
|
|
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
|
DefaultPolicy: "one_factor",
|
|
Rules: []schema.ACLRule{
|
|
schema.ACLRule{
|
|
Domain: "test.example.com",
|
|
Policy: "one_factor",
|
|
},
|
|
schema.ACLRule{
|
|
Domain: "example.com",
|
|
Policy: "two_factor",
|
|
},
|
|
},
|
|
})
|
|
s.mock.Ctx.Request.SetBodyString(`{
|
|
"username": "test",
|
|
"password": "hello",
|
|
"keepMeLoggedIn": false
|
|
}`)
|
|
FirstFactorPost(s.mock.Ctx)
|
|
|
|
// Respond with 200.
|
|
s.mock.Assert200OK(s.T(), nil)
|
|
}
|
|
|
|
func TestFirstFactorSuite(t *testing.T) {
|
|
suite.Run(t, new(FirstFactorSuite))
|
|
suite.Run(t, new(FirstFactorRedirectionSuite))
|
|
}
|