package handlers import ( "fmt" "net" "net/url" "regexp" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/utils" ) var verifyGetCfg = schema.AuthenticationBackend{ RefreshInterval: schema.RefreshIntervalDefault, LDAP: &schema.LDAPAuthenticationBackend{}, } func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-URI", "/abc") originalURL, err := mock.Ctx.GetOriginalURL() assert.NoError(t, err) expectedURL, err := url.ParseRequestURI("https://home.example.com/abc") assert.NoError(t, err) assert.Equal(t, expectedURL, originalURL) } func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() _, err := mock.Ctx.GetOriginalURL() assert.Error(t, err) assert.Equal(t, "Missing header X-Forwarded-Host", err.Error()) } func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") _, err := mock.Ctx.GetOriginalURL() assert.Error(t, err) assert.Equal(t, "Missing header X-Forwarded-Host", err.Error()) } func TestShouldRaiseWhenXForwardedProtoIsNotParsable(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,") mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local") _, err := mock.Ctx.GetOriginalURL() assert.Error(t, err) assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local/: parse \"!:;;:,://myhost.local/\": invalid URI for request", err.Error()) } func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local") mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,") _, err := mock.Ctx.GetOriginalURL() require.Error(t, err) assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse \"https://myhost.local!:;;:,\": invalid port \":,\" after host", err.Error()) } // Test parseBasicAuth. func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) { _, _, err := parseBasicAuth(headerProxyAuthorization, "alzefzlfzemjfej==") assert.Error(t, err) assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error()) } func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) { _, _, err := parseBasicAuth(headerProxyAuthorization, "Basic alzefzlfzemjfej==") assert.Error(t, err) assert.Equal(t, "illegal base64 data at input byte 16", err.Error()) } func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) { // The decoded format should be user:password. _, _, err := parseBasicAuth(headerProxyAuthorization, "Basic am9obiBwYXNzd29yZA==") assert.Error(t, err) assert.Equal(t, "format of Proxy-Authorization header must be user:password", err.Error()) } func TestShouldUseProvidedHeaderName(t *testing.T) { // The decoded format should be user:password. _, _, err := parseBasicAuth([]byte("HeaderName"), "") assert.Error(t, err) assert.Equal(t, "Basic prefix not found in HeaderName header", err.Error()) } func TestShouldReturnUsernameAndPassword(t *testing.T) { // the decoded format should be user:password. user, password, err := parseBasicAuth(headerProxyAuthorization, "Basic am9objpwYXNzd29yZA==") assert.NoError(t, err) assert.Equal(t, "john", user) assert.Equal(t, "password", password) } // Test isTargetURLAuthorized. func TestShouldCheckAuthorizationMatching(t *testing.T) { type Rule struct { Policy string AuthLevel authentication.Level ExpectedMatching authorizationMatching } rules := []Rule{ {"bypass", authentication.NotAuthenticated, Authorized}, {"bypass", authentication.OneFactor, Authorized}, {"bypass", authentication.TwoFactor, Authorized}, {"one_factor", authentication.NotAuthenticated, NotAuthorized}, {"one_factor", authentication.OneFactor, Authorized}, {"one_factor", authentication.TwoFactor, Authorized}, {"two_factor", authentication.NotAuthenticated, NotAuthorized}, {"two_factor", authentication.OneFactor, NotAuthorized}, {"two_factor", authentication.TwoFactor, Authorized}, {"deny", authentication.NotAuthenticated, Forbidden}, {"deny", authentication.OneFactor, Forbidden}, {"deny", authentication.TwoFactor, Forbidden}, } u, _ := url.ParseRequestURI("https://test.example.com") for _, rule := range rules { authorizer := authorization.NewAuthorizer(&schema.Configuration{ AccessControl: schema.AccessControlConfiguration{ DefaultPolicy: "deny", Rules: []schema.ACLRule{{ Domains: []string{"test.example.com"}, Policy: rule.Policy, }}, }}) username := "" if rule.AuthLevel > authentication.NotAuthenticated { username = testUsername } matching := isTargetURLAuthorized(authorizer, *u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel) assert.Equal(t, rule.ExpectedMatching, matching, "policy=%s, authLevel=%v, expected=%v, actual=%v", rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching) } } // Test verifyBasicAuth. func TestShouldVerifyWrongCredentials(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(false, nil) _, _, _, _, _, err := verifyBasicAuth(mock.Ctx, headerProxyAuthorization, []byte("Basic am9objpwYXNzd29yZA==")) assert.Error(t, err) } type BasicAuthorizationSuite struct { suite.Suite } func NewBasicAuthorizationSuite() *BasicAuthorizationSuite { return &BasicAuthorizationSuite{} } func (s *BasicAuthorizationSuite) TestShouldNotBeAbleToParseBasicAuth() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpaaaaaaaaaaaaaaaa") mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldApplyDefaultPolicy() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfBypassDomain() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfOneFactorDomain() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfTwoFactorDomain() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfDenyDomain() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgOk() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") 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) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode()) } func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingNoHeader() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body())) assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate")) assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate"))) } func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingEmptyHeader() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("Authorization", "") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body())) assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate")) assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate"))) } func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongPassword() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(false, nil) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body())) assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate")) assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate"))) } func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongHeader() { mock := mocks.NewMockAutheliaCtx(s.T()) defer mock.Close() mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode()) assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body())) assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate")) assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate"))) } func TestShouldVerifyAuthorizationsUsingBasicAuth(t *testing.T) { suite.Run(t, NewBasicAuthorizationSuite()) } func TestShouldVerifyWrongCredentialsInBasicAuth(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("wrongpass")). Return(false, nil) mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=") mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d", "https://test.example.com", actualStatus, expStatus) } func TestShouldRedirectWithGroups(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{ AccessControl: schema.AccessControlConfiguration{ DefaultPolicy: "deny", Rules: []schema.ACLRule{ { Domains: []string{"app.example.com"}, Policy: "one_factor", Resources: []regexp.Regexp{ *regexp.MustCompile(`^/code-(?P\w+)([/?].*)?$`), }, }, }, }, }) mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https") mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/code-test/login") mock.Ctx.Request.SetRequestURI("/api/verify/?rd=https://auth.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) } func TestShouldVerifyFailingPasswordCheckingInBasicAuth(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("wrongpass")). Return(false, fmt.Errorf("Failed")) mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=") mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d", "https://test.example.com", actualStatus, expStatus) } func TestShouldVerifyFailingDetailsFetchingInBasicAuth(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.UserProviderMock.EXPECT(). CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). Return(true, nil) mock.UserProviderMock.EXPECT(). GetDetails(gomock.Eq("john")). Return(nil, fmt.Errorf("Failed")) mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==") mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d", "https://test.example.com", actualStatus, expStatus) } func TestShouldNotCrashOnEmptyEmail(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.Emails = nil userSession.AuthenticationLevel = authentication.OneFactor userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) fmt.Printf("Time is %v\n", userSession.RefreshTTL) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) expStatus, actualStatus := 200, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d", "https://bypass.example.com", actualStatus, expStatus) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-Email")) } type Pair struct { URL string Username string Emails []string AuthenticationLevel authentication.Level ExpectedStatusCode int } func (p Pair) String() string { return fmt.Sprintf("url=%s, username=%s, auth_lvl=%d, exp_status=%d", p.URL, p.Username, p.AuthenticationLevel, p.ExpectedStatusCode) } //nolint:gocyclo // This is a test. func TestShouldRedirectAuthorizations(t *testing.T) { testCases := []struct { name string method, originalURL, autheliaURL string expected int }{ {"ShouldReturnFoundMethodNone", "", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound}, {"ShouldReturnFoundMethodGET", "GET", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound}, {"ShouldReturnFoundMethodOPTIONS", "OPTIONS", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound}, {"ShouldReturnSeeOtherMethodPOST", "POST", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther}, {"ShouldReturnSeeOtherMethodPATCH", "PATCH", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther}, {"ShouldReturnSeeOtherMethodPUT", "PUT", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther}, {"ShouldReturnSeeOtherMethodDELETE", "DELETE", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther}, {"ShouldReturnUnauthorizedBadDomain", "GET", "https://one-factor.example.com/", "https://auth.notexample.com/", fasthttp.StatusUnauthorized}, } handler := VerifyGET(verifyGetCfg) for _, tc := range testCases { var ( suffix string xhr bool ) for i := 0; i < 2; i++ { switch i { case 0: suffix += "QueryParameter" default: suffix += "RequestHeader" } for j := 0; j < 2; j++ { switch j { case 0: xhr = false case 1: xhr = true suffix += "XHR" } t.Run(tc.name+suffix, func(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) autheliaURL, err := url.ParseRequestURI(tc.autheliaURL) require.NoError(t, err) originalURL, err := url.ParseRequestURI(tc.originalURL) require.NoError(t, err) if xhr { mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest") } var rm string if tc.method != "" { rm = fmt.Sprintf("&rm=%s", tc.method) mock.Ctx.Request.Header.Set("X-Forwarded-Method", tc.method) } mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.Header.Set("X-Original-URL", originalURL.String()) if i == 0 { mock.Ctx.Request.SetRequestURI(fmt.Sprintf("/?rd=%s", url.QueryEscape(autheliaURL.String()))) } else { mock.Ctx.Request.Header.Set("X-Authelia-URL", autheliaURL.String()) } handler(mock.Ctx) if xhr && tc.expected != fasthttp.StatusUnauthorized { assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) } else { assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode()) } switch { case xhr && tc.expected != fasthttp.StatusUnauthorized: href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm)) assert.Equal(t, fmt.Sprintf("%d %s", href, fasthttp.StatusUnauthorized, fasthttp.StatusMessage(fasthttp.StatusUnauthorized)), string(mock.Ctx.Response.Body())) case tc.expected >= fasthttp.StatusMultipleChoices && tc.expected < fasthttp.StatusBadRequest: href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm)) assert.Equal(t, fmt.Sprintf("%d %s", href, tc.expected, fasthttp.StatusMessage(tc.expected)), string(mock.Ctx.Response.Body())) case tc.expected < fasthttp.StatusMultipleChoices: assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body())) default: assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body())) } }) } } } } func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) { testCases := []Pair{ // should apply default policy. {"https://test.example.com", "", nil, authentication.NotAuthenticated, 403}, {"https://bypass.example.com", "", nil, authentication.NotAuthenticated, 200}, {"https://one-factor.example.com", "", nil, authentication.NotAuthenticated, 401}, {"https://two-factor.example.com", "", nil, authentication.NotAuthenticated, 401}, {"https://deny.example.com", "", nil, authentication.NotAuthenticated, 403}, {"https://test.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 403}, {"https://bypass.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 200}, {"https://one-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 200}, {"https://two-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 401}, {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 403}, {"https://test.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403}, {"https://bypass.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200}, {"https://one-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200}, {"https://two-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200}, {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403}, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.String(), func(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = testCase.Username userSession.Emails = testCase.Emails userSession.AuthenticationLevel = testCase.AuthenticationLevel userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL) VerifyGET(verifyGetCfg)(mock.Ctx) expStatus, actualStatus := testCase.ExpectedStatusCode, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d", testCase.URL, testCase.AuthenticationLevel, actualStatus, expStatus) if testCase.ExpectedStatusCode == 200 && testCase.Username != "" { assert.Equal(t, []byte(testCase.Username), mock.Ctx.Response.Header.Peek("Remote-User")) assert.Equal(t, []byte("john.doe@example.com"), mock.Ctx.Response.Header.Peek("Remote-Email")) } else { assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User")) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-Email")) } }) } } func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() clock := mocks.TestingClock{} clock.Set(time.Now()) past := clock.Now().Add(-1 * time.Hour) mock.Ctx.Configuration.Session.Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) // The session has been destroyed. newUserSession := mock.Ctx.GetSession() assert.Equal(t, "", newUserSession.Username) assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel) // Check the inactivity timestamp has been updated to current time in the new session. assert.Equal(t, clock.Now().Unix(), newUserSession.LastActivity) } func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() clock := mocks.TestingClock{} clock.Set(time.Now()) mock.Ctx.Configuration.Session.Inactivity = time.Second * 10 // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = clock.Now().Add(-1 * time.Hour).Unix() err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) // The session has been destroyed. newUserSession := mock.Ctx.GetSession() assert.Equal(t, "", newUserSession.Username) assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel) } func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Inactivity = testInactivity userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.Emails = []string{"john.doe@example.com"} userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = 0 userSession.KeepMeLoggedIn = true userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) // Check the session is still active. newUserSession := mock.Ctx.GetSession() assert.Equal(t, "john", newUserSession.Username) assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel) // Check the inactivity timestamp is set to 0 in case remember me is checked. assert.Equal(t, int64(0), newUserSession.LastActivity) } func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Inactivity = testInactivity past := mock.Clock.Now().Add(-1 * time.Hour) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.Emails = []string{"john.doe@example.com"} userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) // The session has been destroyed. newUserSession := mock.Ctx.GetSession() assert.Equal(t, "john", newUserSession.Username) assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel) // Check the inactivity timestamp has been updated to current time in the new session. assert.Equal(t, mock.Clock.Now().Unix(), newUserSession.LastActivity) } // In the case of Traefik and Nginx ingress controller in Kube, the response to an inactive // session is 302 instead of 401. func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() clock := mocks.TestingClock{} clock.Set(time.Now()) mock.Ctx.Configuration.Session.Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) past := clock.Now().Add(-1 * time.Hour) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, "302 Found", string(mock.Ctx.Response.Body())) assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) // Check the inactivity timestamp has been updated to current time in the new session. newUserSession := mock.Ctx.GetSession() assert.Equal(t, clock.Now().Unix(), newUserSession.LastActivity) } func TestShouldRedirectWithCorrectStatusCodeBasedOnRequestMethod(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, "302 Found", string(mock.Ctx.Response.Body())) assert.Equal(t, 302, mock.Ctx.Response.StatusCode()) mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, "303 See Other", string(mock.Ctx.Response.Body())) assert.Equal(t, 303, mock.Ctx.Response.StatusCode()) } func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) mock.Ctx.Configuration.Session.Inactivity = testInactivity past := mock.Clock.Now().Add(-1 * time.Hour) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) // The resource if forbidden. assert.Equal(t, 403, mock.Ctx.Response.StatusCode()) // Check the inactivity timestamp has been updated to current time in the new session. newUserSession := mock.Ctx.GetSession() assert.Equal(t, mock.Clock.Now().Unix(), newUserSession.LastActivity) } func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.NotAuthenticated userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.SetHost("mydomain.com") mock.Ctx.Request.SetRequestURI("/?rd=https://auth.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, "302 Found", string(mock.Ctx.Response.Body())) } func TestShouldURLEncodeRedirectionHeader(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.NotAuthenticated userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Authelia-URL", "https://auth.example.com") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.SetHost("mydomain.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, "302 Found", string(mock.Ctx.Response.Body())) } func TestIsDomainProtected(t *testing.T) { GetURL := func(u string) *url.URL { x, err := url.ParseRequestURI(u) require.NoError(t, err) return x } assert.True(t, isURLUnderProtectedDomain( GetURL("http://mytest.example.com/abc/?query=abc"), "example.com")) assert.True(t, isURLUnderProtectedDomain( GetURL("http://example.com/abc/?query=abc"), "example.com")) assert.True(t, isURLUnderProtectedDomain( GetURL("https://mytest.example.com/abc/?query=abc"), "example.com")) // Cookies readable by a service on a machine is also readable by a service on the same machine // with a different port as mentioned in https://tools.ietf.org/html/rfc6265#section-8.5. assert.True(t, isURLUnderProtectedDomain( GetURL("https://mytest.example.com:8080/abc/?query=abc"), "example.com")) } func TestSchemeIsHTTPS(t *testing.T) { GetURL := func(u string) *url.URL { x, err := url.ParseRequestURI(u) require.NoError(t, err) return x } assert.False(t, isSchemeHTTPS( GetURL("http://mytest.example.com/abc/?query=abc"))) assert.False(t, isSchemeHTTPS( GetURL("ws://mytest.example.com/abc/?query=abc"))) assert.False(t, isSchemeHTTPS( GetURL("wss://mytest.example.com/abc/?query=abc"))) assert.True(t, isSchemeHTTPS( GetURL("https://mytest.example.com/abc/?query=abc"))) } func TestSchemeIsWSS(t *testing.T) { GetURL := func(u string) *url.URL { x, err := url.ParseRequestURI(u) require.NoError(t, err) return x } assert.False(t, isSchemeWSS( GetURL("ws://mytest.example.com/abc/?query=abc"))) assert.False(t, isSchemeWSS( GetURL("http://mytest.example.com/abc/?query=abc"))) assert.False(t, isSchemeWSS( GetURL("https://mytest.example.com/abc/?query=abc"))) assert.True(t, isSchemeWSS( GetURL("wss://mytest.example.com/abc/?query=abc"))) } func TestShouldNotRefreshUserGroupsFromBackend(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() // Setup pointer to john so we can adjust it during the test. user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } cfg := verifyGetCfg cfg.RefreshInterval = "disable" verifyGet := VerifyGET(cfg) mock.UserProviderMock.EXPECT().GetDetails("john").Times(0) clock := mocks.TestingClock{} clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = clock.Now().Unix() userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Check Refresh TTL has not been updated. userSession = mock.Ctx.GetSession() // Check user groups are correct. require.Len(t, userSession.Groups, len(user.Groups)) assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix()) assert.Equal(t, "admin", userSession.Groups[0]) assert.Equal(t, "users", userSession.Groups[1]) mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Check admin group is not removed from the session. userSession = mock.Ctx.GetSession() assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix()) require.Len(t, userSession.Groups, 2) assert.Equal(t, "admin", userSession.Groups[0]) assert.Equal(t, "users", userSession.Groups[1]) } func TestShouldNotRefreshUserGroupsFromBackendWhenDisabled(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() // Setup user john. user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } mock.UserProviderMock.EXPECT().GetDetails("john").Times(0) clock := mocks.TestingClock{} clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = clock.Now().Unix() userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") config := verifyGetCfg config.RefreshInterval = schema.ProfileRefreshDisabled VerifyGET(config)(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Session time should NOT have been updated, it should still have a refresh TTL 1 minute in the past. userSession = mock.Ctx.GetSession() assert.Equal(t, clock.Now().Add(-1*time.Minute).Unix(), userSession.RefreshTTL.Unix()) } func TestShouldDestroySessionWhenUserNotExist(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() // Setup user john. user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1) clock := mocks.TestingClock{} clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = clock.Now().Unix() userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Session time should NOT have been updated, it should still have a refresh TTL 1 minute in the past. userSession = mock.Ctx.GetSession() assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) // Simulate a Deleted User. userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute) err = mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.UserProviderMock.EXPECT().GetDetails("john").Return(nil, authentication.ErrUserNotFound).Times(1) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, 401, mock.Ctx.Response.StatusCode()) userSession = mock.Ctx.GetSession() assert.Equal(t, "", userSession.Username) assert.Equal(t, authentication.NotAuthenticated, userSession.AuthenticationLevel) } func TestShouldGetRemovedUserGroupsFromBackend(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() // Setup pointer to john so we can adjust it during the test. user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } verifyGet := VerifyGET(verifyGetCfg) mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2) clock := mocks.TestingClock{} clock.Set(time.Now()) userSession := mock.Ctx.GetSession() userSession.Username = user.Username userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = clock.Now().Unix() userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute) userSession.Groups = user.Groups userSession.Emails = user.Emails userSession.KeepMeLoggedIn = true err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Request should get refresh settings and new user details. mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Check Refresh TTL has been updated since admin.example.com has a group subject and refresh is enabled. userSession = mock.Ctx.GetSession() // Check user groups are correct. require.Len(t, userSession.Groups, len(user.Groups)) assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) assert.Equal(t, "admin", userSession.Groups[0]) assert.Equal(t, "users", userSession.Groups[1]) // Remove the admin group, and force the next request to refresh. user.Groups = []string{"users"} userSession.RefreshTTL = clock.Now().Add(-1 * time.Second) err = mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com") verifyGet(mock.Ctx) assert.Equal(t, 403, mock.Ctx.Response.StatusCode()) // Check admin group is removed from the session. userSession = mock.Ctx.GetSession() assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) require.Len(t, userSession.Groups, 1) assert.Equal(t, "users", userSession.Groups[0]) } func TestShouldGetAddedUserGroupsFromBackend(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) // Setup pointer to john so we can adjust it during the test. user := &authentication.UserDetails{ Username: "john", Groups: []string{ "admin", "users", }, Emails: []string{ "john@example.com", }, } mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1) verifyGet := VerifyGET(verifyGetCfg) mock.Clock.Set(time.Now()) userSession := mock.Ctx.GetSession() 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 err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") verifyGet(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com") verifyGet(mock.Ctx) assert.Equal(t, 403, mock.Ctx.Response.StatusCode()) // Check Refresh TTL has been updated since grafana.example.com has a group subject and refresh is enabled. userSession = mock.Ctx.GetSession() // Check user groups are correct. require.Len(t, userSession.Groups, len(user.Groups)) assert.Equal(t, mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) assert.Equal(t, "admin", userSession.Groups[0]) assert.Equal(t, "users", userSession.Groups[1]) // Add the grafana group, and force the next request to refresh. user.Groups = append(user.Groups, "grafana") userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Second) err = mock.Ctx.SaveSession(userSession) require.NoError(t, err) // Reset otherwise we get the last 403 when we check the Response. Is there a better way to do this? mock.Close() mock = mocks.NewMockAutheliaCtx(t) defer mock.Close() err = mock.Ctx.SaveSession(userSession) assert.NoError(t, err) mock.Clock.Set(time.Now()) gomock.InOrder( mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1), ) mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, 200, mock.Ctx.Response.StatusCode()) // Check admin group is removed from the session. userSession = mock.Ctx.GetSession() assert.Equal(t, true, userSession.KeepMeLoggedIn) assert.Equal(t, authentication.TwoFactor, userSession.AuthenticationLevel) assert.Equal(t, mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix()) require.Len(t, userSession.Groups, 3) assert.Equal(t, "admin", userSession.Groups[0]) assert.Equal(t, "users", userSession.Groups[1]) assert.Equal(t, "grafana", userSession.Groups[2]) } func TestShouldCheckValidSessionUsernameHeaderAndReturn200(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) expectedStatusCode := 200 userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.OneFactor userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, testUsername) VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) assert.Equal(t, "", string(mock.Ctx.Response.Body())) } func TestShouldCheckInvalidSessionUsernameHeaderAndReturn401(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) expectedStatusCode := 401 userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.OneFactor userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, "root") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode()) assert.Equal(t, "401 Unauthorized", string(mock.Ctx.Response.Body())) } func TestGetProfileRefreshSettings(t *testing.T) { cfg := verifyGetCfg refresh, interval := getProfileRefreshSettings(cfg) assert.Equal(t, true, refresh) assert.Equal(t, 5*time.Minute, interval) cfg.RefreshInterval = schema.ProfileRefreshDisabled refresh, interval = getProfileRefreshSettings(cfg) assert.Equal(t, false, refresh) assert.Equal(t, time.Duration(0), interval) cfg.RefreshInterval = schema.ProfileRefreshAlways refresh, interval = getProfileRefreshSettings(cfg) assert.Equal(t, true, refresh) assert.Equal(t, time.Duration(0), interval) } func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() clock := mocks.TestingClock{} clock.Set(time.Now()) past := clock.Now().Add(-1 * time.Hour) mock.Ctx.Configuration.Session.Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) userSession := mock.Ctx.GetSession() userSession.Username = testUsername userSession.AuthenticationLevel = authentication.TwoFactor userSession.LastActivity = past.Unix() err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) // Should respond 200 OK. mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) assert.Nil(t, mock.Ctx.Response.Header.Peek("Location")) // Should respond 302 Found. mock.Ctx.QueryArgs().Add("rd", "https://login.example.com") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) assert.Equal(t, "https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&rm=GET", string(mock.Ctx.Response.Header.Peek("Location"))) // Should respond 401 Unauthorized. mock.Ctx.QueryArgs().Del("rd") mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") VerifyGET(verifyGetCfg)(mock.Ctx) assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) assert.Nil(t, mock.Ctx.Response.Header.Peek("Location")) } func TestIsSessionInactiveTooLong(t *testing.T) { testCases := []struct { name string have *session.UserSession now time.Time inactivity time.Duration expected bool }{ { name: "ShouldNotBeInactiveTooLong", have: &session.UserSession{Username: "john", LastActivity: 1656994960}, now: time.Unix(1656994970, 0), inactivity: time.Second * 90, expected: false, }, { name: "ShouldNotBeInactiveTooLongIfAnonymous", have: &session.UserSession{Username: "", LastActivity: 1656994960}, now: time.Unix(1656994990, 0), inactivity: time.Second * 20, expected: false, }, { name: "ShouldNotBeInactiveTooLongIfRemembered", have: &session.UserSession{Username: "john", LastActivity: 1656994960, KeepMeLoggedIn: true}, now: time.Unix(1656994990, 0), inactivity: time.Second * 20, expected: false, }, { name: "ShouldNotBeInactiveTooLongIfDisabled", have: &session.UserSession{Username: "john", LastActivity: 1656994960}, now: time.Unix(1656994990, 0), inactivity: time.Second * 0, expected: false, }, { name: "ShouldBeInactiveTooLong", have: &session.UserSession{Username: "john", LastActivity: 1656994960}, now: time.Unix(4656994990, 0), inactivity: time.Second * 1, expected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := mocks.NewMockAutheliaCtx(t) defer ctx.Close() ctx.Ctx.Configuration.Session.Inactivity = tc.inactivity ctx.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil) ctx.Clock.Set(tc.now) ctx.Ctx.Clock = &ctx.Clock actual := isSessionInactiveTooLong(ctx.Ctx, tc.have, tc.have.Username == "") assert.Equal(t, tc.expected, actual) }) } }