Rewrite and fix remaining suites in Go.
parent
373911d199
commit
c78a732c6a
|
@ -8,7 +8,6 @@ npm-debug.log*
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
src/.baseDir.ts
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
|
|
|
@ -55,10 +55,6 @@ func runCommand(cmd string, args ...string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func installNpmPackages() {
|
|
||||||
runCommand("npm", "ci")
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCommandExist(cmd string) {
|
func checkCommandExist(cmd string) {
|
||||||
fmt.Print("Checking if '" + cmd + "' command is installed...")
|
fmt.Print("Checking if '" + cmd + "' command is installed...")
|
||||||
command := exec.Command("bash", "-c", "command -v "+cmd)
|
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")
|
log.Fatal("GOPATH is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapPrintln("Installing NPM packages for development...")
|
|
||||||
installNpmPackages()
|
|
||||||
|
|
||||||
bootstrapPrintln("Building development Docker images...")
|
bootstrapPrintln("Building development Docker images...")
|
||||||
buildHelperDockerImages()
|
buildHelperDockerImages()
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/clems4ever/authelia/internal/server"
|
"github.com/clems4ever/authelia/internal/server"
|
||||||
"github.com/clems4ever/authelia/internal/session"
|
"github.com/clems4ever/authelia/internal/session"
|
||||||
"github.com/clems4ever/authelia/internal/storage"
|
"github.com/clems4ever/authelia/internal/storage"
|
||||||
|
"github.com/clems4ever/authelia/internal/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,12 +52,15 @@ func main() {
|
||||||
|
|
||||||
switch config.LogsLevel {
|
switch config.LogsLevel {
|
||||||
case "info":
|
case "info":
|
||||||
|
logging.Logger().Info("Logging severity set to info")
|
||||||
logging.SetLevel(logrus.InfoLevel)
|
logging.SetLevel(logrus.InfoLevel)
|
||||||
break
|
break
|
||||||
case "debug":
|
case "debug":
|
||||||
|
logging.Logger().Info("Logging severity set to debug")
|
||||||
logging.SetLevel(logrus.DebugLevel)
|
logging.SetLevel(logrus.DebugLevel)
|
||||||
break
|
break
|
||||||
case "trace":
|
case "trace":
|
||||||
|
logging.Logger().Info("Logging severity set to trace")
|
||||||
logging.SetLevel(logrus.TraceLevel)
|
logging.SetLevel(logrus.TraceLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,9 +94,10 @@ func main() {
|
||||||
log.Fatalf("Unrecognized notifier")
|
log.Fatalf("Unrecognized notifier")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clock := utils.RealClock{}
|
||||||
authorizer := authorization.NewAuthorizer(*config.AccessControl)
|
authorizer := authorization.NewAuthorizer(*config.AccessControl)
|
||||||
sessionProvider := session.NewProvider(config.Session)
|
sessionProvider := session.NewProvider(config.Session)
|
||||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider)
|
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||||
|
|
||||||
providers := middlewares.Providers{
|
providers := middlewares.Providers{
|
||||||
Authorizer: authorizer,
|
Authorizer: authorizer,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
version: '3'
|
version: "3"
|
||||||
services:
|
services:
|
||||||
authelia-backend:
|
authelia-backend:
|
||||||
build:
|
build:
|
||||||
|
@ -12,7 +12,6 @@ services:
|
||||||
- "${GOPATH}:/go"
|
- "${GOPATH}:/go"
|
||||||
- "/tmp/authelia:/tmp/authelia"
|
- "/tmp/authelia:/tmp/authelia"
|
||||||
environment:
|
environment:
|
||||||
- SUITE_PATH=${SUITE_PATH}
|
|
||||||
- ENVIRONMENT=dev
|
- ENVIRONMENT=dev
|
||||||
networks:
|
networks:
|
||||||
authelianet:
|
authelianet:
|
||||||
|
|
|
@ -4,4 +4,7 @@ set -x
|
||||||
|
|
||||||
go get github.com/cespare/reflex
|
go get github.com/cespare/reflex
|
||||||
|
|
||||||
|
mkdir -p /var/lib/authelia
|
||||||
|
mkdir -p /etc/authelia
|
||||||
|
|
||||||
reflex -c /resources/reflex.conf
|
reflex -c /resources/reflex.conf
|
|
@ -27,7 +27,4 @@ retry() {
|
||||||
# Build the binary
|
# Build the binary
|
||||||
go build -o /tmp/authelia/authelia-tmp cmd/authelia/main.go
|
go build -o /tmp/authelia/authelia-tmp cmd/authelia/main.go
|
||||||
|
|
||||||
# Run the temporary binary
|
retry 3 /tmp/authelia/authelia-tmp -config /etc/authelia/configuration.yml
|
||||||
cd $SUITE_PATH
|
|
||||||
|
|
||||||
retry 3 /tmp/authelia/authelia-tmp -config ${SUITE_PATH}/configuration.yml
|
|
|
@ -35,13 +35,15 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password)
|
userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
|
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, false)
|
||||||
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, userPasswordOk)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage)
|
ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage)
|
||||||
|
|
|
@ -3,8 +3,10 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/mocks"
|
"github.com/clems4ever/authelia/internal/mocks"
|
||||||
|
"github.com/clems4ever/authelia/internal/models"
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/authentication"
|
"github.com/clems4ever/authelia/internal/authentication"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
@ -70,6 +72,32 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
|
||||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
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() {
|
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
|
||||||
s.mock.UserProviderMock.
|
s.mock.UserProviderMock.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/regulation"
|
"github.com/clems4ever/authelia/internal/regulation"
|
||||||
"github.com/clems4ever/authelia/internal/storage"
|
"github.com/clems4ever/authelia/internal/storage"
|
||||||
|
@ -32,11 +33,34 @@ type MockAutheliaCtx struct {
|
||||||
NotifierMock *MockNotifier
|
NotifierMock *MockNotifier
|
||||||
|
|
||||||
UserSession *session.UserSession
|
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
|
// NewMockAutheliaCtx create an instance of AutheliaCtx mock
|
||||||
func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
mockAuthelia := new(MockAutheliaCtx)
|
mockAuthelia := new(MockAutheliaCtx)
|
||||||
|
mockAuthelia.Clock = TestingClock{}
|
||||||
|
|
||||||
configuration := schema.Configuration{
|
configuration := schema.Configuration{
|
||||||
AccessControl: new(schema.AccessControlConfiguration),
|
AccessControl: new(schema.AccessControlConfiguration),
|
||||||
|
@ -75,7 +99,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
providers.SessionProvider = session.NewProvider(
|
providers.SessionProvider = session.NewProvider(
|
||||||
configuration.Session)
|
configuration.Session)
|
||||||
|
|
||||||
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider)
|
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock)
|
||||||
|
|
||||||
request := &fasthttp.RequestCtx{}
|
request := &fasthttp.RequestCtx{}
|
||||||
// Set a cookie to identify this client throughout the test
|
// Set a cookie to identify this client throughout the test
|
||||||
|
@ -94,6 +118,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
// Close close the mock
|
// Close close the mock
|
||||||
func (m *MockAutheliaCtx) Close() {
|
func (m *MockAutheliaCtx) Close() {
|
||||||
m.Hook.Reset()
|
m.Hook.Reset()
|
||||||
|
m.Ctrl.Finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert200KO assert an error response from the service.
|
// Assert200KO assert an error response from the service.
|
||||||
|
|
|
@ -7,11 +7,13 @@ import (
|
||||||
"github.com/clems4ever/authelia/internal/configuration/schema"
|
"github.com/clems4ever/authelia/internal/configuration/schema"
|
||||||
"github.com/clems4ever/authelia/internal/models"
|
"github.com/clems4ever/authelia/internal/models"
|
||||||
"github.com/clems4ever/authelia/internal/storage"
|
"github.com/clems4ever/authelia/internal/storage"
|
||||||
|
"github.com/clems4ever/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRegulator create a regulator instance.
|
// 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 := &Regulator{storageProvider: provider}
|
||||||
|
regulator.clock = clock
|
||||||
if configuration != nil {
|
if configuration != nil {
|
||||||
if configuration.FindTime > configuration.BanTime {
|
if configuration.FindTime > configuration.BanTime {
|
||||||
panic(fmt.Errorf("find_time cannot be greater than ban_time"))
|
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{
|
return r.storageProvider.AppendAuthenticationLog(models.AuthenticationAttempt{
|
||||||
Username: username,
|
Username: username,
|
||||||
Successful: successful,
|
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 {
|
if !r.enabled {
|
||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := r.clock.Now()
|
||||||
|
|
||||||
// TODO(c.michaud): make sure FindTime < BanTime.
|
// TODO(c.michaud): make sure FindTime < BanTime.
|
||||||
attempts, err := r.storageProvider.LoadLatestAuthenticationLogs(username, now.Add(-r.banTime))
|
attempts, err := r.storageProvider.LoadLatestAuthenticationLogs(username, now.Add(-r.banTime))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/storage"
|
"github.com/clems4ever/authelia/internal/storage"
|
||||||
|
"github.com/clems4ever/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Regulator an authentication regulator preventing attackers to brute force the service.
|
// Regulator an authentication regulator preventing attackers to brute force the service.
|
||||||
|
@ -18,4 +19,6 @@ type Regulator struct {
|
||||||
banTime time.Duration
|
banTime time.Duration
|
||||||
|
|
||||||
storageProvider storage.Provider
|
storageProvider storage.Provider
|
||||||
|
|
||||||
|
clock utils.Clock
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ jwt_secret: unsecure_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
@ -31,14 +31,13 @@ duo_api:
|
||||||
access_control:
|
access_control:
|
||||||
default_policy: bypass
|
default_policy: bypass
|
||||||
rules:
|
rules:
|
||||||
- domain: 'public.example.com'
|
- domain: "public.example.com"
|
||||||
policy: bypass
|
policy: bypass
|
||||||
- domain: 'secure.example.com'
|
- domain: "secure.example.com"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
notifier:
|
notifier:
|
||||||
smtp:
|
smtp:
|
||||||
host: smtp
|
host: smtp
|
||||||
port: 1025
|
port: 1025
|
||||||
sender: admin@example.com
|
sender: admin@example.com
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
port: 9091
|
port: 9091
|
||||||
|
|
||||||
logs_level: debug
|
logs_level: trace
|
||||||
|
|
||||||
jwt_secret: very_important_secret
|
jwt_secret: very_important_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
@ -54,33 +54,32 @@ access_control:
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: '*.example.com'
|
- domain: "*.example.com"
|
||||||
subject: "group:admins"
|
subject: "group:admins"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/john/.*$'
|
- "^/users/john/.*$"
|
||||||
subject: "user:john"
|
subject: "user:john"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/harry/.*$'
|
- "^/users/harry/.*$"
|
||||||
subject: "user:harry"
|
subject: "user:harry"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: '*.mail.example.com'
|
- domain: "*.mail.example.com"
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/bob/.*$'
|
- "^/users/bob/.*$"
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
|
||||||
# Configuration of the authentication regulation mechanism.
|
# Configuration of the authentication regulation mechanism.
|
||||||
regulation:
|
regulation:
|
||||||
# Set it to 0 to disable max_retries.
|
# Set it to 0 to disable max_retries.
|
||||||
|
@ -98,4 +97,3 @@ notifier:
|
||||||
host: smtp
|
host: smtp
|
||||||
port: 1025
|
port: 1025
|
||||||
sender: admin@example.com
|
sender: admin@example.com
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
authelia-backend:
|
||||||
|
volumes:
|
||||||
|
- "./internal/suites/HighAvailability/configuration.yml:/etc/authelia/configuration.yml:ro"
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
authelia-backend:
|
||||||
|
volumes:
|
||||||
|
- "./internal/suites/LDAP/configuration.yml:/etc/authelia/configuration.yml:ro"
|
|
@ -12,7 +12,7 @@ jwt_secret: very_important_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
authelia-backend:
|
||||||
|
volumes:
|
||||||
|
- "./internal/suites/Mariadb/configuration.yml:/etc/authelia/configuration.yml:ro"
|
|
@ -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"
|
|
@ -12,7 +12,7 @@ jwt_secret: very_important_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
|
|
@ -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"
|
|
@ -12,7 +12,7 @@ default_redirection_url: https://home.example.com:8080/
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
|
|
@ -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"
|
|
@ -12,7 +12,7 @@ jwt_secret: very_important_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
@ -22,7 +22,7 @@ session:
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
path: db.sqlite3
|
path: /tmp/authelia/db.sqlite3
|
||||||
|
|
||||||
totp:
|
totp:
|
||||||
issuer: example.com
|
issuer: example.com
|
||||||
|
@ -40,33 +40,32 @@ access_control:
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: '*.example.com'
|
- domain: "*.example.com"
|
||||||
subject: "group:admins"
|
subject: "group:admins"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/john/.*$'
|
- "^/users/john/.*$"
|
||||||
subject: "user:john"
|
subject: "user:john"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/harry/.*$'
|
- "^/users/harry/.*$"
|
||||||
subject: "user:harry"
|
subject: "user:harry"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: '*.mail.example.com'
|
- domain: "*.mail.example.com"
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- '^/users/bob/.*$'
|
- "^/users/bob/.*$"
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
|
||||||
regulation:
|
regulation:
|
||||||
# Set it to 0 to disable max_retries.
|
# Set it to 0 to disable max_retries.
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
|
@ -80,4 +79,3 @@ notifier:
|
||||||
host: smtp
|
host: smtp
|
||||||
port: 1025
|
port: 1025
|
||||||
sender: admin@example.com
|
sender: admin@example.com
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -1,28 +1,20 @@
|
||||||
###############################################################
|
|
||||||
# Users Database #
|
|
||||||
###############################################################
|
|
||||||
|
|
||||||
# This file can be used if you do not have an LDAP set up.
|
|
||||||
|
|
||||||
users:
|
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:
|
bob:
|
||||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||||
email: bob.dylan@authelia.com
|
email: bob.dylan@authelia.com
|
||||||
groups:
|
groups:
|
||||||
- dev
|
- dev
|
||||||
|
harry:
|
||||||
|
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||||
|
email: harry.potter@authelia.com
|
||||||
|
groups: []
|
||||||
james:
|
james:
|
||||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
password: '{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/'
|
||||||
email: james.dean@authelia.com
|
email: james.dean@authelia.com
|
||||||
|
groups: []
|
||||||
|
john:
|
||||||
|
password: '{CRYPT}$6$rounds=50000$LnfgDsc2WD8F2qNf$0gcCt8jlqAGZRv2ee3mCFsfAr1P4N7kESWEf36Xtw6OjkhAcQuGVOBHXp0lFuZbppa7YlgHk3VD28aSQu9U9S1'
|
||||||
|
email: john.doe@authelia.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
|
- dev
|
||||||
|
|
|
@ -10,7 +10,7 @@ jwt_secret: unsecure_secret
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
|
|
@ -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"
|
|
@ -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()
|
||||||
|
}
|
|
@ -1,24 +1,21 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func doHTTPGetQuery(s *SeleniumSuite, url string) []byte {
|
func doHTTPGetQuery(t *testing.T, url string) []byte {
|
||||||
tr := &http.Transport{
|
client := NewHTTPClient()
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
|
|
@ -2,46 +2,57 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func doFillLoginPageAndClick(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) {
|
func (wds *WebDriverSession) doFillLoginPageAndClick(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) {
|
||||||
usernameElement := WaitElementLocatedByID(ctx, s, "username")
|
usernameElement := wds.WaitElementLocatedByID(ctx, t, "username-textfield")
|
||||||
err := usernameElement.SendKeys(username)
|
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)
|
err = passwordElement.SendKeys(password)
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
if keepMeLoggedIn {
|
if keepMeLoggedIn {
|
||||||
keepMeLoggedInElement := WaitElementLocatedByID(ctx, s, "remember-checkbox")
|
keepMeLoggedInElement := wds.WaitElementLocatedByID(ctx, t, "remember-checkbox")
|
||||||
err = keepMeLoggedInElement.Click()
|
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()
|
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) {
|
// Login 1FA
|
||||||
doVisitLoginPage(ctx, s, targetURL)
|
func (wds *WebDriverSession) doLoginOneFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, targetURL string) {
|
||||||
doFillLoginPageAndClick(ctx, s, username, password, keepMeLoggedIn)
|
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) {
|
// Login 1FA and 2FA subsequently (must already be registered)
|
||||||
doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, targetURL)
|
func (wds *WebDriverSession) doLoginTwoFactor(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) {
|
||||||
verifyIsSecondFactorPage(ctx, s)
|
wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, targetURL)
|
||||||
doValidateTOTP(ctx, s, otpSecret)
|
wds.verifyIsSecondFactorPage(ctx, t)
|
||||||
|
wds.doValidateTOTP(ctx, t, otpSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doLoginAndRegisterTOTP(ctx context.Context, s *SeleniumSuite, username, password string, keepMeLoggedIn bool) string {
|
// Login 1FA and register 2FA.
|
||||||
doLoginOneFactor(ctx, s, username, password, keepMeLoggedIn, "")
|
func (wds *WebDriverSession) doLoginAndRegisterTOTP(ctx context.Context, t *testing.T, username, password string, keepMeLoggedIn bool) string {
|
||||||
secret := doRegisterTOTP(ctx, s)
|
wds.doLoginOneFactor(ctx, t, username, password, keepMeLoggedIn, "")
|
||||||
s.Assert().NotNil(secret)
|
secret := wds.doRegisterTOTP(ctx, t)
|
||||||
doVisit(s, LoginBaseURL)
|
wds.doVisit(t, LoginBaseURL)
|
||||||
verifyIsSecondFactorPage(ctx, s)
|
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
|
return secret
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func doLogout(ctx context.Context, s *SeleniumSuite) {
|
func (wds *WebDriverSession) doLogout(ctx context.Context, t *testing.T) {
|
||||||
doVisit(s, "https://login.example.com:8080/#/logout")
|
wds.doVisit(t, fmt.Sprintf("%s%s", LoginBaseURL, "/logout"))
|
||||||
verifyIsFirstFactorPage(ctx, s)
|
wds.verifyIsFirstFactorPage(ctx, t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package suites
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -13,22 +13,20 @@ type message struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func doGetLinkFromLastMail(s *SeleniumSuite) string {
|
func doGetLinkFromLastMail(t *testing.T) string {
|
||||||
res := doHTTPGetQuery(s, fmt.Sprintf("%s/messages", MailBaseURL))
|
res := doHTTPGetQuery(t, fmt.Sprintf("%s/messages", MailBaseURL))
|
||||||
messages := make([]message, 0)
|
messages := make([]message, 0)
|
||||||
err := json.Unmarshal(res, &messages)
|
err := json.Unmarshal(res, &messages)
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(s.T(), len(messages), 0)
|
assert.Greater(t, len(messages), 0)
|
||||||
|
|
||||||
messageID := messages[len(messages)-1].ID
|
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>`)
|
re := regexp.MustCompile(`<a href="(.+)" class="button">.*<\/a>`)
|
||||||
matches := re.FindStringSubmatch(string(res))
|
matches := re.FindStringSubmatch(string(res))
|
||||||
|
|
||||||
if len(matches) != 2 {
|
assert.Len(t, matches, 2, "Number of match for link in email is not equal to one")
|
||||||
log.Fatal("Number of match for link in email is not equal to one")
|
|
||||||
}
|
|
||||||
return matches[1]
|
return matches[1]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func doRegisterThenLogout(ctx context.Context, s *SeleniumSuite, username, password string) string {
|
func (wds *WebDriverSession) doRegisterThenLogout(ctx context.Context, t *testing.T, username, password string) string {
|
||||||
secret := doLoginAndRegisterTOTP(ctx, s, username, password, false)
|
secret := wds.doLoginAndRegisterTOTP(ctx, t, username, password, false)
|
||||||
doLogout(ctx, s)
|
wds.doLogout(ctx, t)
|
||||||
return secret
|
return secret
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -2,24 +2,35 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func doRegisterTOTP(ctx context.Context, s *SeleniumSuite) string {
|
func (wds *WebDriverSession) doRegisterTOTP(ctx context.Context, t *testing.T) string {
|
||||||
WaitElementLocatedByClassName(ctx, s, "register-totp").Click()
|
wds.WaitElementLocatedByID(ctx, t, "register-link").Click()
|
||||||
verifyBodyContains(ctx, s, "Please check your e-mails")
|
wds.verifyMailNotificationDisplayed(ctx, t)
|
||||||
link := doGetLinkFromLastMail(s)
|
link := doGetLinkFromLastMail(t)
|
||||||
doVisit(s, link)
|
wds.doVisit(t, link)
|
||||||
secret, err := WaitElementLocatedByClassName(ctx, s, "base32-secret").Text()
|
secret, err := wds.WaitElementLocatedByID(ctx, t, "base32-secret").GetAttribute("value")
|
||||||
s.Assert().NoError(err)
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, "", secret)
|
||||||
|
assert.NotNil(t, secret)
|
||||||
return secret
|
return secret
|
||||||
}
|
}
|
||||||
|
|
||||||
func doValidateTOTP(ctx context.Context, s *SeleniumSuite, secret string) {
|
func (wds *WebDriverSession) doEnterOTP(ctx context.Context, t *testing.T, code string) {
|
||||||
code, err := totp.GenerateCode(secret, time.Now())
|
inputs := wds.WaitElementsLocatedByCSSSelector(ctx, t, "#otp-input input")
|
||||||
s.Assert().NoError(err)
|
|
||||||
WaitElementLocatedByID(ctx, s, "totp-token").SendKeys(code)
|
for i := 0; i < 6; i++ {
|
||||||
WaitElementLocatedByID(ctx, s, "totp-button").Click()
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,25 +3,25 @@ package suites
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func doVisit(s *SeleniumSuite, url string) {
|
func (wds *WebDriverSession) doVisit(t *testing.T, url string) {
|
||||||
err := s.WebDriver().Get(url)
|
err := wds.WebDriver.Get(url)
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doVisitAndVerifyURLIs(ctx context.Context, s *SeleniumSuite, url string) {
|
func (wds *WebDriverSession) doVisitAndVerifyURLIs(ctx context.Context, t *testing.T, url string) {
|
||||||
doVisit(s, url)
|
wds.doVisit(t, url)
|
||||||
verifyURLIs(ctx, s, 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 := ""
|
suffix := ""
|
||||||
if targetURL != "" {
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import "fmt"
|
||||||
var BaseDomain = "example.com:8080"
|
var BaseDomain = "example.com:8080"
|
||||||
|
|
||||||
// LoginBaseURL the base URL of the login portal
|
// 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
|
// SingleFactorBaseURL the base URL of the singlefactor domain
|
||||||
var SingleFactorBaseURL = fmt.Sprintf("https://singlefactor.%s", BaseDomain)
|
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)
|
var MailBaseURL = fmt.Sprintf("https://mail.%s", BaseDomain)
|
||||||
|
|
||||||
// HomeBaseURL the base URL of the home domain
|
// 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"
|
||||||
|
|
|
@ -2,7 +2,6 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -21,41 +20,35 @@ func NewDockerEnvironment(files []string) *DockerEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (de *DockerEnvironment) createCommandWithStdout(cmd string) *exec.Cmd {
|
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)
|
log.Trace(dockerCmdLine)
|
||||||
return utils.CommandWithStdout("bash", "-c", dockerCmdLine)
|
return utils.CommandWithStdout("bash", "-c", dockerCmdLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (de *DockerEnvironment) createCommand(cmd string) *exec.Cmd {
|
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)
|
log.Trace(dockerCmdLine)
|
||||||
return exec.Command("bash", "-c", dockerCmdLine)
|
return utils.Command("bash", "-c", dockerCmdLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up spawn a docker environment
|
// Up spawn a docker environment
|
||||||
func (de *DockerEnvironment) Up(suitePath string) error {
|
func (de *DockerEnvironment) Up() error {
|
||||||
cmd := de.createCommandWithStdout("up -d")
|
return de.createCommandWithStdout("up -d").Run()
|
||||||
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart restarts a service
|
// Restart restarts a service
|
||||||
func (de *DockerEnvironment) Restart(suitePath, service string) error {
|
func (de *DockerEnvironment) Restart(service string) error {
|
||||||
cmd := de.createCommandWithStdout(fmt.Sprintf("restart %s", service))
|
return de.createCommandWithStdout(fmt.Sprintf("restart %s", service)).Run()
|
||||||
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Down spawn a docker environment
|
// Down spawn a docker environment
|
||||||
func (de *DockerEnvironment) Down(suitePath string) error {
|
func (de *DockerEnvironment) Down() error {
|
||||||
cmd := de.createCommandWithStdout("down -v")
|
return de.createCommandWithStdout("down -v").Run()
|
||||||
cmd.Env = append(os.Environ(), "SUITE_PATH="+suitePath)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logs get logs of a given service of the environment
|
// Logs get logs of a given service of the environment
|
||||||
func (de *DockerEnvironment) Logs(service string, flags []string) (string, error) {
|
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()
|
content, err := cmd.Output()
|
||||||
return string(content), err
|
return string(content), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ type OneFactorSuite struct {
|
||||||
*SeleniumSuite
|
*SeleniumSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOneFactorSuite() *OneFactorSuite {
|
func NewOneFactorScenario() *OneFactorSuite {
|
||||||
return &OneFactorSuite{
|
return &OneFactorSuite{
|
||||||
SeleniumSuite: new(SeleniumSuite),
|
SeleniumSuite: new(SeleniumSuite),
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func (s *OneFactorSuite) SetupSuite() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SeleniumSuite.WebDriverSession = wds
|
s.WebDriverSession = wds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OneFactorSuite) TearDownSuite() {
|
func (s *OneFactorSuite) TearDownSuite() {
|
||||||
|
@ -42,9 +42,9 @@ func (s *OneFactorSuite) SetupTest() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
doLogout(ctx, s.SeleniumSuite)
|
s.doLogout(ctx, s.T())
|
||||||
doVisit(s.SeleniumSuite, HomeBaseURL)
|
s.doVisit(s.T(), HomeBaseURL)
|
||||||
verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL)
|
s.verifyIsHome(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
|
func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
|
||||||
|
@ -52,8 +52,8 @@ func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
|
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
|
||||||
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL)
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
|
||||||
verifySecretAuthorized(ctx, s.SeleniumSuite)
|
s.verifySecretAuthorized(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() {
|
func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() {
|
||||||
|
@ -61,8 +61,8 @@ func (s *OneFactorSuite) TestShouldRedirectToSecondFactor() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
|
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
|
||||||
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "password", false, targetURL)
|
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, targetURL)
|
||||||
verifyIsSecondFactorPage(ctx, s.SeleniumSuite)
|
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
|
func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
|
||||||
|
@ -70,11 +70,11 @@ func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
|
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
|
||||||
doLoginOneFactor(ctx, s.SeleniumSuite, "john", "bad-password", false, targetURL)
|
s.doLoginOneFactor(ctx, s.T(), "john", "bad-password", false, targetURL)
|
||||||
verifyIsFirstFactorPage(ctx, s.SeleniumSuite)
|
s.verifyIsFirstFactorPage(ctx, s.T())
|
||||||
verifyNotificationDisplayed(ctx, s.SeleniumSuite, "Authentication failed. Check your credentials.")
|
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunOneFactor(t *testing.T) {
|
func TestRunOneFactor(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ type TwoFactorSuite struct {
|
||||||
*SeleniumSuite
|
*SeleniumSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTwoFactorSuite() *TwoFactorSuite {
|
func NewTwoFactorScenario() *TwoFactorSuite {
|
||||||
return &TwoFactorSuite{
|
return &TwoFactorSuite{
|
||||||
SeleniumSuite: new(SeleniumSuite),
|
SeleniumSuite: new(SeleniumSuite),
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func (s *TwoFactorSuite) SetupSuite() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SeleniumSuite.WebDriverSession = wds
|
s.WebDriverSession = wds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TwoFactorSuite) TearDownSuite() {
|
func (s *TwoFactorSuite) TearDownSuite() {
|
||||||
|
@ -42,24 +42,44 @@ func (s *TwoFactorSuite) SetupTest() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
doLogout(ctx, s.SeleniumSuite)
|
s.doLogout(ctx, s.T())
|
||||||
doVisit(s.SeleniumSuite, HomeBaseURL)
|
s.doVisit(s.T(), HomeBaseURL)
|
||||||
verifyURLIs(ctx, s.SeleniumSuite, HomeBaseURL)
|
s.verifyIsHome(ctx, s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
|
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Register TOTP secret and logout.
|
// 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)
|
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) {
|
func TestRunTwoFactor(t *testing.T) {
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ var bypassAllSuiteName = "BypassAll"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/BypassAll/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -19,7 +20,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
return dockerEnvironment.Down(suitePath)
|
return dockerEnvironment.Down()
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalRegistry.Register(bypassAllSuiteName, Suite{
|
GlobalRegistry.Register(bypassAllSuiteName, Suite{
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BypassAllSuite struct {
|
type BypassAllSuite struct {
|
||||||
|
@ -12,6 +18,36 @@ func NewBypassAllSuite() *BypassAllSuite {
|
||||||
return &BypassAllSuite{SeleniumSuite: new(SeleniumSuite)}
|
return &BypassAllSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBypassAllSuite(t *testing.T) {
|
func (s *BypassAllSuite) SetupSuite() {
|
||||||
RunTypescriptSuite(t, bypassAllSuiteName)
|
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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var duoPushSuiteName = "DuoPush"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/DuoPush/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -17,7 +18,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
return dockerEnvironment.Down(suitePath)
|
return dockerEnvironment.Down()
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalRegistry.Register(duoPushSuiteName, Suite{
|
GlobalRegistry.Register(duoPushSuiteName, Suite{
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DuoPushSuite struct {
|
type DuoPushSuite struct {
|
||||||
|
@ -12,6 +17,58 @@ func NewDuoPushSuite() *DuoPushSuite {
|
||||||
return &DuoPushSuite{SeleniumSuite: new(SeleniumSuite)}
|
return &DuoPushSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDuoPushSuite(t *testing.T) {
|
func (s *DuoPushSuite) SetupSuite() {
|
||||||
RunTypescriptSuite(t, duoPushSuiteName)
|
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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,31 +6,32 @@ import (
|
||||||
|
|
||||||
var highAvailabilitySuiteName = "HighAvailability"
|
var highAvailabilitySuiteName = "HighAvailability"
|
||||||
|
|
||||||
func init() {
|
var haDockerEnvironment = NewDockerEnvironment([]string{
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
"docker-compose.yml",
|
||||||
"docker-compose.yml",
|
"internal/suites/HighAvailability/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/mariadb/docker-compose.yml",
|
"example/compose/mariadb/docker-compose.yml",
|
||||||
"example/compose/redis/docker-compose.yml",
|
"example/compose/redis/docker-compose.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
"example/compose/nginx/portal/docker-compose.yml",
|
"example/compose/nginx/portal/docker-compose.yml",
|
||||||
"example/compose/smtp/docker-compose.yml",
|
"example/compose/smtp/docker-compose.yml",
|
||||||
"example/compose/httpbin/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.admin.yml", // This is just used for administration, not for testing.
|
||||||
"example/compose/ldap/docker-compose.yml",
|
"example/compose/ldap/docker-compose.yml",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func init() {
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := haDockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
return waitUntilAutheliaIsReady(haDockerEnvironment)
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
return dockerEnvironment.Down(suitePath)
|
return haDockerEnvironment.Down()
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalRegistry.Register(highAvailabilitySuiteName, Suite{
|
GlobalRegistry.Register(highAvailabilitySuiteName, Suite{
|
||||||
|
|
|
@ -1,18 +1,206 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HighAvailabilitySuite struct {
|
type HighAvailabilityWebDriverSuite struct {
|
||||||
*SeleniumSuite
|
*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 {
|
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) {
|
func TestHighAvailabilitySuite(t *testing.T) {
|
||||||
RunTypescriptSuite(t, highAvailabilitySuiteName)
|
suite.Run(t, NewOneFactorScenario())
|
||||||
TestRunOneFactor(t)
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
|
suite.Run(t, NewRegulationScenario())
|
||||||
|
suite.Run(t, NewCustomHeadersScenario())
|
||||||
|
suite.Run(t, NewRedirectionCheckScenario())
|
||||||
|
suite.Run(t, NewHighAvailabilityWebDriverSuite())
|
||||||
|
suite.Run(t, NewHighAvailabilitySuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,6 @@ func NewKubernetesSuite() *KubernetesSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKubernetesSuite(t *testing.T) {
|
func TestKubernetesSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var ldapSuiteName = "LDAP"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/LDAP/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -18,7 +19,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Up(suitePath)
|
err := dockerEnvironment.Up()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -28,7 +29,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Down(suitePath)
|
err := dockerEnvironment.Down()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ func NewLDAPSuite() *LDAPSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLDAPSuite(t *testing.T) {
|
func TestLDAPSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var mariadbSuiteName = "Mariadb"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/Mariadb/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -19,7 +20,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Down(suitePath)
|
err := dockerEnvironment.Down()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ func NewMariadbSuite() *MariadbSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMariadbSuite(t *testing.T) {
|
func TestMariadbSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var networkACLSuiteName = "NetworkACL"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/NetworkACL/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -20,7 +21,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
return dockerEnvironment.Down(suitePath)
|
return dockerEnvironment.Down()
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalRegistry.Register(networkACLSuiteName, Suite{
|
GlobalRegistry.Register(networkACLSuiteName, Suite{
|
||||||
|
|
|
@ -1,17 +1,102 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetworkACLSuite struct {
|
type NetworkACLSuite struct {
|
||||||
*SeleniumSuite
|
suite.Suite
|
||||||
|
|
||||||
|
clients []*WebDriverSession
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkACLSuite() *NetworkACLSuite {
|
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) {
|
func TestNetworkACLSuite(t *testing.T) {
|
||||||
RunTypescriptSuite(t, networkACLSuiteName)
|
suite.Run(t, NewNetworkACLSuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var postgresSuiteName = "Postgres"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/Postgres/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -19,7 +20,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Down(suitePath)
|
err := dockerEnvironment.Down()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ func NewPostgresSuite() *PostgresSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostgresSuite(t *testing.T) {
|
func TestPostgresSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var shortTimeoutsSuiteName = "ShortTimeouts"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/ShortTimeouts/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -17,7 +18,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
if err := dockerEnvironment.Up(suitePath); err != nil {
|
if err := dockerEnvironment.Up(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
return dockerEnvironment.Down(suitePath)
|
return dockerEnvironment.Down()
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalRegistry.Register(shortTimeoutsSuiteName, Suite{
|
GlobalRegistry.Register(shortTimeoutsSuiteName, Suite{
|
||||||
|
|
|
@ -2,6 +2,8 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShortTimeoutsSuite struct {
|
type ShortTimeoutsSuite struct {
|
||||||
|
@ -13,5 +15,6 @@ func NewShortTimeoutsSuite() *ShortTimeoutsSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShortTimeoutsSuite(t *testing.T) {
|
func TestShortTimeoutsSuite(t *testing.T) {
|
||||||
RunTypescriptSuite(t, shortTimeoutsSuiteName)
|
suite.Run(t, NewInactivityScenario())
|
||||||
|
suite.Run(t, NewRegulationScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var standaloneSuiteName = "Standalone"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/Standalone/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -17,7 +18,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Up(suitePath)
|
err := dockerEnvironment.Up()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -27,7 +28,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Down(suitePath)
|
err := dockerEnvironment.Down()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,138 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StandaloneSuite struct {
|
type StandaloneWebDriverSuite struct {
|
||||||
*SeleniumSuite
|
*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 {
|
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) {
|
func TestStandaloneSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var traefikSuiteName = "Traefik"
|
||||||
func init() {
|
func init() {
|
||||||
dockerEnvironment := NewDockerEnvironment([]string{
|
dockerEnvironment := NewDockerEnvironment([]string{
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
|
"internal/suites/Traefik/docker-compose.yml",
|
||||||
"example/compose/authelia/docker-compose.backend.yml",
|
"example/compose/authelia/docker-compose.backend.yml",
|
||||||
"example/compose/authelia/docker-compose.frontend.yml",
|
"example/compose/authelia/docker-compose.frontend.yml",
|
||||||
"example/compose/nginx/backend/docker-compose.yml",
|
"example/compose/nginx/backend/docker-compose.yml",
|
||||||
|
@ -17,7 +18,7 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
setup := func(suitePath string) error {
|
setup := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Up(suitePath)
|
err := dockerEnvironment.Up()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -27,7 +28,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func(suitePath string) error {
|
teardown := func(suitePath string) error {
|
||||||
err := dockerEnvironment.Down(suitePath)
|
err := dockerEnvironment.Down()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ func NewTraefikSuite() *TraefikSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTraefikSuite(t *testing.T) {
|
func TestTraefikSuite(t *testing.T) {
|
||||||
suite.Run(t, NewOneFactorSuite())
|
suite.Run(t, NewOneFactorScenario())
|
||||||
suite.Run(t, NewTwoFactorSuite())
|
suite.Run(t, NewTwoFactorScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/clems4ever/authelia/internal/utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/tebeka/selenium"
|
"github.com/tebeka/selenium"
|
||||||
)
|
)
|
||||||
|
@ -24,55 +16,3 @@ type SeleniumSuite struct {
|
||||||
func (s *SeleniumSuite) WebDriver() selenium.WebDriver {
|
func (s *SeleniumSuite) WebDriver() selenium.WebDriver {
|
||||||
return s.WebDriverSession.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())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
func verifyBodyContains(ctx context.Context, s *SeleniumSuite, pattern string) {
|
"github.com/stretchr/testify/require"
|
||||||
bodyElement := WaitElementLocatedByTagName(ctx, s, "body")
|
"github.com/tebeka/selenium"
|
||||||
WaitElementTextContains(ctx, s, bodyElement, pattern)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func verifyIsFirstFactorPage(ctx context.Context, s *SeleniumSuite) {
|
func (wds *WebDriverSession) verifyIsFirstFactorPage(ctx context.Context, t *testing.T) {
|
||||||
WaitElementLocatedByClassName(ctx, s, "first-factor-step")
|
wds.WaitElementLocatedByID(ctx, t, "first-factor-stage")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func verifyIsSecondFactorPage(ctx context.Context, s *SeleniumSuite) {
|
func (wds *WebDriverSession) verifyIsSecondFactorPage(ctx context.Context, t *testing.T) {
|
||||||
WaitElementLocatedByClassName(ctx, s, "second-factor-step")
|
wds.WaitElementLocatedByID(ctx, t, "second-factor-stage")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
|
}
|
|
@ -1,9 +1,14 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
func verifyNotificationDisplayed(ctx context.Context, s *SeleniumSuite, message string) {
|
"github.com/stretchr/testify/assert"
|
||||||
txt, err := WaitElementLocatedByClassName(ctx, s, "notification").Text()
|
)
|
||||||
s.Assert().NoError(err)
|
|
||||||
s.Assert().Equal(message, txt)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package suites
|
package suites
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func verifySecretAuthorized(ctx context.Context, s *SeleniumSuite) {
|
func (wds *WebDriverSession) verifySecretAuthorized(ctx context.Context, t *testing.T) {
|
||||||
verifyBodyContains(ctx, s, "This is a very important secret!")
|
wds.verifyBodyContains(ctx, t, "This is a very important secret!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,21 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/tebeka/selenium"
|
"github.com/tebeka/selenium"
|
||||||
)
|
)
|
||||||
|
|
||||||
func verifyURLIs(ctx context.Context, s *SeleniumSuite, url string) {
|
func (wds *WebDriverSession) verifyURLIs(ctx context.Context, t *testing.T, url string) {
|
||||||
err := s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
|
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
|
||||||
currentURL, err := driver.CurrentURL()
|
currentURL, err := driver.CurrentURL()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentURL == url, nil
|
return currentURL == url, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.NoError(s.T(), err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,13 @@ package suites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tebeka/selenium"
|
"github.com/tebeka/selenium"
|
||||||
"github.com/tebeka/selenium/chrome"
|
"github.com/tebeka/selenium/chrome"
|
||||||
)
|
)
|
||||||
|
@ -17,9 +19,8 @@ type WebDriverSession struct {
|
||||||
WebDriver selenium.WebDriver
|
WebDriver selenium.WebDriver
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartWebDriver create a selenium session
|
// StartWebDriverWithProxy create a selenium session
|
||||||
func StartWebDriver() (*WebDriverSession, error) {
|
func StartWebDriverWithProxy(proxy string, port int) (*WebDriverSession, error) {
|
||||||
port := 4444
|
|
||||||
service, err := selenium.NewChromeDriverService("/usr/bin/chromedriver", port)
|
service, err := selenium.NewChromeDriverService("/usr/bin/chromedriver", port)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -34,6 +35,10 @@ func StartWebDriver() (*WebDriverSession, error) {
|
||||||
chromeCaps.Args = append(chromeCaps.Args, "--headless")
|
chromeCaps.Args = append(chromeCaps.Args, "--headless")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if proxy != "" {
|
||||||
|
chromeCaps.Args = append(chromeCaps.Args, fmt.Sprintf("--proxy-server=%s", proxy))
|
||||||
|
}
|
||||||
|
|
||||||
caps := selenium.Capabilities{}
|
caps := selenium.Capabilities{}
|
||||||
caps.AddChrome(chromeCaps)
|
caps.AddChrome(chromeCaps)
|
||||||
|
|
||||||
|
@ -49,6 +54,11 @@ func StartWebDriver() (*WebDriverSession, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartWebDriver create a selenium session
|
||||||
|
func StartWebDriver() (*WebDriverSession, error) {
|
||||||
|
return StartWebDriverWithProxy("", 4444)
|
||||||
|
}
|
||||||
|
|
||||||
// Stop stop the selenium session
|
// Stop stop the selenium session
|
||||||
func (wds *WebDriverSession) Stop() error {
|
func (wds *WebDriverSession) Stop() error {
|
||||||
err := wds.WebDriver.Quit()
|
err := wds.WebDriver.Quit()
|
||||||
|
@ -73,9 +83,24 @@ func WithWebdriver(fn func(webdriver selenium.WebDriver) error) error {
|
||||||
return fn(wds.WebDriver)
|
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
|
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
|
var err error
|
||||||
el, err = driver.FindElement(by, value)
|
el, err = driver.FindElement(by, value)
|
||||||
|
|
||||||
|
@ -89,31 +114,65 @@ func waitElementLocated(ctx context.Context, s *SeleniumSuite, by, value string)
|
||||||
return el != nil, nil
|
return el != nil, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.NoError(s.T(), err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(s.T(), el, "Element has not been located")
|
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
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitElementLocatedByID wait an element is located by id
|
// WaitElementLocatedByID wait an element is located by id
|
||||||
func WaitElementLocatedByID(ctx context.Context, s *SeleniumSuite, id string) selenium.WebElement {
|
func (wds *WebDriverSession) WaitElementLocatedByID(ctx context.Context, t *testing.T, id string) selenium.WebElement {
|
||||||
return waitElementLocated(ctx, s, selenium.ByID, id)
|
return wds.waitElementLocated(ctx, t, selenium.ByID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitElementLocatedByTagName wait an element is located by tag name
|
// WaitElementLocatedByTagName wait an element is located by tag name
|
||||||
func WaitElementLocatedByTagName(ctx context.Context, s *SeleniumSuite, tagName string) selenium.WebElement {
|
func (wds *WebDriverSession) WaitElementLocatedByTagName(ctx context.Context, t *testing.T, tagName string) selenium.WebElement {
|
||||||
return waitElementLocated(ctx, s, selenium.ByTagName, tagName)
|
return wds.waitElementLocated(ctx, t, selenium.ByTagName, tagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitElementLocatedByClassName wait an element is located by class name
|
// WaitElementLocatedByClassName wait an element is located by class name
|
||||||
func WaitElementLocatedByClassName(ctx context.Context, s *SeleniumSuite, className string) selenium.WebElement {
|
func (wds *WebDriverSession) WaitElementLocatedByClassName(ctx context.Context, t *testing.T, className string) selenium.WebElement {
|
||||||
return waitElementLocated(ctx, s, selenium.ByClassName, className)
|
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
|
// WaitElementTextContains wait the text of an element contains a pattern
|
||||||
func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element selenium.WebElement, pattern string) {
|
func (wds *WebDriverSession) WaitElementTextContains(ctx context.Context, t *testing.T, element selenium.WebElement, pattern string) {
|
||||||
assert.NotNil(s.T(), element)
|
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
|
||||||
|
|
||||||
s.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
|
|
||||||
text, err := element.Text()
|
text, err := element.Text()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,4 +181,5 @@ func WaitElementTextContains(ctx context.Context, s *SeleniumSuite, element sele
|
||||||
|
|
||||||
return strings.Contains(text, pattern), nil
|
return strings.Contains(text, pattern), nil
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,9 +15,22 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
// CommandWithStdout create a command forwarding stdout and stderr to the OS streams
|
||||||
func CommandWithStdout(name string, args ...string) *exec.Cmd {
|
func CommandWithStdout(name string, args ...string) *exec.Cmd {
|
||||||
cmd := exec.Command(name, args...)
|
cmd := Command(name, args...)
|
||||||
if log.GetLevel() > log.InfoLevel {
|
if log.GetLevel() > log.InfoLevel {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
})()
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
})()
|
|
|
@ -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/');
|
|
||||||
}
|
|
|
@ -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 }
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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];
|
|
||||||
};
|
|
|
@ -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
Loading…
Reference in New Issue