package handlers import ( "fmt" "net/url" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/valyala/fasthttp" "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/session" "github.com/authelia/authelia/v4/internal/utils" ) type AuthzSuite struct { suite.Suite implementation AuthzImplementation builder *AuthzBuilder setRequest func(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept bool, xhr bool) } func (s *AuthzSuite) GetMock(config *schema.Configuration, targetURI *url.URL, session *session.UserSession) *mocks.MockAutheliaCtx { mock := mocks.NewMockAutheliaCtx(s.T()) if session != nil { domain := mock.Ctx.GetTargetURICookieDomain(targetURI) provider, err := mock.Ctx.GetCookieDomainSessionProvider(domain) s.Require().NoError(err) s.Require().NoError(provider.SaveSession(mock.Ctx.RequestCtx, *session)) } return mock } func (s *AuthzSuite) RequireParseRequestURI(rawURL string) *url.URL { u, err := url.ParseRequestURI(rawURL) s.Require().NoError(err) return u } func (s *AuthzSuite) ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock *mocks.MockAutheliaCtx) { for i, cookie := range mock.Ctx.Configuration.Session.Cookies { mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) } mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) } func (s *AuthzSuite) Builder() (builder *AuthzBuilder) { if s.builder != nil { return s.builder } switch s.implementation { case AuthzImplExtAuthz: return NewAuthzBuilder().WithImplementationExtAuthz() case AuthzImplForwardAuth: return NewAuthzBuilder().WithImplementationForwardAuth() case AuthzImplAuthRequest: return NewAuthzBuilder().WithImplementationAuthRequest() case AuthzImplLegacy: return NewAuthzBuilder().WithImplementationLegacy() } s.T().FailNow() return } func (s *AuthzSuite) TestShouldNotBeAbleToParseBasicAuth() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://test.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpaaaaaaaaaaaaaaaa") authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) default: s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) } } func (s *AuthzSuite) TestShouldApplyDefaultPolicy() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://test.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldDenyObject() { if s.setRequest == nil { s.T().Skip() } testCases := []struct { name string value string }{ { "NotProtected", "https://test.not-a-protected-domain.com", }, { "Insecure", "http://test.example.com", }, } authz := s.Builder().Build() for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI(tc.value) s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplLegacy: assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) default: assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) } }) } } func (s *AuthzSuite) TestShouldApplyPolicyOfBypassDomain() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldVerifyFailureToGetDetailsUsingBasicScheme() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") gomock.InOrder( mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil), mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(nil, fmt.Errorf("generic failure")), ) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) default: s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) } } func (s *AuthzSuite) TestShouldNotFailOnMissingEmail() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Clock.Set(time.Now()) s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.DisplayName = "John Smith" userSession.Groups = []string{"abc,123"} userSession.Emails = nil userSession.AuthenticationLevel = authentication.OneFactor userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) s.Equal(testUsername, string(mock.Ctx.Response.Header.PeekBytes(headerRemoteUser))) s.Equal("John Smith", string(mock.Ctx.Response.Header.PeekBytes(headerRemoteName))) s.Equal("abc,123", string(mock.Ctx.Response.Header.PeekBytes(headerRemoteGroups))) } func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomain() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldHandleAnyCaseSchemeParameter() { if s.setRequest == nil { s.T().Skip() } testCases := []struct { name, scheme string }{ {"Standard", "Basic"}, {"LowerCase", "basic"}, {"UpperCase", "BASIC"}, {"MixedCase", "BaSIc"}, } authz := s.Builder().Build() for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, fmt.Sprintf("%s am9objpwYXNzd29yZA==", tc.scheme)) mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) }) } } func (s *AuthzSuite) TestShouldApplyPolicyOfTwoFactorDomain() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) default: s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) } } func (s *AuthzSuite) TestShouldApplyPolicyOfDenyDomain() { if s.setRequest == nil { s.T().Skip() } authz := s.Builder().Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://deny.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomainWithAuthorizationHeader() { if s.setRequest == nil { s.T().Skip() } // Equivalent of TestShouldVerifyAuthBasicArgOk. builder := NewAuthzBuilder().WithImplementationLegacy() builder = builder.WithStrategies( NewHeaderAuthorizationAuthnStrategy(), NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(), NewCookieSessionAuthnStrategy(builder.config.RefreshInterval), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(&authentication.UserDetails{ Emails: []string{"john@example.com"}, Groups: []string{"dev", "admins"}, }, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldHandleAuthzWithoutHeaderNoCookie() { if s.setRequest == nil { s.T().Skip() } // Equivalent of TestShouldVerifyAuthBasicArgFailingNoHeader. builder := NewAuthzBuilder().WithImplementationLegacy() builder = builder.WithStrategies( NewHeaderAuthorizationAuthnStrategy(), NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldHandleAuthzWithEmptyAuthorizationHeader() { if s.setRequest == nil { s.T().Skip() } // Equivalent of TestShouldVerifyAuthBasicArgFailingEmptyHeader. builder := NewAuthzBuilder().WithImplementationLegacy() builder = builder.WithStrategies( NewHeaderAuthorizationAuthnStrategy(), NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "") authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldHandleAuthzWithAuthorizationHeaderInvalidPassword() { if s.setRequest == nil { s.T().Skip() } // Equivalent of TestShouldVerifyAuthBasicArgFailingWrongPassword. builder := NewAuthzBuilder().WithImplementationLegacy() builder = builder.WithStrategies( NewHeaderAuthorizationAuthnStrategy(), NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "Basic am9objpwYXNzd29yZA==") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(false, nil) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldHandleAuthzWithIncorrectAuthHeader() { // TestShouldVerifyAuthBasicArgFailingWrongHeader. if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewHeaderAuthorizationAuthnStrategy(), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) } func (s *AuthzSuite) TestShouldDestroySessionWhenInactiveForTooLong() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) past := mock.Clock.Now().Add(-1 * time.Hour) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal("", userSession.Username) s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) } func (s *AuthzSuite) TestShouldNotDestroySessionWhenInactiveForTooLongRememberMe() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = 0 userSession.KeepMeLoggedIn = true userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(testUsername, userSession.Username) s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel) s.Equal(int64(0), userSession.LastActivity) } func (s *AuthzSuite) TestShouldNotDestroySessionWhenNotInactiveForTooLong() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) last := mock.Clock.Now().Add(-1 * time.Second) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = last.Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(testUsername, userSession.Username) s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) } func (s *AuthzSuite) TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://deny.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) last := mock.Clock.Now().Add(-3 * time.Second) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = last.Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(testUsername, userSession.Username) s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) } func (s *AuthzSuite) TestShouldNotRefreshUserDetailsFromBackendWhenRefreshDisabled() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(-1 * time.Second), ) authz := builder.Build() user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Clock.Set(time.Now()) mock.Ctx.Clock = &mock.Clock mock.Ctx.Configuration.AuthenticationBackend.RefreshInterval = schema.ProfileRefreshDisabled mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = user.Username userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = mock.Clock.Now().Unix() s.Require().NoError(mock.Ctx.SaveSession(userSession)) mock.UserProviderMock.EXPECT().GetDetails("john").Times(0) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) targetURI = s.RequireParseRequestURI("https://admin.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(user.Username, userSession.Username) s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) s.Require().Len(userSession.Groups, 2) s.Equal("admin", userSession.Groups[0]) s.Equal("users", userSession.Groups[1]) s.Equal(utils.RFC3339Zero, userSession.RefreshTTL.Unix()) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(user.Username, userSession.Username) s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) s.Require().Len(userSession.Groups, 2) s.Equal("admin", userSession.Groups[0]) s.Equal("users", userSession.Groups[1]) s.Equal(utils.RFC3339Zero, userSession.RefreshTTL.Unix()) } func (s *AuthzSuite) TestShouldDestroySessionWhenUserDoesNotExist() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(5 * time.Minute), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = mock.Clock.Now().Unix() userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true s.Require().NoError(mock.Ctx.SaveSession(userSession)) gomock.InOrder( mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), mock.UserProviderMock.EXPECT().GetDetails("john").Return(nil, authentication.ErrUserNotFound).Times(1), ) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) default: s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) } userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal("", userSession.Username) s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel) s.True(userSession.IsAnonymous()) } func (s *AuthzSuite) TestShouldUpdateRemovedUserGroupsFromBackendAndDeny() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(5 * time.Minute), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://admin.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = mock.Clock.Now().Unix() userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true s.Require().NoError(mock.Ctx.SaveSession(userSession)) gomock.InOrder( mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), ) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) s.Require().Len(userSession.Groups, 2) s.Require().Equal("admin", userSession.Groups[0]) s.Require().Equal("users", userSession.Groups[1]) user.Groups = []string{"users"} mock.Clock.Set(mock.Clock.Now().Add(6 * time.Minute)) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) s.Require().Len(userSession.Groups, 1) s.Require().Equal("users", userSession.Groups[0]) } func (s *AuthzSuite) TestShouldUpdateAddedUserGroupsFromBackendAndDeny() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(5 * time.Minute), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://admin.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) user := &authentication.UserDetails{ Username: "john", Groups: []string{ "users", }, Emails: []string{ "john@example.com", }, } userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = mock.Clock.Now().Unix() userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true s.Require().NoError(mock.Ctx.SaveSession(userSession)) gomock.InOrder( mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), ) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) s.Require().Len(userSession.Groups, 1) s.Require().Equal("users", userSession.Groups[0]) user.Groups = []string{"admin", "users"} mock.Clock.Set(mock.Clock.Now().Add(6 * time.Minute)) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) s.Require().Len(userSession.Groups, 2) s.Require().Equal("admin", userSession.Groups[0]) s.Require().Equal("users", userSession.Groups[1]) } func (s *AuthzSuite) TestShouldCheckValidSessionUsernameHeaderAndReturn200() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, testUsername) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.OneFactor userSession.LastActivity = mock.Clock.Now().Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal(testUsername, userSession.Username) s.Equal(authentication.OneFactor, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) } func (s *AuthzSuite) TestShouldCheckInvalidSessionUsernameHeaderAndReturn401AndDestroySession() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, "root") userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.OneFactor userSession.LastActivity = mock.Clock.Now().Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) default: s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) location := s.RequireParseRequestURI(mock.Ctx.Configuration.Session.Cookies[0].AutheliaURL.String()) if location.Path == "" { location.Path = "/" } query := location.Query() query.Set(queryArgRD, targetURI.String()) query.Set(queryArgRM, fasthttp.MethodGet) location.RawQuery = query.Encode() s.Equal(location.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) } userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal("", userSession.Username) s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) } func (s *AuthzSuite) TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Clock = &mock.Clock mock.Clock.Set(time.Now()) past := mock.Clock.Now().Add(-24 * time.Hour) mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) userSession, err := mock.Ctx.GetSession() s.Require().NoError(err) userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() s.Require().NoError(mock.Ctx.SaveSession(userSession)) authz.Handler(mock.Ctx) s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) userSession, err = mock.Ctx.GetSession() s.Require().NoError(err) s.Equal("", userSession.Username) s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel) s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity) targetURI = s.RequireParseRequestURI("https://two-factor.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) authz.Handler(mock.Ctx) switch s.implementation { case AuthzImplAuthRequest, AuthzImplLegacy: s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) default: s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) location := s.RequireParseRequestURI(mock.Ctx.Configuration.Session.Cookies[0].AutheliaURL.String()) if location.Path == "" { location.Path = "/" } query := location.Query() query.Set(queryArgRD, targetURI.String()) query.Set(queryArgRM, fasthttp.MethodGet) location.RawQuery = query.Encode() s.Equal(location.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) } } func (s *AuthzSuite) TestShouldFailToParsePortalURL() { if s.setRequest == nil { s.T().Skip() } builder := s.Builder() builder = builder.WithStrategies( NewCookieSessionAuthnStrategy(testInactivity), ) authz := builder.Build() mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) expected := fasthttp.StatusBadRequest switch s.implementation { case AuthzImplLegacy: expected = fasthttp.StatusUnauthorized mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, "JKL$#N%KJ#@$N") case AuthzImplForwardAuth, AuthzImplAuthRequest: mock.Ctx.RequestCtx.QueryArgs().Set("authelia_url", "JKL$#N%KJ#@$N") case AuthzImplExtAuthz: mock.Ctx.Request.Header.Set("X-Authelia-URL", "JKL$#N%KJ#@$N") } authz.Handler(mock.Ctx) s.Equal(expected, mock.Ctx.Response.StatusCode()) s.Equal(fmt.Sprintf("%d %s", expected, fasthttp.StatusMessage(expected)), string(mock.Ctx.Response.Body())) s.Equal("", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) s.Equal("text/plain; charset=utf-8", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderContentType))) } func setRequestXHRValues(ctx *middlewares.AutheliaCtx, accept, xhr bool) { if accept { ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8") } if xhr { ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest") } } type urlpair struct { TargetURI *url.URL AutheliaURI *url.URL }