diff --git a/cmd/authelia-gen/cmd_github.go b/cmd/authelia-gen/cmd_github.go
index 8d8ec2f49..99d80378a 100644
--- a/cmd/authelia-gen/cmd_github.go
+++ b/cmd/authelia-gen/cmd_github.go
@@ -204,7 +204,7 @@ func cmdGitHubIssueTemplatesBugReportRunE(cmd *cobra.Command, args []string) (er
data := &tmplIssueTemplateData{
Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()},
Versions: tagsRecent,
- Proxies: []string{"Caddy", "Traefik", "Envoy", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"},
+ Proxies: []string{"Caddy", "Traefik", "Envoy", "Istio", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"},
}
if err = tmplGitHubIssueTemplateBug.Execute(f, data); err != nil {
diff --git a/config.template.yml b/config.template.yml
index a878e05f4..85f1bea72 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -443,7 +443,7 @@ password_policy:
## to anyone. Otherwise restrictions follow the rules defined.
##
## Note: One can use the wildcard * to match any subdomain.
-## It must stand at the beginning of the pattern. (example: *.mydomain.com)
+## It must stand at the beginning of the pattern. (example: *.example.com)
##
## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
##
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index a878e05f4..85f1bea72 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -443,7 +443,7 @@ password_policy:
## to anyone. Otherwise restrictions follow the rules defined.
##
## Note: One can use the wildcard * to match any subdomain.
-## It must stand at the beginning of the pattern. (example: *.mydomain.com)
+## It must stand at the beginning of the pattern. (example: *.example.com)
##
## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
##
diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go
index 201bb521f..771fc49bf 100644
--- a/internal/handlers/handler_sign_duo_test.go
+++ b/internal/handlers/handler_sign_duo_test.go
@@ -614,14 +614,14 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{
- TargetURL: "https://example.com",
+ TargetURL: "https://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
DuoPOST(duoMock)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
- Redirect: "https://example.com",
+ Redirect: "https://mydomain.example.com",
})
}
@@ -663,7 +663,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{
- TargetURL: "http://example.com",
+ TargetURL: "http://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
@@ -710,7 +710,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessi
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{
- TargetURL: "http://example.com",
+ TargetURL: "http://mydomain.example.com",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go
index e5e38e9f3..13b84c351 100644
--- a/internal/handlers/handler_sign_totp_test.go
+++ b/internal/handlers/handler_sign_totp_test.go
@@ -167,7 +167,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
- TargetURL: "https://example.com",
+ TargetURL: "https://mydomain.example.com",
})
s.Require().NoError(err)
@@ -175,7 +175,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
- Redirect: "https://example.com",
+ Redirect: "https://mydomain.example.com",
})
}
@@ -205,7 +205,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
- TargetURL: "http://example.com",
+ TargetURL: "http://mydomain.example.com",
})
s.Require().NoError(err)
diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go
index a78d699ca..eacd23b43 100644
--- a/internal/handlers/handler_verify.go
+++ b/internal/handlers/handler_verify.go
@@ -180,7 +180,6 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) {
var (
statusCode int
- redirectionURL string
friendlyUsername string
friendlyRequestMethod string
)
@@ -200,10 +199,6 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
return
}
- // Kubernetes ingress controller and Traefik use the rd parameter of the verify
- // endpoint to provide the URL of the login portal. The target URL of the user
- // is computed from X-Forwarded-* headers or X-Original-URL.
- rd := string(ctx.QueryArgs().Peek("rd"))
rm := string(method)
switch rm {
@@ -213,17 +208,30 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
friendlyRequestMethod = rm
}
- if rd != "" {
- switch rm {
- case "":
- redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String()))
- default:
- redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm)
+ redirectionURL := ctxGetPortalURL(ctx)
+
+ if redirectionURL != nil {
+ if !utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) {
+ ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, ctx.Configuration.Session.Domain)
+
+ ctx.ReplyUnauthorized()
+
+ return
}
+
+ qry := redirectionURL.Query()
+
+ qry.Set("rd", targetURL.String())
+
+ if rm != "" {
+ qry.Set("rm", rm)
+ }
+
+ redirectionURL.RawQuery = qry.Encode()
}
switch {
- case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || rd == "":
+ case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
statusCode = fasthttp.StatusUnauthorized
default:
switch rm {
@@ -234,9 +242,9 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
}
}
- if redirectionURL != "" {
+ if redirectionURL != nil {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode, redirectionURL)
- ctx.SpecialRedirect(redirectionURL, statusCode)
+ ctx.SpecialRedirect(redirectionURL.String(), statusCode)
} else {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode)
ctx.ReplyUnauthorized()
diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go
index 42e66fee2..ae2cc9d25 100644
--- a/internal/handlers/handler_verify_test.go
+++ b/internal/handlers/handler_verify_test.go
@@ -453,7 +453,7 @@ func TestShouldRedirectWithGroups(t *testing.T) {
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/code-test/login")
- mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
+ mock.Ctx.Request.SetRequestURI("/api/verify/?rd=https://auth.example.com")
VerifyGET(verifyGetCfg)(mock.Ctx)
@@ -537,6 +537,110 @@ func (p Pair) String() string {
p.URL, p.Username, p.AuthenticationLevel, p.ExpectedStatusCode)
}
+//nolint:gocyclo // This is a test.
+func TestShouldRedirectAuthorizations(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ method, originalURL, autheliaURL string
+
+ expected int
+ }{
+ {"ShouldReturnFoundMethodNone", "", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
+ {"ShouldReturnFoundMethodGET", "GET", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
+ {"ShouldReturnFoundMethodOPTIONS", "OPTIONS", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
+ {"ShouldReturnSeeOtherMethodPOST", "POST", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
+ {"ShouldReturnSeeOtherMethodPATCH", "PATCH", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
+ {"ShouldReturnSeeOtherMethodPUT", "PUT", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
+ {"ShouldReturnSeeOtherMethodDELETE", "DELETE", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
+ {"ShouldReturnUnauthorizedBadDomain", "GET", "https://one-factor.example.com/", "https://auth.notexample.com/", fasthttp.StatusUnauthorized},
+ }
+
+ handler := VerifyGET(verifyGetCfg)
+
+ for _, tc := range testCases {
+ var (
+ suffix string
+ xhr bool
+ )
+
+ for i := 0; i < 2; i++ {
+ switch i {
+ case 0:
+ suffix += "QueryParameter"
+ default:
+ suffix += "RequestHeader"
+ }
+
+ for j := 0; j < 2; j++ {
+ switch j {
+ case 0:
+ xhr = false
+ case 1:
+ xhr = true
+ suffix += "XHR"
+ }
+
+ t.Run(tc.name+suffix, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Clock.Set(time.Now())
+
+ autheliaURL, err := url.ParseRequestURI(tc.autheliaURL)
+
+ require.NoError(t, err)
+
+ originalURL, err := url.ParseRequestURI(tc.originalURL)
+
+ require.NoError(t, err)
+
+ if xhr {
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
+ }
+
+ var rm string
+
+ if tc.method != "" {
+ rm = fmt.Sprintf("&rm=%s", tc.method)
+ mock.Ctx.Request.Header.Set("X-Forwarded-Method", tc.method)
+ }
+
+ mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
+ mock.Ctx.Request.Header.Set("X-Original-URL", originalURL.String())
+
+ if i == 0 {
+ mock.Ctx.Request.SetRequestURI(fmt.Sprintf("/?rd=%s", url.QueryEscape(autheliaURL.String())))
+ } else {
+ mock.Ctx.Request.Header.Set("X-Authelia-URL", autheliaURL.String())
+ }
+
+ handler(mock.Ctx)
+
+ if xhr && tc.expected != fasthttp.StatusUnauthorized {
+ assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ } else {
+ assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
+ }
+
+ switch {
+ case xhr && tc.expected != fasthttp.StatusUnauthorized:
+ href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
+ assert.Equal(t, fmt.Sprintf("%d %s", href, fasthttp.StatusUnauthorized, fasthttp.StatusMessage(fasthttp.StatusUnauthorized)), string(mock.Ctx.Response.Body()))
+ case tc.expected >= fasthttp.StatusMultipleChoices && tc.expected < fasthttp.StatusBadRequest:
+ href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
+ assert.Equal(t, fmt.Sprintf("%d %s", href, tc.expected, fasthttp.StatusMessage(tc.expected)), string(mock.Ctx.Response.Body()))
+ case tc.expected < fasthttp.StatusMultipleChoices:
+ assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
+ default:
+ assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
+ }
+ })
+ }
+ }
+ }
+}
+
func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
testCases := []Pair{
// should apply default policy.
@@ -837,11 +941,36 @@ func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
mock.Ctx.Request.SetHost("mydomain.com")
- mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
+ mock.Ctx.Request.SetRequestURI("/?rd=https://auth.example.com")
VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, "302 Found",
+ assert.Equal(t, "302 Found",
+ string(mock.Ctx.Response.Body()))
+}
+
+func TestShouldURLEncodeRedirectionHeader(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Clock.Set(time.Now())
+
+ userSession := mock.Ctx.GetSession()
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.NotAuthenticated
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ err := mock.Ctx.SaveSession(userSession)
+ require.NoError(t, err)
+
+ mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
+ mock.Ctx.Request.Header.Set("X-Authelia-URL", "https://auth.example.com")
+ mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
+ mock.Ctx.Request.SetHost("mydomain.com")
+
+ VerifyGET(verifyGetCfg)(mock.Ctx)
+
+ assert.Equal(t, "302 Found",
string(mock.Ctx.Response.Body()))
}
diff --git a/internal/handlers/util.go b/internal/handlers/util.go
new file mode 100644
index 000000000..3117b74bf
--- /dev/null
+++ b/internal/handlers/util.go
@@ -0,0 +1,26 @@
+package handlers
+
+import (
+ "bytes"
+ "net/url"
+
+ "github.com/authelia/authelia/v4/internal/middlewares"
+)
+
+var bytesEmpty = []byte("")
+
+func ctxGetPortalURL(ctx *middlewares.AutheliaCtx) (portalURL *url.URL) {
+ var rawURL []byte
+
+ if rawURL = ctx.QueryArgRedirect(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
+ portalURL, _ = url.ParseRequestURI(string(rawURL))
+
+ return portalURL
+ } else if rawURL = ctx.XAutheliaURL(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
+ portalURL, _ = url.ParseRequestURI(string(rawURL))
+
+ return portalURL
+ }
+
+ return nil
+}
diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go
index 5189e1435..aa7fd4926 100644
--- a/internal/middlewares/authelia_context.go
+++ b/internal/middlewares/authelia_context.go
@@ -171,6 +171,16 @@ func (ctx *AutheliaCtx) XForwardedURI() (uri []byte) {
return uri
}
+// XAutheliaURL return the content of the X-Authelia-URL header.
+func (ctx *AutheliaCtx) XAutheliaURL() (autheliaURL []byte) {
+ return ctx.RequestCtx.Request.Header.PeekBytes(headerXAutheliaURL)
+}
+
+// QueryArgRedirect return the content of the rd query argument.
+func (ctx *AutheliaCtx) QueryArgRedirect() (val []byte) {
+ return ctx.RequestCtx.QueryArgs().PeekBytes(queryArgRedirect)
+}
+
// BasePath returns the base_url as per the path visited by the client.
func (ctx *AutheliaCtx) BasePath() (base string) {
if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go
index bbbbb8235..88ef4472b 100644
--- a/internal/middlewares/const.go
+++ b/internal/middlewares/const.go
@@ -7,6 +7,8 @@ import (
)
var (
+ headerXAutheliaURL = []byte("X-Authelia-URL")
+
headerAccept = []byte(fasthttp.HeaderAccept)
headerContentLength = []byte(fasthttp.HeaderContentLength)
headerLocation = []byte(fasthttp.HeaderLocation)
@@ -71,6 +73,8 @@ var (
protoHTTPS = []byte(strProtoHTTPS)
protoHTTP = []byte(strProtoHTTP)
+ queryArgRedirect = []byte("rd")
+
// UserValueKeyBaseURL is the User Value key where we store the Base URL.
UserValueKeyBaseURL = []byte("base_url")
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 9f77e0bbb..b9c9c95da 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -167,6 +167,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.GET("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
r.HEAD("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
+ r.GET("/api/verify/{path:*}", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
+ r.HEAD("/api/verify/{path:*}", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
+
r.POST("/api/checks/safe-redirection", middlewareAPI(handlers.CheckSafeRedirectionPOST))
delayFunc := middlewares.TimingAttackDelay(10, 250, 85, time.Second, true)
diff --git a/internal/suites/Envoy/configuration.yml b/internal/suites/Envoy/configuration.yml
new file mode 100644
index 000000000..7a5f55448
--- /dev/null
+++ b/internal/suites/Envoy/configuration.yml
@@ -0,0 +1,54 @@
+---
+###############################################################
+# Authelia minimal configuration #
+###############################################################
+
+jwt_secret: unsecure_secret
+
+server:
+ port: 9091
+ asset_path: /config/assets/
+ tls:
+ certificate: /config/ssl/cert.pem
+ key: /config/ssl/key.pem
+
+log:
+ level: debug
+
+authentication_backend:
+ file:
+ path: /config/users.yml
+
+session:
+ secret: unsecure_session_secret
+ domain: example.com
+ expiration: 3600 # 1 hour
+ inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
+
+storage:
+ encryption_key: a_not_so_secure_encryption_key
+ local:
+ path: /config/db.sqlite
+
+access_control:
+ default_policy: bypass
+ rules:
+ - domain: "login.example.com"
+ policy: bypass
+ - domain: "public.example.com"
+ policy: bypass
+ - domain: "admin.example.com"
+ policy: two_factor
+ - domain: "secure.example.com"
+ policy: two_factor
+ - domain: "singlefactor.example.com"
+ policy: one_factor
+
+notifier:
+ smtp:
+ host: smtp
+ port: 1025
+ sender: admin@example.com
+ disable_require_tls: true
+...
diff --git a/internal/suites/Envoy/docker-compose.yml b/internal/suites/Envoy/docker-compose.yml
new file mode 100644
index 000000000..194d32ab4
--- /dev/null
+++ b/internal/suites/Envoy/docker-compose.yml
@@ -0,0 +1,9 @@
+---
+version: '3'
+services:
+ authelia-backend:
+ volumes:
+ - './Envoy/configuration.yml:/config/configuration.yml:ro'
+ - './Envoy/users.yml:/config/users.yml'
+ - './common/ssl:/config/ssl:ro'
+...
diff --git a/internal/suites/Envoy/users.yml b/internal/suites/Envoy/users.yml
new file mode 100644
index 000000000..a52978b20
--- /dev/null
+++ b/internal/suites/Envoy/users.yml
@@ -0,0 +1,35 @@
+---
+###############################################################
+# Users Database #
+###############################################################
+
+# This file can be used if you do not have an LDAP set up.
+
+# List of users
+users:
+ john:
+ displayname: "John Doe"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: john.doe@authelia.com
+ groups:
+ - admins
+ - dev
+
+ harry:
+ displayname: "Harry Potter"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: harry.potter@authelia.com
+ groups: []
+
+ bob:
+ displayname: "Bob Dylan"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: bob.dylan@authelia.com
+ groups:
+ - dev
+
+ james:
+ displayname: "James Dean"
+ password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
+ email: james.dean@authelia.com
+...
diff --git a/internal/suites/example/compose/envoy/docker-compose.yml b/internal/suites/example/compose/envoy/docker-compose.yml
new file mode 100644
index 000000000..691c93599
--- /dev/null
+++ b/internal/suites/example/compose/envoy/docker-compose.yml
@@ -0,0 +1,12 @@
+---
+version: '3'
+services:
+ envoy:
+ image: envoyproxy/envoy:v1.23.0
+ volumes:
+ - ./example/compose/envoy/envoy.yaml:/etc/envoy/envoy.yaml
+ - ./example/compose/nginx/portal/ssl:/etc/ssl
+ networks:
+ authelianet:
+ ipv4_address: 192.168.240.100
+...
diff --git a/internal/suites/example/compose/envoy/envoy.yaml b/internal/suites/example/compose/envoy/envoy.yaml
new file mode 100644
index 000000000..fc5039ebe
--- /dev/null
+++ b/internal/suites/example/compose/envoy/envoy.yaml
@@ -0,0 +1,236 @@
+---
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 8080
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager # yamllint disable-line rule:line-length
+ stat_prefix: ingress_http
+ use_remote_address: true
+ skip_xff_append: false
+ access_log:
+ - name: envoy.access_loggers.stdout
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: login_service
+ domains: ["login.example.com:8080"]
+ typed_per_filter_config:
+ envoy.filters.http.ext_authz:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
+ disabled: true
+ routes:
+ - match:
+ prefix: "/.well-known/"
+ route:
+ cluster: authelia-backend
+ - match:
+ prefix: "/api/"
+ route:
+ cluster: authelia-backend
+ - match:
+ prefix: "/locales/"
+ route:
+ cluster: authelia-backend
+ - match:
+ path: "/jwks.json"
+ route:
+ cluster: authelia-backend
+ - match:
+ prefix: "/"
+ route:
+ cluster: authelia-frontend
+ - name: mail_service
+ domains: ["mail.example.com:8080"]
+ typed_per_filter_config:
+ envoy.filters.http.ext_authz:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
+ disabled: true
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: smtp
+ - name: http_service
+ domains: ["*.example.com:8080"]
+ routes:
+ - match:
+ prefix: "/headers"
+ route:
+ cluster: httpbin
+ - match:
+ prefix: "/"
+ route:
+ cluster: nginx-backend
+ http_filters:
+ - name: envoy.filters.http.ext_authz
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
+ http_service:
+ path_prefix: /api/verify/
+ server_uri:
+ uri: authelia-backend:9091
+ cluster: authelia-backend
+ timeout: 0.25s
+ authorization_request:
+ allowed_headers:
+ patterns:
+ - exact: accept
+ - exact: cookie
+ - exact: proxy-authorization
+ headers_to_add:
+ - key: X-Authelia-URL
+ value: 'https://login.example.com:8080/'
+ - key: X-Forwarded-Method
+ value: '%REQ(:METHOD)%'
+ - key: X-Forwarded-Proto
+ value: '%REQ(:SCHEME)%'
+ - key: X-Forwarded-Host
+ value: '%REQ(:AUTHORITY)%'
+ - key: X-Forwarded-URI
+ value: '%REQ(:PATH)%'
+ - key: X-Forwarded-For
+ value: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
+ authorization_response:
+ allowed_upstream_headers:
+ patterns:
+ - prefix: remote-
+ allowed_client_headers:
+ patterns:
+ - exact: set-cookie
+ allowed_client_headers_on_success:
+ patterns:
+ - exact: set-cookie
+ failure_mode_allow: false
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ transport_socket:
+ name: envoy.transport_sockets.tls
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
+ common_tls_context:
+ tls_certificates:
+ - certificate_chain:
+ filename: /etc/ssl/server.cert
+ private_key:
+ filename: /etc/ssl/server.key
+ clusters:
+ - name: authelia-frontend
+ transport_socket_matches:
+ - name: "enableTLS"
+ match:
+ enableTLS: true
+ transport_socket:
+ name: envoy.transport_sockets.tls
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+ common_tls_context: {}
+ - name: "defaultTLSDisabled"
+ match: {}
+ transport_socket:
+ name: envoy.transport_sockets.raw_buffer
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer
+ connect_timeout: 0.25s
+ type: STRICT_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: authelia-frontend
+ endpoints:
+ - locality:
+ region: dev
+ priority: 0
+ lb_endpoints:
+ - endpoint:
+ health_check_config:
+ hostname: authelia-frontend
+ port_value: 3000
+ address:
+ socket_address:
+ address: authelia-frontend
+ port_value: 3000
+ - locality:
+ region: ci
+ priority: 1
+ lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: authelia-backend
+ port_value: 9091
+ metadata:
+ filter_metadata:
+ envoy.transport_socket_match:
+ enableTLS: true
+ - name: authelia-backend
+ connect_timeout: 0.25s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: authelia-backend
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: authelia-backend
+ port_value: 9091
+ transport_socket:
+ name: envoy.transport_sockets.tls
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+ common_tls_context: {}
+ - name: smtp
+ connect_timeout: 0.25s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: smtp
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: smtp
+ port_value: 1080
+ - name: httpbin
+ connect_timeout: 0.25s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: httpbin
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: httpbin
+ port_value: 8000
+ - name: nginx-backend
+ connect_timeout: 0.25s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: nginx-backend
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: nginx-backend
+ port_value: 80
+...
diff --git a/internal/suites/suite_envoy.go b/internal/suites/suite_envoy.go
new file mode 100644
index 000000000..9b9ff3916
--- /dev/null
+++ b/internal/suites/suite_envoy.go
@@ -0,0 +1,84 @@
+package suites
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+var envoySuiteName = "Envoy"
+
+func init() {
+ dockerEnvironment := NewDockerEnvironment([]string{
+ "internal/suites/docker-compose.yml",
+ "internal/suites/Envoy/docker-compose.yml",
+ "internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
+ "internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
+ "internal/suites/example/compose/nginx/backend/docker-compose.yml",
+ "internal/suites/example/compose/envoy/docker-compose.yml",
+ "internal/suites/example/compose/smtp/docker-compose.yml",
+ "internal/suites/example/compose/httpbin/docker-compose.yml",
+ })
+
+ if os.Getenv("CI") == t {
+ dockerEnvironment = NewDockerEnvironment([]string{
+ "internal/suites/docker-compose.yml",
+ "internal/suites/Envoy/docker-compose.yml",
+ "internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
+ "internal/suites/example/compose/nginx/backend/docker-compose.yml",
+ "internal/suites/example/compose/envoy/docker-compose.yml",
+ "internal/suites/example/compose/smtp/docker-compose.yml",
+ "internal/suites/example/compose/httpbin/docker-compose.yml",
+ })
+ }
+
+ setup := func(suitePath string) error {
+ if err := dockerEnvironment.Up(); err != nil {
+ return err
+ }
+
+ return waitUntilAutheliaIsReady(dockerEnvironment, envoySuiteName)
+ }
+
+ displayAutheliaLogs := func() error {
+ backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(backendLogs)
+
+ if os.Getenv("CI") != t {
+ frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(frontendLogs)
+ }
+
+ envoyLogs, err := dockerEnvironment.Logs("envoy", nil)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(envoyLogs)
+
+ return nil
+ }
+
+ teardown := func(suitePath string) error {
+ err := dockerEnvironment.Down()
+ return err
+ }
+
+ GlobalRegistry.Register(envoySuiteName, Suite{
+ SetUp: setup,
+ SetUpTimeout: 5 * time.Minute,
+ OnSetupTimeout: displayAutheliaLogs,
+ OnError: displayAutheliaLogs,
+ TestTimeout: 2 * time.Minute,
+ TearDown: teardown,
+ TearDownTimeout: 2 * time.Minute,
+ })
+}
diff --git a/internal/suites/suite_envoy_test.go b/internal/suites/suite_envoy_test.go
new file mode 100644
index 000000000..ca7cb1149
--- /dev/null
+++ b/internal/suites/suite_envoy_test.go
@@ -0,0 +1,39 @@
+package suites
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+)
+
+type EnvoySuite struct {
+ *RodSuite
+}
+
+func NewEnvoySuite() *EnvoySuite {
+ return &EnvoySuite{RodSuite: new(RodSuite)}
+}
+
+func (s *EnvoySuite) Test1FAScenario() {
+ suite.Run(s.T(), New1FAScenario())
+}
+
+func (s *EnvoySuite) Test2FAScenario() {
+ suite.Run(s.T(), New2FAScenario())
+}
+
+func (s *EnvoySuite) TestCustomHeaders() {
+ suite.Run(s.T(), NewCustomHeadersScenario())
+}
+
+func (s *EnvoySuite) TestResetPasswordScenario() {
+ suite.Run(s.T(), NewResetPasswordScenario())
+}
+
+func TestEnvoySuite(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping suite test in short mode")
+ }
+
+ suite.Run(t, NewEnvoySuite())
+}