Rewrite and fix remaining suites in Go.

This commit is contained in:
Clement Michaud 2019-11-24 21:27:59 +01:00 committed by Clément Michaud
parent 373911d199
commit c78a732c6a
197 changed files with 2054 additions and 6517 deletions

1
.gitignore vendored
View File

@ -8,7 +8,6 @@ npm-debug.log*
# Coverage reports
coverage/
src/.baseDir.ts
.vscode/
*.swp

View File

@ -55,10 +55,6 @@ func runCommand(cmd string, args ...string) {
}
}
func installNpmPackages() {
runCommand("npm", "ci")
}
func checkCommandExist(cmd string) {
fmt.Print("Checking if '" + cmd + "' command is installed...")
command := exec.Command("bash", "-c", "command -v "+cmd)
@ -213,9 +209,6 @@ func Bootstrap(cobraCmd *cobra.Command, args []string) {
log.Fatal("GOPATH is not set")
}
bootstrapPrintln("Installing NPM packages for development...")
installNpmPackages()
bootstrapPrintln("Building development Docker images...")
buildHelperDockerImages()

View File

@ -16,6 +16,7 @@ import (
"github.com/clems4ever/authelia/internal/server"
"github.com/clems4ever/authelia/internal/session"
"github.com/clems4ever/authelia/internal/storage"
"github.com/clems4ever/authelia/internal/utils"
"github.com/sirupsen/logrus"
)
@ -51,12 +52,15 @@ func main() {
switch config.LogsLevel {
case "info":
logging.Logger().Info("Logging severity set to info")
logging.SetLevel(logrus.InfoLevel)
break
case "debug":
logging.Logger().Info("Logging severity set to debug")
logging.SetLevel(logrus.DebugLevel)
break
case "trace":
logging.Logger().Info("Logging severity set to trace")
logging.SetLevel(logrus.TraceLevel)
}
@ -90,9 +94,10 @@ func main() {
log.Fatalf("Unrecognized notifier")
}
clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(*config.AccessControl)
sessionProvider := session.NewProvider(config.Session)
regulator := regulation.NewRegulator(config.Regulation, storageProvider)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
providers := middlewares.Providers{
Authorizer: authorizer,

View File

@ -1,4 +1,4 @@
version: '3'
version: "3"
services:
authelia-backend:
build:
@ -12,7 +12,6 @@ services:
- "${GOPATH}:/go"
- "/tmp/authelia:/tmp/authelia"
environment:
- SUITE_PATH=${SUITE_PATH}
- ENVIRONMENT=dev
networks:
authelianet:

View File

@ -4,4 +4,7 @@ set -x
go get github.com/cespare/reflex
mkdir -p /var/lib/authelia
mkdir -p /etc/authelia
reflex -c /resources/reflex.conf

View File

@ -27,7 +27,4 @@ retry() {
# Build the binary
go build -o /tmp/authelia/authelia-tmp cmd/authelia/main.go
# Run the temporary binary
cd $SUITE_PATH
retry 3 /tmp/authelia/authelia-tmp -config ${SUITE_PATH}/configuration.yml
retry 3 /tmp/authelia/authelia-tmp -config /etc/authelia/configuration.yml

View File

@ -35,13 +35,15 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password)
if err != nil {
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
ctx.Providers.Regulator.Mark(bodyJSON.Username, false)
ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
return
}
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
// Mark the authentication attempt and whether it was successful.
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, userPasswordOk)
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, false)
if err != nil {
ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage)

View File

@ -3,8 +3,10 @@ package handlers
import (
"fmt"
"testing"
"time"
"github.com/clems4ever/authelia/internal/mocks"
"github.com/clems4ever/authelia/internal/models"
"github.com/clems4ever/authelia/internal/authentication"
"github.com/golang/mock/gomock"
@ -70,6 +72,32 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
}
func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() {
t, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
s.mock.Clock.Set(t)
s.mock.UserProviderMock.
EXPECT().
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(false, fmt.Errorf("Invalid credentials"))
s.mock.StorageProviderMock.
EXPECT().
AppendAuthenticationLog(gomock.Eq(models.AuthenticationAttempt{
Username: "test",
Successful: false,
Time: t,
}))
s.mock.Ctx.Request.SetBodyString(`{
"username": "test",
"password": "hello",
"keepMeLoggedIn": true
}`)
FirstFactorPost(s.mock.Ctx)
}
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
s.mock.UserProviderMock.
EXPECT().

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/clems4ever/authelia/internal/regulation"
"github.com/clems4ever/authelia/internal/storage"
@ -32,11 +33,34 @@ type MockAutheliaCtx struct {
NotifierMock *MockNotifier
UserSession *session.UserSession
Clock TestingClock
}
// TestingClock implementation of clock for tests
type TestingClock struct {
now time.Time
}
// Now return the stored clock
func (dc *TestingClock) Now() time.Time {
return dc.now
}
// After return a channel receiving the time after duration has elapsed
func (dc *TestingClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
// Set set the time of the clock
func (dc *TestingClock) Set(now time.Time) {
dc.now = now
}
// NewMockAutheliaCtx create an instance of AutheliaCtx mock
func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia := new(MockAutheliaCtx)
mockAuthelia.Clock = TestingClock{}
configuration := schema.Configuration{
AccessControl: new(schema.AccessControlConfiguration),
@ -75,7 +99,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
providers.SessionProvider = session.NewProvider(
configuration.Session)
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider)
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock)
request := &fasthttp.RequestCtx{}
// Set a cookie to identify this client throughout the test
@ -94,6 +118,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
// Close close the mock
func (m *MockAutheliaCtx) Close() {
m.Hook.Reset()
m.Ctrl.Finish()
}
// Assert200KO assert an error response from the service.

View File

@ -7,11 +7,13 @@ import (
"github.com/clems4ever/authelia/internal/configuration/schema"
"github.com/clems4ever/authelia/internal/models"
"github.com/clems4ever/authelia/internal/storage"
"github.com/clems4ever/authelia/internal/utils"
)
// NewRegulator create a regulator instance.
func NewRegulator(configuration *schema.RegulationConfiguration, provider storage.Provider) *Regulator {
func NewRegulator(configuration *schema.RegulationConfiguration, provider storage.Provider, clock utils.Clock) *Regulator {
regulator := &Regulator{storageProvider: provider}
regulator.clock = clock
if configuration != nil {
if configuration.FindTime > configuration.BanTime {
panic(fmt.Errorf("find_time cannot be greater than ban_time"))
@ -30,7 +32,7 @@ func (r *Regulator) Mark(username string, successful bool) error {
return r.storageProvider.AppendAuthenticationLog(models.AuthenticationAttempt{
Username: username,
Successful: successful,
Time: time.Now(),
Time: r.clock.Now(),
})
}
@ -42,7 +44,7 @@ func (r *Regulator) Regulate(username string) (time.Time, error) {
if !r.enabled {
return time.Time{}, nil
}
now := time.Now()
now := r.clock.Now()
// TODO(c.michaud): make sure FindTime < BanTime.
attempts, err := r.storageProvider.LoadLatestAuthenticationLogs(username, now.Add(-r.banTime))

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/clems4ever/authelia/internal/storage"
"github.com/clems4ever/authelia/internal/utils"
)
// Regulator an authentication regulator preventing attackers to brute force the service.
@ -18,4 +19,6 @@ type Regulator struct {
banTime time.Duration
storageProvider storage.Provider
clock utils.Clock
}

View File

@ -10,7 +10,7 @@ jwt_secret: unsecure_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret
@ -31,14 +31,13 @@ duo_api:
access_control:
default_policy: bypass
rules:
- domain: 'public.example.com'
policy: bypass
- domain: 'secure.example.com'
policy: two_factor
- domain: "public.example.com"
policy: bypass
- domain: "secure.example.com"
policy: two_factor
notifier:
smtp:
host: smtp
port: 1025
sender: admin@example.com

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/BypassAll/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/BypassAll/users.yml:/var/lib/authelia/users.yml"

View File

@ -4,13 +4,13 @@
port: 9091
logs_level: debug
logs_level: trace
jwt_secret: very_important_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret
@ -38,7 +38,7 @@ duo_api:
# Access Control
#
# Access control is a set of rules you can use to restrict user access to certain
# Access control is a set of rules you can use to restrict user access to certain
# resources.
access_control:
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
@ -54,39 +54,38 @@ access_control:
- domain: secure.example.com
policy: two_factor
- domain: '*.example.com'
- domain: "*.example.com"
subject: "group:admins"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/john/.*$'
- "^/users/john/.*$"
subject: "user:john"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/harry/.*$'
- "^/users/harry/.*$"
subject: "user:harry"
policy: two_factor
- domain: '*.mail.example.com'
- domain: "*.mail.example.com"
subject: "user:bob"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/bob/.*$'
- "^/users/bob/.*$"
subject: "user:bob"
policy: two_factor
# Configuration of the authentication regulation mechanism.
regulation:
regulation:
# Set it to 0 to disable max_retries.
max_retries: 3
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 300
# The length of time before a banned user can login again.
@ -98,4 +97,3 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/DuoPush/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/DuoPush/users.yml:/var/lib/authelia/users.yml"

View File

@ -0,0 +1,5 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/HighAvailability/configuration.yml:/etc/authelia/configuration.yml:ro"

View File

@ -0,0 +1,5 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/LDAP/configuration.yml:/etc/authelia/configuration.yml:ro"

View File

@ -12,7 +12,7 @@ jwt_secret: very_important_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret

View File

@ -0,0 +1,5 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/Mariadb/configuration.yml:/etc/authelia/configuration.yml:ro"

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/NetworkACL/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/NetworkACL/users.yml:/var/lib/authelia/users.yml"

View File

@ -12,7 +12,7 @@ jwt_secret: very_important_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/Postgres/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/Postgres/users.yml:/var/lib/authelia/users.yml"

View File

@ -12,7 +12,7 @@ default_redirection_url: https://home.example.com:8080/
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/ShortTimeouts/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/ShortTimeouts/users.yml:/var/lib/authelia/users.yml"

View File

@ -12,7 +12,7 @@ jwt_secret: very_important_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret
@ -22,7 +22,7 @@ session:
storage:
local:
path: db.sqlite3
path: /tmp/authelia/db.sqlite3
totp:
issuer: example.com
@ -40,37 +40,36 @@ access_control:
- domain: secure.example.com
policy: two_factor
- domain: '*.example.com'
- domain: "*.example.com"
subject: "group:admins"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/john/.*$'
- "^/users/john/.*$"
subject: "user:john"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/harry/.*$'
- "^/users/harry/.*$"
subject: "user:harry"
policy: two_factor
- domain: '*.mail.example.com'
- domain: "*.mail.example.com"
subject: "user:bob"
policy: two_factor
- domain: dev.example.com
resources:
- '^/users/bob/.*$'
- "^/users/bob/.*$"
subject: "user:bob"
policy: two_factor
regulation:
regulation:
# Set it to 0 to disable max_retries.
max_retries: 3
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 300
# The length of time before a banned user can login again.
ban_time: 900
@ -80,4 +79,3 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml"

View File

@ -1,28 +1,20 @@
###############################################################
# Users Database #
###############################################################
# This file can be used if you do not have an LDAP set up.
users:
john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
harry:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: harry.potter@authelia.com
groups: []
bob:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
email: bob.dylan@authelia.com
groups:
- dev
- dev
harry:
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
email: harry.potter@authelia.com
groups: []
james:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
email: james.dean@authelia.com
groups: []
john:
password: '{CRYPT}$6$rounds=50000$LnfgDsc2WD8F2qNf$0gcCt8jlqAGZRv2ee3mCFsfAr1P4N7kESWEf36Xtw6OjkhAcQuGVOBHXp0lFuZbppa7YlgHk3VD28aSQu9U9S1'
email: john.doe@authelia.com
groups:
- admins
- dev

View File

@ -10,7 +10,7 @@ jwt_secret: unsecure_secret
authentication_backend:
file:
path: users.yml
path: /var/lib/authelia/users.yml
session:
secret: unsecure_session_secret

View File

@ -0,0 +1,6 @@
version: "3"
services:
authelia-backend:
volumes:
- "./internal/suites/Traefik/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/Traefik/users.yml:/var/lib/authelia/users.yml"

View File

@ -0,0 +1,12 @@
package suites
import (
"context"
"fmt"
"testing"
)
func (wds *WebDriverSession) doChangeMethod(ctx context.Context, t *testing.T, method string) {
wds.WaitElementLocatedByID(ctx, t, "methods-button").Click()
wds.WaitElementLocatedByID(ctx, t, fmt.Sprintf("%s-option", method)).Click()
}

View File

@ -1,24 +1,21 @@
package suites
import (
"crypto/tls"
"io/ioutil"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func doHTTPGetQuery(s *SeleniumSuite, url string) []byte {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
func doHTTPGetQuery(t *testing.T, url string) []byte {
client := NewHTTPClient()
req, err := http.NewRequest("GET", url, nil)
assert.NoError(s.T(), err)
assert.NoError(t, err)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
assert.NoError(s.T(), err)
assert.NoError(t, err)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)

View File

@ -2,46 +2,57 @@ package suites
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func doFillLoginPageAndClick(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) {
usernameElement := WaitElementLocatedByID(ctx, s, "username")
func (wds *WebDriverSession) doFillLoginPageAndClick(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) {
usernameElement := wds.WaitElementLocatedByID(ctx, t, "username-textfield")
err := usernameElement.SendKeys(username)
assert.NoError(s.T(), err)
assert.NoError(t, err)
passwordElement := WaitElementLocatedByID(ctx, s, "password")
passwordElement := wds.WaitElementLocatedByID(ctx, t, "password-textfield")
err = passwordElement.SendKeys(password)
assert.NoError(s.T(), err)
assert.NoError(t, err)
if keepMeLoggedIn {
keepMeLoggedInElement := WaitElementLocatedByID(ctx, s, "remember-checkbox")
keepMeLoggedInElement := wds.WaitElementLocatedByID(ctx, t, "remember-checkbox")
err = keepMeLoggedInElement.Click()
assert.NoError(s.T(), err)
assert.NoError(t, err)
}
buttonElement := WaitElementLocatedByTagName(ctx, s, "button")
buttonElement := wds.WaitElementLocatedByID(ctx, t, "sign-in-button")
err = buttonElement.Click()
assert.NoError(s.T(), err)
assert.NoError(t, err)
}
func doLoginOneFactor(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool, targetURL string) {
doVisitLoginPage(ctx, s, targetURL)
doFillLoginPageAndClick(ctx, s, username, password, keepMeLoggedIn)
// Login 1FA
func (wds *WebDriverSession) doLoginOneFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, targetURL string) {
wds.doVisitLoginPage(ctx, t, targetURL)
wds.doFillLoginPageAndClick(ctx, t, username, password, keepMeLoggedIn)
}
func doLoginTwoFactor(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) {
doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, targetURL)
verifyIsSecondFactorPage(ctx, s)
doValidateTOTP(ctx, s, otpSecret)
// Login 1FA and 2FA subsequently (must already be registered)
func (wds *WebDriverSession) doLoginTwoFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) {
wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, targetURL)
wds.verifyIsSecondFactorPage(ctx, t)
wds.doValidateTOTP(ctx, t, otpSecret)
}
func doLoginAndRegisterTOTP(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) string {
doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, "")
secret := doRegisterTOTP(ctx, s)
s.Assert().NotNil(secret)
doVisit(s, LoginBaseURL)
verifyIsSecondFactorPage(ctx, s)
// Login 1FA and register 2FA.
func (wds *WebDriverSession) doLoginAndRegisterTOTP(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) string {
wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, "")
secret := wds.doRegisterTOTP(ctx, t)
wds.doVisit(t, LoginBaseURL)
wds.verifyIsSecondFactorPage(ctx, t)
return secret
}
// Register a user with TOTP, logout and then authenticate until TOTP-2FA.
func (wds *WebDriverSession) doRegisterAndLogin2FA(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, targetURL string) string {
// Register TOTP secret and logout.
secret := wds.doRegisterThenLogout(ctx, t, username, password)
wds.doLoginTwoFactor(ctx, t, username, password, false, secret, targetURL)
return secret
}

View File

@ -1,8 +1,12 @@
package suites
import "context"
import (
"context"
"fmt"
"testing"
)
func doLogout(ctx context.Context, s *SeleniumSuite) {
doVisit(s, "https://login.example.com:8080/#/logout")
verifyIsFirstFactorPage(ctx, s)
func (wds *WebDriverSession) doLogout(ctx context.Context, t *testing.T) {
wds.doVisit(t, fmt.Sprintf("%s%s", LoginBaseURL, "/logout"))
wds.verifyIsFirstFactorPage(ctx, t)
}

View File

@ -3,8 +3,8 @@ package suites
import (
"encoding/json"
"fmt"
"log"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
@ -13,22 +13,20 @@ type message struct {
ID int `json:"id"`
}
func doGetLinkFromLastMail(s *SeleniumSuite) string {
res := doHTTPGetQuery(s, fmt.Sprintf("%s/messages", MailBaseURL))
func doGetLinkFromLastMail(t *testing.T) string {
res := doHTTPGetQuery(t, fmt.Sprintf("%s/messages", MailBaseURL))
messages := make([]message, 0)
err := json.Unmarshal(res, &messages)
assert.NoError(s.T(), err)
assert.Greater(s.T(), len(messages), 0)
assert.NoError(t, err)
assert.Greater(t, len(messages), 0)
messageID := messages[len(messages)-1].ID
res = doHTTPGetQuery(s, fmt.Sprintf("%s/messages/%d.html", MailBaseURL, messageID))
res = doHTTPGetQuery(t, fmt.Sprintf("%s/messages/%d.html", MailBaseURL, messageID))
re := regexp.MustCompile(`<a href="(.+)" class="button">.*<\/a>`)
matches := re.FindStringSubmatch(string(res))
if len(matches) != 2 {
log.Fatal("Number of match for link in email is not equal to one")
}
assert.Len(t, matches, 2, "Number of match for link in email is not equal to one")
return matches[1]
}

View File

@ -1,9 +1,12 @@
package suites
import "context"
import (
"context"
"testing"
)
func doRegisterThenLogout(ctx context.Context, s *SeleniumSuite, username, password string) string {
secret := doLoginAndRegisterTOTP(ctx, s, username, password, false)
doLogout(ctx, s)
func (wds *WebDriverSession) doRegisterThenLogout(ctx context.Context, t *testing.T, username, password string) string {
secret := wds.doLoginAndRegisterTOTP(ctx, t, username, password, false)
wds.doLogout(ctx, t)
return secret
}

View File

@ -0,0 +1,35 @@
package suites
import (
"context"
"testing"
)
func (wds *WebDriverSession) doInitiatePasswordReset(ctx context.Context, t *testing.T, username string) {
wds.WaitElementLocatedByID(ctx, t, "reset-password-button").Click()
// Fill in username
wds.WaitElementLocatedByID(ctx, t, "username-textfield").SendKeys(username)
// And click on the reset button
wds.WaitElementLocatedByID(ctx, t, "reset-button").Click()
}
func (wds *WebDriverSession) doCompletePasswordReset(ctx context.Context, t *testing.T, newPassword1, newPassword2 string) {
link := doGetLinkFromLastMail(t)
wds.doVisit(t, link)
wds.WaitElementLocatedByID(ctx, t, "password1-textfield").SendKeys(newPassword1)
wds.WaitElementLocatedByID(ctx, t, "password2-textfield").SendKeys(newPassword2)
wds.WaitElementLocatedByID(ctx, t, "reset-button").Click()
}
func (wds *WebDriverSession) doSuccessfullyCompletePasswordReset(ctx context.Context, t *testing.T, newPassword1, newPassword2 string) {
wds.doCompletePasswordReset(ctx, t, newPassword1, newPassword2)
wds.verifyIsFirstFactorPage(ctx, t)
}
func (wds *WebDriverSession) doResetPassword(ctx context.Context, t *testing.T, username, newPassword1, newPassword2 string) {
wds.doInitiatePasswordReset(ctx, t, username)
// then wait for the "email sent notification"
wds.verifyMailNotificationDisplayed(ctx, t)
wds.doSuccessfullyCompletePasswordReset(ctx, t, newPassword1, newPassword2)
}

View File

@ -2,24 +2,35 @@ package suites
import (
"context"
"testing"
"time"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
)
func doRegisterTOTP(ctx context.Context, s *SeleniumSuite) string {
WaitElementLocatedByClassName(ctx, s, "register-totp").Click()
verifyBodyContains(ctx, s, "Please check your e-mails")
link := doGetLinkFromLastMail(s)
doVisit(s, link)
secret, err := WaitElementLocatedByClassName(ctx, s, "base32-secret").Text()
s.Assert().NoError(err)
func (wds *WebDriverSession) doRegisterTOTP(ctx context.Context, t *testing.T) string {
wds.WaitElementLocatedByID(ctx, t, "register-link").Click()
wds.verifyMailNotificationDisplayed(ctx, t)
link := doGetLinkFromLastMail(t)
wds.doVisit(t, link)
secret, err := wds.WaitElementLocatedByID(ctx, t, "base32-secret").GetAttribute("value")
assert.NoError(t, err)
assert.NotEqual(t, "", secret)
assert.NotNil(t, secret)
return secret
}
func doValidateTOTP(ctx context.Context, s *SeleniumSuite, secret string) {
code, err := totp.GenerateCode(secret, time.Now())
s.Assert().NoError(err)
WaitElementLocatedByID(ctx, s, "totp-token").SendKeys(code)
WaitElementLocatedByID(ctx, s, "totp-button").Click()
func (wds *WebDriverSession) doEnterOTP(ctx context.Context, t *testing.T, code string) {
inputs := wds.WaitElementsLocatedByCSSSelector(ctx, t, "#otp-input input")
for i := 0; i < 6; i++ {
inputs[i].SendKeys(string(code[i]))
}
}
func (wds *WebDriverSession) doValidateTOTP(ctx context.Context, t *testing.T, secret string) {
code, err := totp.GenerateCode(secret, time.Now())
assert.NoError(t, err)
wds.doEnterOTP(ctx, t, code)
}

View File

@ -3,25 +3,25 @@ package suites
import (
"context"
"fmt"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func doVisit(s *SeleniumSuite, url string) {
err := s.WebDriver().Get(url)
assert.NoError(s.T(), err)
func (wds *WebDriverSession) doVisit(t *testing.T, url string) {
err := wds.WebDriver.Get(url)
assert.NoError(t, err)
}
func doVisitAndVerifyURLIs(ctx context.Context, s *SeleniumSuite, url string) {
doVisit(s, url)
verifyURLIs(ctx, s, url)
func (wds *WebDriverSession) doVisitAndVerifyURLIs(ctx context.Context, t *testing.T, url string) {
wds.doVisit(t, url)
wds.verifyURLIs(ctx, t, url)
}
func doVisitLoginPage(ctx context.Context, s *SeleniumSuite, targetURL string) {
func (wds *WebDriverSession) doVisitLoginPage(ctx context.Context, t *testing.T, targetURL string) {
suffix := ""
if targetURL != "" {
suffix = fmt.Sprintf("?rd=%s", url.QueryEscape(targetURL))
suffix = fmt.Sprintf("?rd=%s", targetURL)
}
doVisitAndVerifyURLIs(ctx, s, fmt.Sprintf("%s%s", LoginBaseURL, suffix))
wds.doVisitAndVerifyURLIs(ctx, t, fmt.Sprintf("%s/%s", LoginBaseURL, suffix))
}

View File

@ -6,7 +6,7 @@ import "fmt"
var BaseDomain = "example.com:8080"
// LoginBaseURL the base URL of the login portal
var LoginBaseURL = fmt.Sprintf("https://login.%s/", BaseDomain)
var LoginBaseURL = fmt.Sprintf("https://login.%s", BaseDomain)
// SingleFactorBaseURL the base URL of the singlefactor domain
var SingleFactorBaseURL = fmt.Sprintf("https://singlefactor.%s", BaseDomain)
@ -18,4 +18,25 @@ var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain)
var MailBaseURL = fmt.Sprintf("https://mail.%s", BaseDomain)
// HomeBaseURL the base URL of the home domain
var HomeBaseURL = fmt.Sprintf("https://home.%s/", BaseDomain)
var HomeBaseURL = fmt.Sprintf("https://home.%s", BaseDomain)
// PublicBaseURL the base URL of the public domain
var PublicBaseURL = fmt.Sprintf("https://public.%s", BaseDomain)
// SecureBaseURL the base URL of the secure domain
var SecureBaseURL = fmt.Sprintf("https://secure.%s", BaseDomain)
// DevBaseURL the base URL of the dev domain
var DevBaseURL = fmt.Sprintf("https://dev.%s", BaseDomain)
// MX1MailBaseURL the base URL of the mx1.mail domain
var MX1MailBaseURL = fmt.Sprintf("https://mx1.mail.%s", BaseDomain)
// MX2MailBaseURL the base URL of the mx2.mail domain
var MX2MailBaseURL = fmt.Sprintf("https://mx2.mail.%s", BaseDomain)
// DuoBaseURL the base URL of the Duo configuration API
var DuoBaseURL = "https://duo.example.com"
// AutheliaBaseURL the base URL of Authelia service
var AutheliaBaseURL = "http://authelia.example.com:9091"

View File

@ -2,7 +2,6 @@ package suites
import (
"fmt"
"os"
"os/exec"
"strings"
@ -21,41 +20,35 @@ func NewDockerEnvironment(files []string) *DockerEnvironment {
}
func (de *DockerEnvironment) createCommandWithStdout(cmd string) *exec.Cmd {
dockerCmdLine := "docker-compose -f " + strings.Join(de.dockerComposeFiles, " -f ") + " " + cmd
dockerCmdLine := fmt.Sprintf("docker-compose -f %s %s", strings.Join(de.dockerComposeFiles, " -f "), cmd)
log.Trace(dockerCmdLine)
return utils.CommandWithStdout("bash", "-c", dockerCmdLine)
}
func (de *DockerEnvironment) createCommand(cmd string) *exec.Cmd {
dockerCmdLine := "docker-compose -f " + strings.Join(de.dockerComposeFiles, " -f ") + " " + cmd
dockerCmdLine := fmt.Sprintf("docker-compose -f %s %s", strings.Join(de.dockerComposeFiles, " -f "), cmd)
log.Trace(dockerCmdLine)
return exec.Command("bash", "-c", dockerCmdLine)
return utils.Command("bash", "-c", dockerCmdLine)
}
// Up spawn a docker environment
func (de *DockerEnvironment) Up(suitePath string) error {
cmd := de.createCommandWithStdout("up -d")
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
return cmd.Run()
func (de *DockerEnvironment) Up() error {
return de.createCommandWithStdout("up -d").Run()
}
// Restart restarts a service
func (de *DockerEnvironment) Restart(suitePath, service string) error {
cmd := de.createCommandWithStdout(fmt.Sprintf("restart %s", service))
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
return cmd.Run()
func (de *DockerEnvironment) Restart(service string) error {
return de.createCommandWithStdout(fmt.Sprintf("restart %s", service)).Run()
}
// Down spawn a docker environment
func (de *DockerEnvironment) Down(suitePath string) error {
cmd := de.createCommandWithStdout("down -v")
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
return cmd.Run()
func (de *DockerEnvironment) Down() error {
return de.createCommandWithStdout("down -v").Run()
}
// Logs get logs of a given service of the environment
func (de *DockerEnvironment) Logs(service string, flags []string) (string, error) {
cmd := de.createCommand("logs " + strings.Join(flags, " ") + " " + service)
cmd := de.createCommand(fmt.Sprintf("logs %s %s", strings.Join(flags, " "), service))
content, err := cmd.Output()
return string(content), err
}

35
internal/suites/duo.go Normal file
View File

@ -0,0 +1,35 @@
package suites
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
// DuoPolicy a type of policy
type DuoPolicy int32
const (
// Deny deny policy
Deny DuoPolicy = iota
// Allow allow policy
Allow DuoPolicy = iota
)
// ConfigureDuo configure duo api to allow or block auth requests
func ConfigureDuo(t *testing.T, allowDeny DuoPolicy) {
url := fmt.Sprintf("%s/allow", DuoBaseURL)
if allowDeny == Deny {
url = fmt.Sprintf("%s/deny", DuoBaseURL)
}
req, err := http.NewRequest("POST", url, nil)
assert.NoError(t, err)
client := NewHTTPClient()
res, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, 200, res.StatusCode)
}

21
internal/suites/http.go Normal file
View File

@ -0,0 +1,21 @@
package suites
import (
"crypto/tls"
"net/http"
)
// NewHTTPClient create a new client skipping TLS verification and not redirecting
func NewHTTPClient() *http.Client {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}

View File

@ -0,0 +1,87 @@
package suites
import (
"context"
"log"
"time"
"github.com/tebeka/selenium"
)
type AvailableMethodsScenario struct {
*SeleniumSuite
methods []string
}
func NewAvailableMethodsScenario(methods []string) *AvailableMethodsScenario {
return &AvailableMethodsScenario{
SeleniumSuite: new(SeleniumSuite),
methods: methods,
}
}
func (s *AvailableMethodsScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.SeleniumSuite.WebDriverSession = wds
}
func (s *AvailableMethodsScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *AvailableMethodsScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func IsStringInList(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}
func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
methodsButton := s.WaitElementLocatedByID(ctx, s.T(), "methods-button")
err := methodsButton.Click()
s.Assert().NoError(err)
methodsDialog := s.WaitElementLocatedByID(ctx, s.T(), "methods-dialog")
options, err := methodsDialog.FindElements(selenium.ByClassName, "method-option")
s.Assert().NoError(err)
s.Assert().Len(options, len(s.methods))
optionsList := make([]string, 0)
for _, o := range options {
txt, err := o.Text()
s.Assert().NoError(err)
optionsList = append(optionsList, txt)
}
s.Assert().Len(optionsList, len(s.methods))
for _, m := range s.methods {
s.Assert().True(IsStringInList(m, optionsList))
}
}

View File

@ -0,0 +1,57 @@
package suites
import (
"crypto/tls"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/suite"
)
type BackendProtectionScenario struct {
suite.Suite
}
func NewBackendProtectionScenario() *BackendProtectionScenario {
return &BackendProtectionScenario{}
}
func (s *BackendProtectionScenario) AssertRequestStatusCode(method, url string, expectedStatusCode int) {
s.Run(url, func() {
req, err := http.NewRequest(method, url, nil)
s.Assert().NoError(err)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, expectedStatusCode)
})
}
func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/finish", AutheliaBaseURL), 403)
}
func TestRunBackendProtection(t *testing.T) {
suite.Run(t, NewBackendProtectionScenario())
}

View File

@ -0,0 +1,63 @@
package suites
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type BypassPolicyScenario struct {
*SeleniumSuite
}
func NewBypassPolicyScenario() *BypassPolicyScenario {
return &BypassPolicyScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *BypassPolicyScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *BypassPolicyScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *BypassPolicyScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *BypassPolicyScenario) TestShouldAccessPublicResource() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doVisit(s.T(), AdminBaseURL)
s.verifyIsFirstFactorPage(ctx, s.T())
s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", PublicBaseURL))
s.verifySecretAuthorized(ctx, s.T())
}
func TestBypassPolicyScenario(t *testing.T) {
suite.Run(t, NewBypassPolicyScenario())
}

View File

@ -0,0 +1,78 @@
package suites
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/tebeka/selenium"
)
type CustomHeadersScenario struct {
*SeleniumSuite
}
func NewCustomHeadersScenario() *CustomHeadersScenario {
return &CustomHeadersScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *CustomHeadersScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *CustomHeadersScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *CustomHeadersScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *CustomHeadersScenario) TestShouldNotForwardCustomHeaderForUnauthenticatedUser() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doVisit(s.T(), fmt.Sprintf("%s/headers", PublicBaseURL))
body, err := s.WebDriver().FindElement(selenium.ByTagName, "body")
s.Assert().NoError(err)
s.WaitElementTextContains(ctx, s.T(), body, "httpbin:8000")
}
func (s *CustomHeadersScenario) TestShouldForwardCustomHeaderForAuthenticatedUser() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/headers", PublicBaseURL)
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
s.verifyURLIs(ctx, s.T(), targetURL)
body, err := s.WebDriver().FindElement(selenium.ByTagName, "body")
s.Assert().NoError(err)
s.WaitElementTextContains(ctx, s.T(), body, "\"Custom-Forwarded-User\": \"john\"")
s.WaitElementTextContains(ctx, s.T(), body, "\"Custom-Forwarded-Groups\": \"admins,dev\"")
}
func TestCustomHeadersScenario(t *testing.T) {
suite.Run(t, NewCustomHeadersScenario())
}

View File

@ -0,0 +1,117 @@
package suites
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type InactivityScenario struct {
*SeleniumSuite
secret string
}
func NewInactivityScenario() *InactivityScenario {
return &InactivityScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *InactivityScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.secret = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
func (s *InactivityScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *InactivityScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *InactivityScenario) TestShouldRequireReauthenticationAfterInactivityPeriod() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, s.secret, "")
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
time.Sleep(6 * time.Second)
s.doVisit(s.T(), targetURL)
s.verifyIsFirstFactorPage(ctx, s.T())
}
func (s *InactivityScenario) TestShouldRequireReauthenticationAfterCookieExpiration() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, s.secret, "")
for i := 0; i < 3; i++ {
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
time.Sleep(2 * time.Second)
s.doVisit(s.T(), targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
time.Sleep(2 * time.Second)
s.doVisit(s.T(), targetURL)
s.verifyIsFirstFactorPage(ctx, s.T())
}
func (s *InactivityScenario) TestShouldDisableCookieExpirationAndInactivity() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", true, s.secret, "")
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
time.Sleep(9 * time.Second)
s.doVisit(s.T(), targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
func TestInactivityScenario(t *testing.T) {
suite.Run(t, NewInactivityScenario())
}

View File

@ -14,7 +14,7 @@ type OneFactorSuite struct {
*SeleniumSuite
}
func NewOneFactorSuite() *OneFactorSuite {
func NewOneFactorScenario() *OneFactorSuite {
return &OneFactorSuite{
SeleniumSuite: new(SeleniumSuite),
}
@ -27,7 +27,7 @@ func (s *OneFactorSuite) SetupSuite() {
log.Fatal(err)
}
s.SeleniumSuite.WebDriverSession = wds
s.WebDriverSession = wds
}
func (s *OneFactorSuite) TearDownSuite() {
@ -42,9 +42,9 @@ func (s *OneFactorSuite) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
doLogout(ctx, s.SeleniumSuite)
doVisit(s.SeleniumSuite, HomeBaseURL)
verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL)
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
@ -52,8 +52,8 @@ func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL)
verifySecretAuthorized(ctx, s.SeleniumSuite)
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() {
@ -61,8 +61,8 @@ func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() {
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL)
verifyIsSecondFactorPage(ctx, s.SeleniumSuite)
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
s.verifyIsSecondFactorPage(ctx, s.T())
}
func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
@ -70,11 +70,11 @@ func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "bad-password", false, targetURL)
verifyIsFirstFactorPage(ctx, s.SeleniumSuite)
verifyNotificationDisplayed(ctx, s.SeleniumSuite, "Authentication failed. Check your credentials.")
s.doLoginOneFactor(ctx, s.T(), "john", "bad-password", false, targetURL)
s.verifyIsFirstFactorPage(ctx, s.T())
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.")
}
func TestRunOneFactor(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewOneFactorScenario())
}

View File

@ -0,0 +1,82 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type RedirectionCheckScenario struct {
*SeleniumSuite
}
func NewRedirectionCheckScenario() *RedirectionCheckScenario {
return &RedirectionCheckScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *RedirectionCheckScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *RedirectionCheckScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *RedirectionCheckScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
var redirectionAuthorizations = map[string]bool{
// external website
"https://www.google.fr": false,
// Not the right domain
"https://public.example.com.a:8080/secret.html": false,
// Not https
"http://secure.example.com:8080/secret.html": false,
// Domain handled by Authelia
"https://secure.example.com:8080/secret.html": true,
}
func (s *RedirectionCheckScenario) TestShouldRedirectOnlyWhenDomainIsHandledByAuthelia() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
for url, redirected := range redirectionAuthorizations {
s.T().Run(url, func(t *testing.T) {
s.doLoginTwoFactor(ctx, t, "john", "password", false, secret, url)
time.Sleep(1 * time.Second)
if redirected {
s.verifySecretAuthorized(ctx, t)
} else {
s.WaitElementLocatedByClassName(ctx, t, "success-icon")
}
s.doLogout(ctx, t)
})
}
}
func TestRedirectionCheckScenario(t *testing.T) {
suite.Run(t, NewRedirectionCheckScenario())
}

View File

@ -0,0 +1,83 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/tebeka/selenium"
)
type RegulationScenario struct {
*SeleniumSuite
}
func NewRegulationScenario() *RegulationScenario {
return &RegulationScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *RegulationScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *RegulationScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *RegulationScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.doVisitLoginPage(ctx, s.T(), "")
s.doFillLoginPageAndClick(ctx, s.T(), "john", "bad-password", false)
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.")
for i := 0; i < 3; i++ {
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
time.Sleep(2 * time.Second)
}
// Reset password field
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").
SendKeys(selenium.ControlKey + "a" + selenium.BackspaceKey)
// And enter the correct password
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("password")
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.")
time.Sleep(1 * time.Second)
s.verifyIsFirstFactorPage(ctx, s.T())
time.Sleep(9 * time.Second)
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
s.verifyIsSecondFactorPage(ctx, s.T())
}
func TestBlacklistingScenario(t *testing.T) {
suite.Run(t, NewRegulationScenario())
}

View File

@ -0,0 +1,101 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type ResetPasswordScenario struct {
*SeleniumSuite
}
func NewResetPasswordScenario() *ResetPasswordScenario {
return &ResetPasswordScenario{SeleniumSuite: new(SeleniumSuite)}
}
func (s *ResetPasswordScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *ResetPasswordScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *ResetPasswordScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *ResetPasswordScenario) TestShouldResetPassword() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsFirstFactorPage(ctx, s.T())
// Reset the password to abc
s.doResetPassword(ctx, s.T(), "john", "abc", "abc")
// Try to login with the old password
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.")
// Try to login with the new password
s.doLoginOneFactor(ctx, s.T(), "john", "abc", false, "")
// Logout
s.doLogout(ctx, s.T())
// Reset the original password
s.doResetPassword(ctx, s.T(), "john", "password", "password")
}
func (s *ResetPasswordScenario) TestShouldMakeAttackerThinkPasswordResetIsInitiated() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsFirstFactorPage(ctx, s.T())
// Try to initiate a password reset of an inexistant user
s.doInitiatePasswordReset(ctx, s.T(), "i_dont_exist")
// Check that the notification make the attacker thinks the process is initiated
s.verifyMailNotificationDisplayed(ctx, s.T())
}
func (s *ResetPasswordScenario) TestShouldLetUserNoticeThereIsAPasswordMismatch() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsFirstFactorPage(ctx, s.T())
s.doInitiatePasswordReset(ctx, s.T(), "john")
s.verifyMailNotificationDisplayed(ctx, s.T())
s.doCompletePasswordReset(ctx, s.T(), "password", "another_password")
s.verifyNotificationDisplayed(ctx, s.T(), "Passwords do not match.")
}
func TestRunResetPasswordScenario(t *testing.T) {
suite.Run(t, NewResetPasswordScenario())
}

View File

@ -14,7 +14,7 @@ type TwoFactorSuite struct {
*SeleniumSuite
}
func NewTwoFactorSuite() *TwoFactorSuite {
func NewTwoFactorScenario() *TwoFactorSuite {
return &TwoFactorSuite{
SeleniumSuite: new(SeleniumSuite),
}
@ -27,7 +27,7 @@ func (s *TwoFactorSuite) SetupSuite() {
log.Fatal(err)
}
s.SeleniumSuite.WebDriverSession = wds
s.WebDriverSession = wds
}
func (s *TwoFactorSuite) TearDownSuite() {
@ -42,24 +42,44 @@ func (s *TwoFactorSuite) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
doLogout(ctx, s.SeleniumSuite)
doVisit(s.SeleniumSuite, HomeBaseURL)
verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL)
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Register TOTP secret and logout.
secret := doRegisterThenLogout(ctx, s.SeleniumSuite, "john", "password")
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
doLoginTwoFactor(ctx, s.SeleniumSuite, "john", "password", false, secret, targetURL)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, targetURL)
s.verifySecretAuthorized(ctx, s.T())
verifySecretAuthorized(ctx, s.SeleniumSuite)
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
s.doVisit(s.T(), targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
func (s *TwoFactorSuite) TestShouldFailTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Register TOTP secret and logout.
s.doRegisterThenLogout(ctx, s.T(), "john", "password")
wrongPasscode := "123456"
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.verifyIsSecondFactorPage(ctx, s.T())
s.doEnterOTP(ctx, s.T(), wrongPasscode)
s.verifyNotificationDisplayed(ctx, s.T(), "The one-time password might be wrong")
}
func TestRunTwoFactor(t *testing.T) {
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -0,0 +1,99 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type UserPreferencesScenario struct {
*SeleniumSuite
}
func NewUserPreferencesScenario() *UserPreferencesScenario {
return &UserPreferencesScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *UserPreferencesScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *UserPreferencesScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *UserPreferencesScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Authenticate
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.verifyIsSecondFactorPage(ctx, s.T())
// And select OTP method
s.doChangeMethod(ctx, s.T(), "one-time-password")
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
// Then switch to push notification method
s.doChangeMethod(ctx, s.T(), "push-notification")
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
// Switch context to clean up state in portal.
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
// Then go back to portal.
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsSecondFactorPage(ctx, s.T())
// And check the latest method is still used.
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
// Meaning the authentication is successful
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
// Logout the user and see what user 'harry' sees.
s.doLogout(ctx, s.T())
s.doLoginOneFactor(ctx, s.T(), "harry", "password", false, "")
s.verifyIsSecondFactorPage(ctx, s.T())
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
s.doLogout(ctx, s.T())
s.verifyIsFirstFactorPage(ctx, s.T())
// Then log back as previous user and verify the push notification is still the default method
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.verifyIsSecondFactorPage(ctx, s.T())
s.WaitElementLocatedByID(ctx, s.T(), "push-notification-method")
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
// Eventually restore the default method
s.doChangeMethod(ctx, s.T(), "one-time-password")
s.WaitElementLocatedByID(ctx, s.T(), "one-time-password-method")
}
func TestUserPreferencesScenario(t *testing.T) {
suite.Run(t, NewUserPreferencesScenario())
}

View File

@ -9,6 +9,7 @@ var bypassAllSuiteName = "BypassAll"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/BypassAll/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -19,7 +20,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -27,7 +28,7 @@ func init() {
}
teardown := func(suitePath string) error {
return dockerEnvironment.Down(suitePath)
return dockerEnvironment.Down()
}
GlobalRegistry.Register(bypassAllSuiteName, Suite{

View File

@ -1,7 +1,13 @@
package suites
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type BypassAllSuite struct {
@ -12,6 +18,36 @@ func NewBypassAllSuite() *BypassAllSuite {
return &BypassAllSuite{SeleniumSuite: new(SeleniumSuite)}
}
func TestBypassAllSuite(t *testing.T) {
RunTypescriptSuite(t, bypassAllSuiteName)
func (s *BypassAllSuite) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *BypassAllSuite) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *BypassAllSuite) TestShouldAccessPublicResource() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", AdminBaseURL))
s.verifySecretAuthorized(ctx, s.T())
s.doVisit(s.T(), fmt.Sprintf("%s/secret.html", PublicBaseURL))
s.verifySecretAuthorized(ctx, s.T())
}
func TestBypassAllSuite(t *testing.T) {
suite.Run(t, NewBypassAllSuite())
suite.Run(t, NewCustomHeadersScenario())
}

View File

@ -9,6 +9,7 @@ var duoPushSuiteName = "DuoPush"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/DuoPush/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -17,7 +18,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -25,7 +26,7 @@ func init() {
}
teardown := func(suitePath string) error {
return dockerEnvironment.Down(suitePath)
return dockerEnvironment.Down()
}
GlobalRegistry.Register(duoPushSuiteName, Suite{

View File

@ -1,7 +1,12 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type DuoPushSuite struct {
@ -12,6 +17,58 @@ func NewDuoPushSuite() *DuoPushSuite {
return &DuoPushSuite{SeleniumSuite: new(SeleniumSuite)}
}
func TestDuoPushSuite(t *testing.T) {
RunTypescriptSuite(t, duoPushSuiteName)
func (s *DuoPushSuite) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *DuoPushSuite) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *DuoPushSuite) TearDownTest() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.doChangeMethod(ctx, s.T(), "one-time-password")
}
func (s *DuoPushSuite) TestShouldSucceedAuthentication() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ConfigureDuo(s.T(), Allow)
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.doChangeMethod(ctx, s.T(), "push-notification")
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
}
func (s *DuoPushSuite) TestShouldFailAuthentication() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ConfigureDuo(s.T(), Deny)
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.doChangeMethod(ctx, s.T(), "push-notification")
s.WaitElementLocatedByClassName(ctx, s.T(), "failure-icon")
}
func TestDuoPushSuite(t *testing.T) {
suite.Run(t, NewDuoPushSuite())
suite.Run(t, NewAvailableMethodsScenario([]string{
"ONE-TIME PASSWORD",
"PUSH NOTIFICATION",
}))
suite.Run(t, NewUserPreferencesScenario())
}

View File

@ -6,31 +6,32 @@ import (
var highAvailabilitySuiteName = "HighAvailability"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/mariadb/docker-compose.yml",
"example/compose/redis/docker-compose.yml",
"example/compose/nginx/backend/docker-compose.yml",
"example/compose/nginx/portal/docker-compose.yml",
"example/compose/smtp/docker-compose.yml",
"example/compose/httpbin/docker-compose.yml",
"example/compose/ldap/docker-compose.admin.yml", // This is just used for administration, not for testing.
"example/compose/ldap/docker-compose.yml",
})
var haDockerEnvironment = NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/HighAvailability/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/mariadb/docker-compose.yml",
"example/compose/redis/docker-compose.yml",
"example/compose/nginx/backend/docker-compose.yml",
"example/compose/nginx/portal/docker-compose.yml",
"example/compose/smtp/docker-compose.yml",
"example/compose/httpbin/docker-compose.yml",
"example/compose/ldap/docker-compose.admin.yml", // This is just used for administration, not for testing.
"example/compose/ldap/docker-compose.yml",
})
func init() {
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := haDockerEnvironment.Up(); err != nil {
return err
}
return waitUntilAutheliaIsReady(dockerEnvironment)
return waitUntilAutheliaIsReady(haDockerEnvironment)
}
teardown := func(suitePath string) error {
return dockerEnvironment.Down(suitePath)
return haDockerEnvironment.Down()
}
GlobalRegistry.Register(highAvailabilitySuiteName, Suite{

View File

@ -1,18 +1,206 @@
package suites
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type HighAvailabilitySuite struct {
type HighAvailabilityWebDriverSuite struct {
*SeleniumSuite
}
func NewHighAvailabilityWebDriverSuite() *HighAvailabilityWebDriverSuite {
return &HighAvailabilityWebDriverSuite{SeleniumSuite: new(SeleniumSuite)}
}
func (s *HighAvailabilityWebDriverSuite) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *HighAvailabilityWebDriverSuite) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
err := haDockerEnvironment.Restart("mariadb")
s.Assert().NoError(err)
time.Sleep(2 * time.Second)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepSessionAfterAutheliaRestart() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
secret := s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "")
err := haDockerEnvironment.Restart("authelia-backend")
s.Assert().NoError(err)
loop := true
for loop {
logs, err := haDockerEnvironment.Logs("authelia-backend", []string{"--tail", "10"})
s.Assert().NoError(err)
select {
case <-time.After(1 * time.Second):
if strings.Contains(logs, "Authelia is listening on :9091") {
loop = false
}
break
case <-ctx.Done():
loop = false
break
}
}
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
// Verify the user is still authenticated
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsSecondFactorPage(ctx, s.T())
// Then logout and login again to check the secret is still there
s.doLogout(ctx, s.T())
s.verifyIsFirstFactorPage(ctx, s.T())
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, fmt.Sprintf("%s/secret.html", SecureBaseURL))
s.verifySecretAuthorized(ctx, s.T())
}
var UserJohn = "john"
var UserBob = "bob"
var UserHarry = "harry"
var Users = []string{UserJohn, UserBob, UserHarry}
var expectedAuthorizations = map[string](map[string]bool){
fmt.Sprintf("%s/secret.html", PublicBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: true,
},
fmt.Sprintf("%s/secret.html", SecureBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: true,
},
fmt.Sprintf("%s/secret.html", AdminBaseURL): map[string]bool{
UserJohn: true, UserBob: false, UserHarry: false,
},
fmt.Sprintf("%s/secret.html", SingleFactorBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: true,
},
fmt.Sprintf("%s/secret.html", MX1MailBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: false,
},
fmt.Sprintf("%s/secret.html", MX2MailBaseURL): map[string]bool{
UserJohn: false, UserBob: true, UserHarry: false,
},
fmt.Sprintf("%s/groups/admin/secret.html", DevBaseURL): map[string]bool{
UserJohn: true, UserBob: false, UserHarry: false,
},
fmt.Sprintf("%s/groups/dev/secret.html", DevBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: false,
},
fmt.Sprintf("%s/users/john/secret.html", DevBaseURL): map[string]bool{
UserJohn: true, UserBob: false, UserHarry: false,
},
fmt.Sprintf("%s/users/harry/secret.html", DevBaseURL): map[string]bool{
UserJohn: true, UserBob: false, UserHarry: true,
},
fmt.Sprintf("%s/users/bob/secret.html", DevBaseURL): map[string]bool{
UserJohn: true, UserBob: true, UserHarry: false,
},
}
func (s *HighAvailabilityWebDriverSuite) TestShouldVerifyAccessControl() {
verifyUserIsAuthorized := func(ctx context.Context, t *testing.T, username, targetURL string, authorized bool) {
s.doVisit(t, targetURL)
s.verifyURLIs(ctx, t, targetURL)
if authorized {
s.verifySecretAuthorized(ctx, t)
} else {
s.verifyBodyContains(ctx, t, "403 Forbidden")
}
}
verifyAuthorization := func(username string) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
s.doRegisterAndLogin2FA(ctx, t, username, "password", false, "")
for url, authorizations := range expectedAuthorizations {
verifyUserIsAuthorized(ctx, t, username, url, authorizations[username])
}
s.doLogout(ctx, t)
}
}
for _, user := range []string{UserJohn, UserBob, UserHarry} {
s.T().Run(fmt.Sprintf("user %s", user), verifyAuthorization(user))
}
}
type HighAvailabilitySuite struct {
suite.Suite
}
func NewHighAvailabilitySuite() *HighAvailabilitySuite {
return &HighAvailabilitySuite{SeleniumSuite: new(SeleniumSuite)}
return &HighAvailabilitySuite{}
}
func DoGetWithAuth(t *testing.T, username, password string) int {
client := NewHTTPClient()
req, err := http.NewRequest("GET", fmt.Sprintf("%s/secret.html", SingleFactorBaseURL), nil)
req.SetBasicAuth(username, password)
assert.NoError(t, err)
res, err := client.Do(req)
assert.NoError(t, err)
return res.StatusCode
}
func (s *HighAvailabilitySuite) TestBasicAuth() {
s.Assert().Equal(DoGetWithAuth(s.T(), "john", "password"), 200)
s.Assert().Equal(DoGetWithAuth(s.T(), "john", "bad-password"), 302)
s.Assert().Equal(DoGetWithAuth(s.T(), "dontexist", "password"), 302)
}
func TestHighAvailabilitySuite(t *testing.T) {
RunTypescriptSuite(t, highAvailabilitySuiteName)
TestRunOneFactor(t)
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
suite.Run(t, NewRegulationScenario())
suite.Run(t, NewCustomHeadersScenario())
suite.Run(t, NewRedirectionCheckScenario())
suite.Run(t, NewHighAvailabilityWebDriverSuite())
suite.Run(t, NewHighAvailabilitySuite())
}

View File

@ -15,6 +15,6 @@ func NewKubernetesSuite() *KubernetesSuite {
}
func TestKubernetesSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -9,6 +9,7 @@ var ldapSuiteName = "LDAP"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/LDAP/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -18,7 +19,7 @@ func init() {
})
setup := func(suitePath string) error {
err := dockerEnvironment.Up(suitePath)
err := dockerEnvironment.Up()
if err != nil {
return err
@ -28,7 +29,7 @@ func init() {
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down(suitePath)
err := dockerEnvironment.Down()
return err
}

View File

@ -15,6 +15,6 @@ func NewLDAPSuite() *LDAPSuite {
}
func TestLDAPSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -9,6 +9,7 @@ var mariadbSuiteName = "Mariadb"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/Mariadb/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -19,7 +20,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -27,7 +28,7 @@ func init() {
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down(suitePath)
err := dockerEnvironment.Down()
return err
}

View File

@ -15,6 +15,6 @@ func NewMariadbSuite() *MariadbSuite {
}
func TestMariadbSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -9,6 +9,7 @@ var networkACLSuiteName = "NetworkACL"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/NetworkACL/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -20,7 +21,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -28,7 +29,7 @@ func init() {
}
teardown := func(suitePath string) error {
return dockerEnvironment.Down(suitePath)
return dockerEnvironment.Down()
}
GlobalRegistry.Register(networkACLSuiteName, Suite{

View File

@ -1,17 +1,102 @@
package suites
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type NetworkACLSuite struct {
*SeleniumSuite
suite.Suite
clients []*WebDriverSession
}
func NewNetworkACLSuite() *NetworkACLSuite {
return &NetworkACLSuite{SeleniumSuite: new(SeleniumSuite)}
return &NetworkACLSuite{clients: make([]*WebDriverSession, 3)}
}
func (s *NetworkACLSuite) createClient(idx int) {
wds, err := StartWebDriverWithProxy(fmt.Sprintf("http://proxy-client%d.example.com:3128", idx), 4444+idx)
if err != nil {
log.Fatal(err)
}
s.clients[idx] = wds
}
func (s *NetworkACLSuite) teardownClient(idx int) {
if err := s.clients[idx].Stop(); err != nil {
log.Fatal(err)
}
}
func (s *NetworkACLSuite) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.clients[0] = wds
for i := 1; i <= 2; i++ {
s.createClient(i)
}
}
func (s *NetworkACLSuite) TearDownSuite() {
if err := s.clients[0].Stop(); err != nil {
log.Fatal(err)
}
for i := 1; i <= 2; i++ {
s.teardownClient(i)
}
}
func (s *NetworkACLSuite) TestShouldAccessSecretUpon2FA() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL)
secret := s.clients[0].doRegisterThenLogout(ctx, s.T(), "john", "password")
s.clients[0].doVisit(s.T(), targetURL)
s.clients[0].verifyIsFirstFactorPage(ctx, s.T())
s.clients[0].doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
s.clients[0].verifyIsSecondFactorPage(ctx, s.T())
s.clients[0].doValidateTOTP(ctx, s.T(), secret)
s.clients[0].verifySecretAuthorized(ctx, s.T())
}
// from network 192.168.240.201/32
func (s *NetworkACLSuite) TestShouldAccessSecretUpon1FA() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL)
s.clients[1].doVisit(s.T(), targetURL)
s.clients[1].verifyIsFirstFactorPage(ctx, s.T())
s.clients[1].doLoginOneFactor(ctx, s.T(), "john", "password",
false, fmt.Sprintf("%s/secret.html", SecureBaseURL))
s.clients[1].verifySecretAuthorized(ctx, s.T())
}
// from network 192.168.240.202/32
func (s *NetworkACLSuite) TestShouldAccessSecretUpon0FA() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.clients[2].doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL))
s.clients[2].verifySecretAuthorized(ctx, s.T())
}
func TestNetworkACLSuite(t *testing.T) {
RunTypescriptSuite(t, networkACLSuiteName)
suite.Run(t, NewNetworkACLSuite())
}

View File

@ -9,6 +9,7 @@ var postgresSuiteName = "Postgres"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/Postgres/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -19,7 +20,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -27,7 +28,7 @@ func init() {
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down(suitePath)
err := dockerEnvironment.Down()
return err
}

View File

@ -15,6 +15,6 @@ func NewPostgresSuite() *PostgresSuite {
}
func TestPostgresSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -9,6 +9,7 @@ var shortTimeoutsSuiteName = "ShortTimeouts"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/ShortTimeouts/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -17,7 +18,7 @@ func init() {
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(suitePath); err != nil {
if err := dockerEnvironment.Up(); err != nil {
return err
}
@ -25,7 +26,7 @@ func init() {
}
teardown := func(suitePath string) error {
return dockerEnvironment.Down(suitePath)
return dockerEnvironment.Down()
}
GlobalRegistry.Register(shortTimeoutsSuiteName, Suite{

View File

@ -2,6 +2,8 @@ package suites
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ShortTimeoutsSuite struct {
@ -13,5 +15,6 @@ func NewShortTimeoutsSuite() *ShortTimeoutsSuite {
}
func TestShortTimeoutsSuite(t *testing.T) {
RunTypescriptSuite(t, shortTimeoutsSuiteName)
suite.Run(t, NewInactivityScenario())
suite.Run(t, NewRegulationScenario())
}

View File

@ -9,6 +9,7 @@ var standaloneSuiteName = "Standalone"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/Standalone/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -17,7 +18,7 @@ func init() {
})
setup := func(suitePath string) error {
err := dockerEnvironment.Up(suitePath)
err := dockerEnvironment.Up()
if err != nil {
return err
@ -27,7 +28,7 @@ func init() {
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down(suitePath)
err := dockerEnvironment.Down()
return err
}

View File

@ -1,22 +1,138 @@
package suites
import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type StandaloneSuite struct {
type StandaloneWebDriverSuite struct {
*SeleniumSuite
}
func NewStandaloneWebDriverSuite() *StandaloneWebDriverSuite {
return &StandaloneWebDriverSuite{SeleniumSuite: new(SeleniumSuite)}
}
func (s *StandaloneWebDriverSuite) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *StandaloneWebDriverSuite) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *StandaloneWebDriverSuite) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.WebDriverSession.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_ = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, "")
// Visit home page to change context
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
// Visit the login page and wait for redirection to 2FA page with success icon displayed
s.doVisit(s.T(), LoginBaseURL)
s.verifyIsSecondFactorPage(ctx, s.T())
// Check whether the success icon is displayed
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
}
type StandaloneSuite struct {
suite.Suite
}
func NewStandaloneSuite() *StandaloneSuite {
return &StandaloneSuite{SeleniumSuite: new(SeleniumSuite)}
return &StandaloneSuite{}
}
// Standard case using nginx
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyUnauthorize() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify", AutheliaBaseURL), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL)
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 401)
body, err := ioutil.ReadAll(res.Body)
s.Assert().NoError(err)
s.Assert().Equal(string(body), "Unauthorized")
}
// Standard case using Kubernetes
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, LoginBaseURL), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Original-URL", AdminBaseURL)
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := ioutil.ReadAll(res.Body)
s.Assert().NoError(err)
s.Assert().Equal(string(body), fmt.Sprintf("Found. Redirecting to %s?rd=%s", LoginBaseURL, AdminBaseURL))
}
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/verify?rd=%s", AutheliaBaseURL, LoginBaseURL), nil)
s.Assert().NoError(err)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "secure.example.com:8080")
req.Header.Set("X-Forwarded-URI", "/")
client := NewHTTPClient()
res, err := client.Do(req)
s.Assert().NoError(err)
s.Assert().Equal(res.StatusCode, 302)
body, err := ioutil.ReadAll(res.Body)
s.Assert().NoError(err)
s.Assert().Equal(string(body), fmt.Sprintf("Found. Redirecting to %s?rd=https://secure.example.com:8080/", LoginBaseURL))
}
func TestStandaloneWebDriverScenario(t *testing.T) {
suite.Run(t, NewStandaloneWebDriverSuite())
}
func TestStandaloneSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
suite.Run(t, NewBypassPolicyScenario())
suite.Run(t, NewBackendProtectionScenario())
suite.Run(t, NewResetPasswordScenario())
suite.Run(t, NewAvailableMethodsScenario([]string{"ONE-TIME PASSWORD"}))
RunTypescriptSuite(t, standaloneSuiteName)
suite.Run(t, NewStandaloneWebDriverSuite())
suite.Run(t, NewStandaloneSuite())
}

View File

@ -9,6 +9,7 @@ var traefikSuiteName = "Traefik"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"docker-compose.yml",
"internal/suites/Traefik/docker-compose.yml",
"example/compose/authelia/docker-compose.backend.yml",
"example/compose/authelia/docker-compose.frontend.yml",
"example/compose/nginx/backend/docker-compose.yml",
@ -17,7 +18,7 @@ func init() {
})
setup := func(suitePath string) error {
err := dockerEnvironment.Up(suitePath)
err := dockerEnvironment.Up()
if err != nil {
return err
@ -27,7 +28,7 @@ func init() {
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down(suitePath)
err := dockerEnvironment.Down()
return err
}

View File

@ -15,6 +15,6 @@ func NewTraefikSuite() *TraefikSuite {
}
func TestTraefikSuite(t *testing.T) {
suite.Run(t, NewOneFactorSuite())
suite.Run(t, NewTwoFactorSuite())
suite.Run(t, NewOneFactorScenario())
suite.Run(t, NewTwoFactorScenario())
}

View File

@ -1,14 +1,6 @@
package suites
import (
"context"
"errors"
"fmt"
"os"
"testing"
"github.com/clems4ever/authelia/internal/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/tebeka/selenium"
)
@ -24,55 +16,3 @@ type SeleniumSuite struct {
func (s *SeleniumSuite) WebDriver() selenium.WebDriver {
return s.WebDriverSession.WebDriver
}
// Wait wait until condition holds true
func (s *SeleniumSuite) Wait(ctx context.Context, condition selenium.Condition) error {
done := make(chan error, 1)
go func() {
done <- s.WebDriverSession.WebDriver.Wait(condition)
}()
select {
case <-ctx.Done():
return errors.New("waiting timeout reached")
case err := <-done:
return err
}
}
func rootPath() string {
rootPath := os.Getenv("ROOT_PATH")
// If env variable is not provided, use relative path.
if rootPath == "" {
rootPath = "../.."
}
return rootPath
}
func relativePath(path string) string {
return fmt.Sprintf("%s/%s", rootPath(), path)
}
// RunTypescriptSuite run the tests of the typescript suite
func RunTypescriptSuite(t *testing.T, suite string) {
forbidFlags := ""
if os.Getenv("ONLY_FORBIDDEN") == "true" {
forbidFlags = "--forbid-only --forbid-pending"
}
cmdline := "./node_modules/.bin/mocha" +
" --exit --require ts-node/register " + forbidFlags + " " +
fmt.Sprintf("test/suites/%s/test.ts", suite)
command := utils.CommandWithStdout("bash", "-c", cmdline)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
command.Dir = rootPath()
command.Env = append(
os.Environ(),
"ENVIRONMENT=dev",
fmt.Sprintf("TS_NODE_PROJECT=%s", "test/tsconfig.json"))
assert.NoError(t, command.Run())
}

View File

@ -1,8 +1,26 @@
package suites
import "context"
import (
"context"
"strings"
"testing"
func verifyBodyContains(ctx context.Context, s *SeleniumSuite, pattern string) {
bodyElement := WaitElementLocatedByTagName(ctx, s, "body")
WaitElementTextContains(ctx, s, bodyElement, pattern)
"github.com/stretchr/testify/require"
"github.com/tebeka/selenium"
)
func (wds *WebDriverSession) verifyBodyContains(ctx context.Context, t *testing.T, pattern string) {
err := wds.Wait(ctx, func(wd selenium.WebDriver) (bool, error) {
bodyElement := wds.WaitElementLocatedByTagName(ctx, t, "body")
require.NotNil(t, bodyElement)
content, err := bodyElement.Text()
if err != nil {
return false, err
}
return strings.Contains(content, pattern), nil
})
require.NoError(t, err)
}

View File

@ -1,7 +1,10 @@
package suites
import "context"
import (
"context"
"testing"
)
func verifyIsFirstFactorPage(ctx context.Context, s *SeleniumSuite) {
WaitElementLocatedByClassName(ctx, s, "first-factor-step")
func (wds *WebDriverSession) verifyIsFirstFactorPage(ctx context.Context, t *testing.T) {
wds.WaitElementLocatedByID(ctx, t, "first-factor-stage")
}

View File

@ -0,0 +1,11 @@
package suites
import (
"context"
"fmt"
"testing"
)
func (wds *WebDriverSession) verifyIsHome(ctx context.Context, t *testing.T) {
wds.verifyURLIs(ctx, t, fmt.Sprintf("%s/", HomeBaseURL))
}

View File

@ -1,7 +1,10 @@
package suites
import "context"
import (
"context"
"testing"
)
func verifyIsSecondFactorPage(ctx context.Context, s *SeleniumSuite) {
WaitElementLocatedByClassName(ctx, s, "second-factor-step")
func (wds *WebDriverSession) verifyIsSecondFactorPage(ctx context.Context, t *testing.T) {
wds.WaitElementLocatedByID(ctx, t, "second-factor-stage")
}

View File

@ -0,0 +1,10 @@
package suites
import (
"context"
"testing"
)
func (wds *WebDriverSession) verifyMailNotificationDisplayed(ctx context.Context, t *testing.T) {
wds.verifyNotificationDisplayed(ctx, t, "An email has been sent to your address to complete the process.")
}

View File

@ -1,9 +1,14 @@
package suites
import "context"
import (
"context"
"testing"
func verifyNotificationDisplayed(ctx context.Context, s *SeleniumSuite, message string) {
txt, err := WaitElementLocatedByClassName(ctx, s, "notification").Text()
s.Assert().NoError(err)
s.Assert().Equal(message, txt)
"github.com/stretchr/testify/assert"
)
func (wds *WebDriverSession) verifyNotificationDisplayed(ctx context.Context, t *testing.T, message string) {
el := wds.WaitElementLocatedByClassName(ctx, t, "notification")
assert.NotNil(t, el)
wds.WaitElementTextContains(ctx, t, el, message)
}

View File

@ -1,7 +1,10 @@
package suites
import "context"
import (
"context"
"testing"
)
func verifySecretAuthorized(ctx context.Context, s *SeleniumSuite) {
verifyBodyContains(ctx, s, "This is a very important secret!")
func (wds *WebDriverSession) verifySecretAuthorized(ctx context.Context, t *testing.T) {
wds.verifyBodyContains(ctx, t, "This is a very important secret!")
}

View File

@ -2,21 +2,21 @@ package suites
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tebeka/selenium"
)
func verifyURLIs(ctx context.Context, s *SeleniumSuite, url string) {
err := s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
func (wds *WebDriverSession) verifyURLIs(ctx context.Context, t *testing.T, url string) {
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
currentURL, err := driver.CurrentURL()
if err != nil {
return false, err
}
return currentURL == url, nil
})
assert.NoError(s.T(), err)
assert.NoError(t, err)
}

View File

@ -2,11 +2,13 @@ package suites
import (
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tebeka/selenium"
"github.com/tebeka/selenium/chrome"
)
@ -17,9 +19,8 @@ type WebDriverSession struct {
WebDriver selenium.WebDriver
}
// StartWebDriver create a selenium session
func StartWebDriver() (*WebDriverSession, error) {
port := 4444
// StartWebDriverWithProxy create a selenium session
func StartWebDriverWithProxy(proxy string, port int) (*WebDriverSession, error) {
service, err := selenium.NewChromeDriverService("/usr/bin/chromedriver", port)
if err != nil {
@ -34,6 +35,10 @@ func StartWebDriver() (*WebDriverSession, error) {
chromeCaps.Args = append(chromeCaps.Args, "--headless")
}
if proxy != "" {
chromeCaps.Args = append(chromeCaps.Args, fmt.Sprintf("--proxy-server=%s", proxy))
}
caps := selenium.Capabilities{}
caps.AddChrome(chromeCaps)
@ -49,6 +54,11 @@ func StartWebDriver() (*WebDriverSession, error) {
}, nil
}
// StartWebDriver create a selenium session
func StartWebDriver() (*WebDriverSession, error) {
return StartWebDriverWithProxy("", 4444)
}
// Stop stop the selenium session
func (wds *WebDriverSession) Stop() error {
err := wds.WebDriver.Quit()
@ -73,9 +83,24 @@ func WithWebdriver(fn func(webdriver selenium.WebDriver) error) error {
return fn(wds.WebDriver)
}
func waitElementLocated(ctx context.Context, s *SeleniumSuite, by, value string) selenium.WebElement {
// Wait wait until condition holds true
func (wds *WebDriverSession) Wait(ctx context.Context, condition selenium.Condition) error {
done := make(chan error, 1)
go func() {
done <- wds.WebDriver.Wait(condition)
}()
select {
case <-ctx.Done():
return errors.New("waiting timeout reached")
case err := <-done:
return err
}
}
func (wds *WebDriverSession) waitElementLocated(ctx context.Context, t *testing.T, by, value string) selenium.WebElement {
var el selenium.WebElement
err := s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
var err error
el, err = driver.FindElement(by, value)
@ -89,31 +114,65 @@ func waitElementLocated(ctx context.Context, s *SeleniumSuite, by, value string)
return el != nil, nil
})
assert.NoError(s.T(), err)
assert.NotNil(s.T(), el, "Element has not been located")
require.NoError(t, err)
require.NotNil(t, el)
return el
}
func (wds *WebDriverSession) waitElementsLocated(ctx context.Context, t *testing.T, by, value string) []selenium.WebElement {
var el []selenium.WebElement
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
var err error
el, err = driver.FindElements(by, value)
if err != nil {
if strings.Contains(err.Error(), "no such element") {
return false, nil
}
return false, err
}
return el != nil, nil
})
require.NoError(t, err)
require.NotNil(t, el)
return el
}
// WaitElementLocatedByID wait an element is located by id
func WaitElementLocatedByID(ctx context.Context, s *SeleniumSuite, id string) selenium.WebElement {
return waitElementLocated(ctx, s, selenium.ByID, id)
func (wds *WebDriverSession) WaitElementLocatedByID(ctx context.Context, t *testing.T, id string) selenium.WebElement {
return wds.waitElementLocated(ctx, t, selenium.ByID, id)
}
// WaitElementLocatedByTagName wait an element is located by tag name
func WaitElementLocatedByTagName(ctx context.Context, s *SeleniumSuite, tagName string) selenium.WebElement {
return waitElementLocated(ctx, s, selenium.ByTagName, tagName)
func (wds *WebDriverSession) WaitElementLocatedByTagName(ctx context.Context, t *testing.T, tagName string) selenium.WebElement {
return wds.waitElementLocated(ctx, t, selenium.ByTagName, tagName)
}
// WaitElementLocatedByClassName wait an element is located by class name
func WaitElementLocatedByClassName(ctx context.Context, s *SeleniumSuite, className string) selenium.WebElement {
return waitElementLocated(ctx, s, selenium.ByClassName, className)
func (wds *WebDriverSession) WaitElementLocatedByClassName(ctx context.Context, t *testing.T, className string) selenium.WebElement {
return wds.waitElementLocated(ctx, t, selenium.ByClassName, className)
}
// WaitElementLocatedByLinkText wait an element is located by link text
func (wds *WebDriverSession) WaitElementLocatedByLinkText(ctx context.Context, t *testing.T, linkText string) selenium.WebElement {
return wds.waitElementLocated(ctx, t, selenium.ByLinkText, linkText)
}
// WaitElementLocatedByCSSSelector wait an element is located by class name
func (wds *WebDriverSession) WaitElementLocatedByCSSSelector(ctx context.Context, t *testing.T, cssSelector string) selenium.WebElement {
return wds.waitElementLocated(ctx, t, selenium.ByCSSSelector, cssSelector)
}
// WaitElementsLocatedByCSSSelector wait an element is located by CSS selector
func (wds *WebDriverSession) WaitElementsLocatedByCSSSelector(ctx context.Context, t *testing.T, cssSelector string) []selenium.WebElement {
return wds.waitElementsLocated(ctx, t, selenium.ByCSSSelector, cssSelector)
}
// WaitElementTextContains wait the text of an element contains a pattern
func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element selenium.WebElement, pattern string) {
assert.NotNil(s.T(), element)
s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
func (wds *WebDriverSession) WaitElementTextContains(ctx context.Context, t *testing.T, element selenium.WebElement, pattern string) {
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
text, err := element.Text()
if err != nil {
@ -122,4 +181,5 @@ func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element sele
return strings.Contains(text, pattern), nil
})
require.NoError(t, err)
}

22
internal/utils/clock.go Normal file
View File

@ -0,0 +1,22 @@
package utils
import "time"
// Clock is an interface for a clock
type Clock interface {
Now() time.Time
After(d time.Duration) <-chan time.Time
}
// RealClock is the implementation of a clock for production code
type RealClock struct{}
// Now return the current time
func (RealClock) Now() time.Time {
return time.Now()
}
// After return a channel receiving the time after the defined duration
func (RealClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}

View File

@ -6,6 +6,8 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
@ -13,9 +15,22 @@ import (
log "github.com/sirupsen/logrus"
)
// Command create a command at the project root
func Command(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
// By default set the working directory to the project root directory
wd, _ := os.Getwd()
for !strings.HasSuffix(wd, "authelia") {
wd = filepath.Dir(wd)
}
cmd.Dir = wd
return cmd
}
// CommandWithStdout create a command forwarding stdout and stderr to the OS streams
func CommandWithStdout(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
cmd := Command(name, args...)
if log.GetLevel() > log.InfoLevel {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

3915
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
{
"name": "authelia",
"version": "3.16.3",
"description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F",
"engines": {
"node": ">=8.0.0 <10.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/clems4ever/authelia"
},
"author": "Clement Michaud <clement.michaud34@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/clems4ever/authelia/issues"
},
"apidoc": {
"title": "Authelia API documentation"
},
"dependencies": {},
"devDependencies": {
"@types/mocha": "^5.2.6",
"@types/node-fetch": "^2.1.4",
"@types/query-string": "^5.1.0",
"@types/request": "^2.0.5",
"@types/request-promise": "^4.1.38",
"@types/selenium-webdriver": "^3.0.16",
"@types/speakeasy": "^2.0.2",
"chromedriver": "^77.0.0",
"ejs": "^2.6.2",
"mocha": "^6.1.4",
"node-fetch": "^2.3.0",
"query-string": "^6.0.0",
"readable-stream": "^2.3.3",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"selenium-webdriver": "^4.0.0-alpha.4",
"speakeasy": "^2.0.0",
"ts-node": "^6.0.1",
"tslint": "^5.2.0",
"typescript": "^2.9.2"
}
}

View File

@ -1,10 +0,0 @@
var { setup } = require(`../test/suites/${process.argv[2]}/environment`);
(async function() {
try {
await setup();
} catch(err) {
console.error(err);
process.exit(1);
}
})()

View File

@ -1,10 +0,0 @@
var { teardown } = require(`../test/suites/${process.argv[2]}/environment`);
(async function() {
try {
await teardown();
} catch(err) {
console.error(err);
process.exit(1);
}
})()

View File

@ -1,13 +0,0 @@
const { lstatSync, readdirSync } = require('fs')
const { join } = require('path')
const isDirectory = source => lstatSync(source).isDirectory()
const getDirectories = source =>
readdirSync(source)
.map(name => join(source, name))
.filter(isDirectory)
.map(x => x.split('/').slice(-1)[0])
module.exports = function() {
return getDirectories('test/suites/');
}

View File

@ -1,18 +0,0 @@
var spawn = require('child_process').spawn;
function exec(cmd) {
return new Promise((resolve, reject) => {
const command = spawn(cmd, {shell: true, env: process.env});
command.stdout.pipe(process.stdout);
command.stderr.pipe(process.stderr);
command.on('exit', function(statusCode) {
if (statusCode != 0) {
reject(new Error('Command \'' + cmd + '\' has exited with status ' + statusCode + '.'));
return;
}
resolve();
})
})
}
module.exports = { exec }

View File

@ -1,7 +0,0 @@
import SeleniumWebdriver, { WebDriver, Locator } from "selenium-webdriver";
export default async function(driver: WebDriver, locator: Locator, timeout: number = 5000) {
const el = await driver.wait(
SeleniumWebdriver.until.elementLocated(locator), timeout);
await el.click();
};

View File

@ -1,8 +0,0 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, linkText: string, timeout: number = 5000) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.linkText(linkText)), timeout);
await element.click();
};

View File

@ -1,9 +0,0 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, fieldName: string, text: string, timeout: number = 5000) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.name(fieldName)), timeout)
await element.sendKeys(text);
};

View File

@ -1,17 +0,0 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(
driver: WebDriver,
username: string,
password: string,
keepMeLoggedIn: boolean = false,
timeout: number = 5000) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), timeout)
await driver.findElement(SeleniumWebdriver.By.id("username")).sendKeys(username);
await driver.findElement(SeleniumWebdriver.By.id("password")).sendKeys(password);
if (keepMeLoggedIn) {
await driver.findElement(SeleniumWebdriver.By.id("remember-checkbox")).click();
}
await driver.findElement(SeleniumWebdriver.By.tagName("button")).click();
};

View File

@ -1,11 +0,0 @@
import FillLoginPageAndClick from "./FillLoginPageAndClick";
import ValidateTotp from "./ValidateTotp";
import { WebDriver } from "selenium-webdriver";
import VisitPageAndWaitUrlIs from "./behaviors/VisitPageAndWaitUrlIs";
// Validate the two factors!
export default async function(driver: WebDriver, user: string, secret: string, url: string, timeout: number = 5000) {
await VisitPageAndWaitUrlIs(driver, `https://login.example.com:8080/#/?rd=${url}`, timeout);
await FillLoginPageAndClick(driver, user, 'password', false, timeout);
await ValidateTotp(driver, secret, timeout);
}

View File

@ -1,37 +0,0 @@
import Bluebird = require("bluebird");
import Fs = require("fs");
import Request = require("request-promise");
export async function GetLinkFromFile() {
const data = await Bluebird.promisify(Fs.readFile)(
"/tmp/authelia/notification.txt")
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data.toLocaleString());
if (match == null) {
throw new Error('No match');
}
return match[1];
};
export async function GetLinkFromEmail() {
const data = await Request({
method: "GET",
uri: "https://mail.example.com:8080/messages",
json: true,
rejectUnauthorized: false,
});
const messageId = data[data.length - 1].id;
const data2 = await Request({
method: "GET",
rejectUnauthorized: false,
uri: `https://mail.example.com:8080/messages/${messageId}.html`,
});
const regexp = new RegExp(/<a href="(.+)" class="button">.*<\/a>/);
const match = regexp.exec(data2);
if (match == null) {
throw new Error('No match');
}
return match[1];
};

View File

@ -1,10 +0,0 @@
import RegisterTotp from './RegisterTotp';
import LoginAs from './LoginAs';
import { WebDriver } from 'selenium-webdriver';
import VerifyIsSecondFactorStage from './assertions/VerifyIsSecondFactorStage';
export default async function(driver: WebDriver, user: string, password: string, email: boolean = false, timeout: number = 5000) {
await LoginAs(driver, user, password, undefined, timeout);
await VerifyIsSecondFactorStage(driver, timeout);
return RegisterTotp(driver, email, timeout);
}

Some files were not shown because too many files have changed in this diff Show More