2019-04-25 04:52:08 +07:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"net/url"
|
|
|
|
"testing"
|
2020-01-18 05:48:48 +07:00
|
|
|
"time"
|
2019-04-25 04:52:08 +07:00
|
|
|
|
2020-04-05 19:37:21 +07:00
|
|
|
"github.com/authelia/authelia/internal/session"
|
|
|
|
|
2019-04-25 04:52:08 +07:00
|
|
|
"github.com/golang/mock/gomock"
|
|
|
|
"github.com/stretchr/testify/assert"
|
2020-02-06 09:24:25 +07:00
|
|
|
"github.com/stretchr/testify/require"
|
2019-11-30 21:33:45 +07:00
|
|
|
"github.com/stretchr/testify/suite"
|
2020-04-05 19:37:21 +07:00
|
|
|
|
|
|
|
"github.com/authelia/authelia/internal/authentication"
|
|
|
|
"github.com/authelia/authelia/internal/authorization"
|
|
|
|
"github.com/authelia/authelia/internal/configuration/schema"
|
|
|
|
"github.com/authelia/authelia/internal/mocks"
|
2019-04-25 04:52:08 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
// Test getOriginalURL
|
|
|
|
func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
|
|
|
|
originalURL, err := getOriginalURL(mock.Ctx)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, expectedURL, originalURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(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")
|
|
|
|
originalURL, err := getOriginalURL(mock.Ctx)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, expectedURL, originalURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
|
|
|
|
_, err := getOriginalURL(mock.Ctx)
|
|
|
|
assert.Error(t, err)
|
2020-02-06 09:24:25 +07:00
|
|
|
assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse htt-ps//home?-.example.com: invalid URI for request", err.Error())
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
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 := getOriginalURL(mock.Ctx)
|
|
|
|
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 := getOriginalURL(mock.Ctx)
|
|
|
|
assert.Error(t, err)
|
2020-02-13 09:12:37 +07:00
|
|
|
assert.Equal(t, "Missing header X-Fowarded-Proto", err.Error())
|
2020-02-06 09:24:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
|
|
|
_, err := getOriginalURL(mock.Ctx)
|
|
|
|
assert.Error(t, err)
|
2020-02-13 09:12:37 +07:00
|
|
|
assert.Equal(t, "Missing header X-Fowarded-Host", err.Error())
|
2020-02-06 09:24:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldRaiseWhenXForwardedProtoIsNotParseable(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 := getOriginalURL(mock.Ctx)
|
|
|
|
assert.Error(t, err)
|
|
|
|
assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local: parse !:;;:,://myhost.local: invalid URI for request", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldRaiseWhenXForwardedURIIsNotParseable(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 := getOriginalURL(mock.Ctx)
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse https://myhost.local!:;;:,: invalid port \":,\" after host", err.Error())
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test parseBasicAuth
|
|
|
|
func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
|
|
|
|
_, _, err := parseBasicAuth("alzefzlfzemjfej==")
|
|
|
|
assert.Error(t, err)
|
2020-02-06 09:24:25 +07:00
|
|
|
assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
|
|
|
|
_, _, err := parseBasicAuth("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("Basic am9obiBwYXNzd29yZA==")
|
|
|
|
assert.Error(t, err)
|
2020-02-06 09:24:25 +07:00
|
|
|
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error())
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldReturnUsernameAndPassword(t *testing.T) {
|
|
|
|
// the decoded format should be user:password.
|
|
|
|
user, password, err := parseBasicAuth("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{
|
2020-04-09 08:05:17 +07:00
|
|
|
{"bypass", authentication.NotAuthenticated, Authorized},
|
|
|
|
{"bypass", authentication.OneFactor, Authorized},
|
|
|
|
{"bypass", authentication.TwoFactor, Authorized},
|
2019-04-25 04:52:08 +07:00
|
|
|
|
2020-04-09 08:05:17 +07:00
|
|
|
{"one_factor", authentication.NotAuthenticated, NotAuthorized},
|
|
|
|
{"one_factor", authentication.OneFactor, Authorized},
|
|
|
|
{"one_factor", authentication.TwoFactor, Authorized},
|
2019-04-25 04:52:08 +07:00
|
|
|
|
2020-04-09 08:05:17 +07:00
|
|
|
{"two_factor", authentication.NotAuthenticated, NotAuthorized},
|
|
|
|
{"two_factor", authentication.OneFactor, NotAuthorized},
|
|
|
|
{"two_factor", authentication.TwoFactor, Authorized},
|
2019-04-25 04:52:08 +07:00
|
|
|
|
2020-04-09 08:05:17 +07:00
|
|
|
{"deny", authentication.NotAuthenticated, NotAuthorized},
|
|
|
|
{"deny", authentication.OneFactor, Forbidden},
|
|
|
|
{"deny", authentication.TwoFactor, Forbidden},
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
url, _ := url.ParseRequestURI("https://test.example.com")
|
|
|
|
|
|
|
|
for _, rule := range rules {
|
|
|
|
authorizer := authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
|
|
|
DefaultPolicy: "deny",
|
2020-04-09 08:05:17 +07:00
|
|
|
Rules: []schema.ACLRule{{
|
2020-04-16 07:18:11 +07:00
|
|
|
Domains: []string{"test.example.com"},
|
|
|
|
Policy: rule.Policy,
|
2019-04-25 04:52:08 +07:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
|
|
|
|
username := ""
|
|
|
|
if rule.AuthLevel > authentication.NotAuthenticated {
|
|
|
|
username = "john"
|
|
|
|
}
|
|
|
|
|
|
|
|
matching := isTargetURLAuthorized(authorizer, *url, username, []string{}, net.ParseIP("127.0.0.1"), 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)
|
|
|
|
|
|
|
|
url, _ := url.ParseRequestURI("https://test.example.com")
|
|
|
|
_, _, _, err := verifyBasicAuth([]byte("Basic am9objpwYXNzd29yZA=="), *url, mock.Ctx)
|
|
|
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
type TestCase struct {
|
|
|
|
URL string
|
|
|
|
Authorization string
|
|
|
|
ExpectedStatusCode int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tc TestCase) String() string {
|
|
|
|
return fmt.Sprintf("url=%s, auth=%s, exp_status=%d", tc.URL, tc.Authorization, tc.ExpectedStatusCode)
|
|
|
|
}
|
|
|
|
|
2019-11-30 21:33:45 +07:00
|
|
|
type BasicAuthorizationSuite struct {
|
|
|
|
suite.Suite
|
|
|
|
}
|
2019-04-25 04:52:08 +07:00
|
|
|
|
2019-11-30 21:33:45 +07:00
|
|
|
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(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"},
|
2019-11-30 23:49:52 +07:00
|
|
|
Groups: []string{"dev", "admins"},
|
2019-11-30 21:33:45 +07:00
|
|
|
}, nil)
|
|
|
|
|
|
|
|
VerifyGet(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"},
|
2019-11-30 23:49:52 +07:00
|
|
|
Groups: []string{"dev", "admins"},
|
2019-11-30 21:33:45 +07:00
|
|
|
}, nil)
|
|
|
|
|
|
|
|
VerifyGet(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"},
|
2019-11-30 23:49:52 +07:00
|
|
|
Groups: []string{"dev", "admins"},
|
2019-11-30 21:33:45 +07:00
|
|
|
}, nil)
|
|
|
|
|
|
|
|
VerifyGet(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"},
|
2019-11-30 23:49:52 +07:00
|
|
|
Groups: []string{"dev", "admins"},
|
2019-11-30 21:33:45 +07:00
|
|
|
}, nil)
|
|
|
|
|
|
|
|
VerifyGet(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"},
|
2019-11-30 23:49:52 +07:00
|
|
|
Groups: []string{"dev", "admins"},
|
2019-11-30 21:33:45 +07:00
|
|
|
}, nil)
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldVerifyAuthorizationsUsingBasicAuth(t *testing.T) {
|
|
|
|
suite.Run(t, NewBasicAuthorizationSuite())
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
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(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 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(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(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)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Pair struct {
|
|
|
|
URL string
|
|
|
|
Username 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
|
|
|
|
testCases := []Pair{
|
2020-04-09 08:05:17 +07:00
|
|
|
{"https://test.example.com", "", authentication.NotAuthenticated, 401},
|
|
|
|
{"https://bypass.example.com", "", authentication.NotAuthenticated, 200},
|
|
|
|
{"https://one-factor.example.com", "", authentication.NotAuthenticated, 401},
|
|
|
|
{"https://two-factor.example.com", "", authentication.NotAuthenticated, 401},
|
|
|
|
{"https://deny.example.com", "", authentication.NotAuthenticated, 401},
|
|
|
|
|
|
|
|
{"https://test.example.com", "john", authentication.OneFactor, 403},
|
|
|
|
{"https://bypass.example.com", "john", authentication.OneFactor, 200},
|
|
|
|
{"https://one-factor.example.com", "john", authentication.OneFactor, 200},
|
|
|
|
{"https://two-factor.example.com", "john", authentication.OneFactor, 401},
|
|
|
|
{"https://deny.example.com", "john", authentication.OneFactor, 403},
|
|
|
|
|
|
|
|
{"https://test.example.com", "john", authentication.TwoFactor, 403},
|
|
|
|
{"https://bypass.example.com", "john", authentication.TwoFactor, 200},
|
|
|
|
{"https://one-factor.example.com", "john", authentication.TwoFactor, 200},
|
|
|
|
{"https://two-factor.example.com", "john", authentication.TwoFactor, 200},
|
|
|
|
{"https://deny.example.com", "john", authentication.TwoFactor, 403},
|
2019-04-25 04:52:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, testCase := range testCases {
|
|
|
|
testCase := testCase
|
|
|
|
t.Run(testCase.String(), func(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = testCase.Username
|
|
|
|
userSession.AuthenticationLevel = testCase.AuthenticationLevel
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2019-04-25 04:52:08 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL)
|
|
|
|
|
|
|
|
VerifyGet(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"))
|
|
|
|
} else {
|
|
|
|
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User"))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
clock := mocks.TestingClock{}
|
|
|
|
clock.Set(time.Now())
|
2020-04-25 06:29:36 +07:00
|
|
|
past := clock.Now().Add(-1 * time.Hour)
|
2020-01-18 05:48:48 +07:00
|
|
|
|
2020-04-05 19:37:21 +07:00
|
|
|
mock.Ctx.Configuration.Session.Inactivity = "10"
|
|
|
|
// Reload the session provider since the configuration is indirect
|
|
|
|
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session)
|
|
|
|
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
2020-04-25 06:29:36 +07:00
|
|
|
userSession.LastActivity = past.Unix()
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2020-04-05 19:37:21 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
// The session has been destroyed
|
|
|
|
newUserSession := mock.Ctx.GetSession()
|
|
|
|
assert.Equal(t, "", newUserSession.Username)
|
|
|
|
assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
|
2020-04-25 06:29:36 +07:00
|
|
|
|
|
|
|
// Check the inactivity timestamp has been updated to current time in the new session.
|
|
|
|
assert.Equal(t, clock.Now().Unix(), newUserSession.LastActivity)
|
2020-04-05 19:37:21 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
clock := mocks.TestingClock{}
|
|
|
|
clock.Set(time.Now())
|
|
|
|
|
|
|
|
mock.Ctx.Configuration.Session.Inactivity = "10s"
|
|
|
|
// Reload the session provider since the configuration is indirect
|
|
|
|
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session)
|
|
|
|
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
|
|
|
userSession.LastActivity = clock.Now().Add(-1 * time.Hour).Unix()
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
|
|
|
|
VerifyGet(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()
|
|
|
|
|
|
|
|
clock := mocks.TestingClock{}
|
|
|
|
clock.Set(time.Now())
|
|
|
|
|
2020-04-05 19:37:21 +07:00
|
|
|
mock.Ctx.Configuration.Session.Inactivity = "10"
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
2020-04-25 06:29:36 +07:00
|
|
|
userSession.LastActivity = 0
|
2020-01-18 05:48:48 +07:00
|
|
|
userSession.KeepMeLoggedIn = true
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
// The session has been destroyed
|
|
|
|
newUserSession := mock.Ctx.GetSession()
|
|
|
|
assert.Equal(t, "john", newUserSession.Username)
|
|
|
|
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
|
2020-04-25 06:29:36 +07:00
|
|
|
|
|
|
|
// Check the inactivity timestamp is set to 0 in case remember me is checked.
|
|
|
|
assert.Equal(t, int64(0), newUserSession.LastActivity)
|
2020-01-18 05:48:48 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
clock := mocks.TestingClock{}
|
|
|
|
clock.Set(time.Now())
|
|
|
|
|
2020-04-05 19:37:21 +07:00
|
|
|
mock.Ctx.Configuration.Session.Inactivity = "10"
|
2020-01-18 05:48:48 +07:00
|
|
|
|
2020-04-25 06:29:36 +07:00
|
|
|
past := clock.Now().Add(-1 * time.Hour)
|
|
|
|
|
2020-01-18 05:48:48 +07:00
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
2020-04-25 06:29:36 +07:00
|
|
|
userSession.LastActivity = past.Unix()
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2020-01-18 05:48:48 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
// The session has been destroyed
|
|
|
|
newUserSession := mock.Ctx.GetSession()
|
|
|
|
assert.Equal(t, "john", newUserSession.Username)
|
|
|
|
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
|
2020-04-25 06:29:36 +07:00
|
|
|
|
|
|
|
// Check the inactivity timestamp has been updated to current time in the new session.
|
|
|
|
assert.Equal(t, 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 = "10"
|
|
|
|
// Reload the session provider since the configuration is indirect
|
|
|
|
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session)
|
|
|
|
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
|
|
|
|
|
|
|
past := clock.Now().Add(-1 * time.Hour)
|
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
|
|
|
userSession.LastActivity = past.Unix()
|
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
|
|
|
|
|
|
|
mock.Ctx.QueryArgs().Add("rd", "https://login.example.com")
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
assert.Equal(t, "Found. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
|
|
|
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 TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
clock := mocks.TestingClock{}
|
|
|
|
clock.Set(time.Now())
|
|
|
|
|
|
|
|
mock.Ctx.Configuration.Session.Inactivity = "10"
|
|
|
|
|
|
|
|
past := clock.Now().Add(-1 * time.Hour)
|
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
|
|
|
userSession.LastActivity = past.Unix()
|
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com")
|
|
|
|
|
|
|
|
VerifyGet(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, clock.Now().Unix(), newUserSession.LastActivity)
|
2020-01-18 05:48:48 +07:00
|
|
|
}
|
2020-01-18 21:57:42 +07:00
|
|
|
|
|
|
|
func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
|
|
|
|
mock := mocks.NewMockAutheliaCtx(t)
|
|
|
|
defer mock.Close()
|
|
|
|
|
|
|
|
userSession := mock.Ctx.GetSession()
|
|
|
|
userSession.Username = "john"
|
|
|
|
userSession.AuthenticationLevel = authentication.NotAuthenticated
|
2020-04-22 10:33:14 +07:00
|
|
|
mock.Ctx.SaveSession(userSession) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
2020-01-18 21:57:42 +07:00
|
|
|
|
|
|
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
|
|
|
mock.Ctx.Request.SetHost("mydomain.com")
|
|
|
|
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
|
|
|
|
|
|
|
|
VerifyGet(mock.Ctx)
|
|
|
|
|
|
|
|
assert.Equal(t, "Found. Redirecting to https://auth.mydomain.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
|
|
|
string(mock.Ctx.Response.Body()))
|
|
|
|
}
|
2020-02-19 05:39:07 +07:00
|
|
|
|
|
|
|
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")))
|
2020-02-28 06:28:53 +07:00
|
|
|
assert.False(t, isSchemeHTTPS(
|
|
|
|
GetURL("ws://mytest.example.com/abc/?query=abc")))
|
|
|
|
assert.False(t, isSchemeHTTPS(
|
|
|
|
GetURL("wss://mytest.example.com/abc/?query=abc")))
|
2020-02-19 05:39:07 +07:00
|
|
|
assert.True(t, isSchemeHTTPS(
|
|
|
|
GetURL("https://mytest.example.com/abc/?query=abc")))
|
|
|
|
}
|
2020-02-28 06:28:53 +07:00
|
|
|
|
|
|
|
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")))
|
|
|
|
}
|