From 2302debac22e2ab0cb7a0ae22fe435e6f264605c Mon Sep 17 00:00:00 2001 From: Nelson Isioma Date: Fri, 13 Dec 2024 10:38:37 +0100 Subject: [PATCH] Add an option to preserve the ForwardAuth Server Location header --- docs/content/middlewares/http/forwardauth.md | 42 +++++++++++++++++++ .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 1 + .../reference/dynamic-configuration/file.yaml | 1 + .../kubernetes-crd-definition-v1.yml | 5 +++ .../reference/dynamic-configuration/kv-ref.md | 1 + .../traefik.io_middlewares.yaml | 5 +++ integration/fixtures/k8s/01-traefik-crd.yml | 5 +++ pkg/config/dynamic/middlewares.go | 2 + pkg/config/label/label_test.go | 1 + pkg/middlewares/auth/forward.go | 19 +++++++-- pkg/middlewares/auth/forward_test.go | 28 +++++++++++++ pkg/provider/kubernetes/crd/kubernetes.go | 1 + .../crd/traefikio/v1alpha1/middleware.go | 2 + pkg/provider/kv/kv_test.go | 6 ++- 15 files changed, 115 insertions(+), 5 deletions(-) diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index c09a00bda..f6e30b827 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -705,4 +705,46 @@ http: headerField = "X-WebAuth-User" ``` +### `preserveLocationHeader` + +_Optional, Default=false_ + +`preserveLocationHeader` defines whether to forward the `Location` header to the client as is or prefix it with the domain name of the authentication server. + +```yaml tab="Docker & Swarm" +labels: + - "traefik.http.middlewares.test-auth.forwardauth.preserveLocationHeader=true" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-auth +spec: + forwardAuth: + # ... + preserveLocationHeader: true +``` + +```json tab="Consul Catalog" +- "traefik.http.middlewares.test-auth.forwardauth.preserveLocationHeader=true" +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-auth: + forwardAuth: + # ... + preserveLocationHeader: true +``` + +```toml tab="File (TOML)" +[http.middlewares.test-auth.forwardAuth] + # ... + preserveLocationHeader = true +``` + + {!traefik-for-business-applications.md!} diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 5f1810ddc..71e113f88 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -41,6 +41,7 @@ - "traefik.http.middlewares.middleware10.forwardauth.forwardbody=true" - "traefik.http.middlewares.middleware10.forwardauth.headerfield=foobar" - "traefik.http.middlewares.middleware10.forwardauth.maxbodysize=42" +- "traefik.http.middlewares.middleware10.forwardauth.preservelocationheader=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar" - "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.cert=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 9196929cd..1fc5bfebd 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -184,6 +184,7 @@ headerField = "foobar" forwardBody = true maxBodySize = 42 + preserveLocationHeader = true [http.middlewares.Middleware10.forwardAuth.tls] ca = "foobar" cert = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 078a65a74..796209b52 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -211,6 +211,7 @@ http: headerField: foobar forwardBody: true maxBodySize: 42 + preserveLocationHeader: true Middleware11: grpcWeb: allowOrigins: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 9a13876f9..a0b7ed552 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1243,6 +1243,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + preserveLocationHeader: + description: PreserveLocationHeader defines whether to forward + the Location header to the client as is or prefix it with the + domain name of the authentication server. + type: boolean tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 9bc672efc..d5711cf03 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -51,6 +51,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware10/forwardAuth/forwardBody` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/headerField` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/maxBodySize` | `42` | +| `traefik/http/middlewares/Middleware10/forwardAuth/preserveLocationHeader` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/caOptional` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/cert` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 13145ee21..d0473dfc0 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -501,6 +501,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + preserveLocationHeader: + description: PreserveLocationHeader defines whether to forward + the Location header to the client as is or prefix it with the + domain name of the authentication server. + type: boolean tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 9a13876f9..a0b7ed552 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1243,6 +1243,11 @@ spec: allowed to be forwarded to the authentication server. format: int64 type: integer + preserveLocationHeader: + description: PreserveLocationHeader defines whether to forward + the Location header to the client as is or prefix it with the + domain name of the authentication server. + type: boolean tls: description: TLS defines the configuration used to secure the connection to the authentication server. diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 07b22a10a..81f6533a1 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -258,6 +258,8 @@ type ForwardAuth struct { ForwardBody bool `json:"forwardBody,omitempty" toml:"forwardBody,omitempty" yaml:"forwardBody,omitempty" export:"true"` // MaxBodySize defines the maximum body size in bytes allowed to be forwarded to the authentication server. MaxBodySize *int64 `json:"maxBodySize,omitempty" toml:"maxBodySize,omitempty" yaml:"maxBodySize,omitempty" export:"true"` + // PreserveLocationHeader defines whether to forward the Location header to the client as is or prefix it with the domain name of the authentication server. + PreserveLocationHeader bool `json:"preserveLocationHeader,omitempty" toml:"preserveLocationHeader,omitempty" yaml:"preserveLocationHeader,omitempty" export:"true"` } func (f *ForwardAuth) SetDefaults() { diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 408f31ae0..963fa6c87 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -1329,6 +1329,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.PreserveLocationHeader": "false", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index abdde3b6b..b8fa9ac62 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/url" "regexp" "strings" "time" @@ -55,6 +56,7 @@ type forwardAuth struct { headerField string forwardBody bool maxBodySize int64 + preserveLocationHeader bool } // NewForward creates a forward auth middleware. @@ -78,6 +80,7 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu headerField: config.HeaderField, forwardBody: config.ForwardBody, maxBodySize: dynamic.ForwardAuthDefaultMaxBodySize, + preserveLocationHeader: config.PreserveLocationHeader, } if config.MaxBodySize != nil { @@ -222,9 +225,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { utils.CopyHeaders(rw.Header(), forwardResponse.Header) utils.RemoveHeaders(rw.Header(), hopHeaders...) - // Grab the location header, if any. - redirectURL, err := forwardResponse.Location() - + redirectURL, err := fa.redirectURL(forwardResponse) if err != nil { if !errors.Is(err, http.ErrNoLocation) { logger.Debug().Err(err).Msgf("Error reading response location header %s", fa.address) @@ -282,6 +283,18 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { fa.next.ServeHTTP(middlewares.NewResponseModifier(rw, req, fa.buildModifier(authCookies)), req) } +func (fa *forwardAuth) redirectURL(forwardResponse *http.Response) (*url.URL, error) { + if !fa.preserveLocationHeader { + return forwardResponse.Location() + } + + // Preserve the Location header if it exists. + if lv := forwardResponse.Header.Get("Location"); lv != "" { + return url.Parse(lv) + } + return nil, http.ErrNoLocation +} + func (fa *forwardAuth) buildModifier(authCookies []*http.Cookie) func(res *http.Response) error { return func(res *http.Response) error { cookies := res.Cookies() diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 52abe1ea6..9d09617f7 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -711,6 +711,34 @@ func TestForwardAuthTracing(t *testing.T) { } } +func TestForwardAuthPreserveLocationHeader(t *testing.T) { + relativeURL := "/index.html" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", relativeURL) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + t.Cleanup(server.Close) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + auth := dynamic.ForwardAuth{ + Address: server.URL, + PreserveLocationHeader: true, + } + middleware, err := NewForward(context.Background(), next, auth, "authTest") + require.NoError(t, err) + + ts := httptest.NewServer(middleware) + t.Cleanup(ts.Close) + + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Equal(t, relativeURL, res.Header.Get("Location")) +} + type mockTracer struct { embedded.Tracer diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 1183eb3ac..7f004d809 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -790,6 +790,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef AuthRequestHeaders: auth.AuthRequestHeaders, AddAuthCookiesToResponse: auth.AddAuthCookiesToResponse, ForwardBody: auth.ForwardBody, + PreserveLocationHeader: auth.PreserveLocationHeader, } forwardAuth.SetDefaults() diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index 0872a5a9e..fd39ecb7e 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go @@ -165,6 +165,8 @@ type ForwardAuth struct { ForwardBody bool `json:"forwardBody,omitempty"` // MaxBodySize defines the maximum body size in bytes allowed to be forwarded to the authentication server. MaxBodySize *int64 `json:"maxBodySize,omitempty"` + // PreserveLocationHeader defines whether to forward the Location header to the client as is or prefix it with the domain name of the authentication server. + PreserveLocationHeader bool `json:"preserveLocationHeader,omitempty"` } // ClientTLS holds the client TLS configuration. diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 57e115dd9..492bf5936 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -90,6 +90,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true", "traefik/http/middlewares/Middleware08/forwardAuth/forwardBody": "true", "traefik/http/middlewares/Middleware08/forwardAuth/maxBodySize": "42", + "traefik/http/middlewares/Middleware08/forwardAuth/preserveLocationHeader": "true", "traefik/http/middlewares/Middleware15/redirectScheme/scheme": "foobar", "traefik/http/middlewares/Middleware15/redirectScheme/port": "foobar", "traefik/http/middlewares/Middleware15/redirectScheme/permanent": "true", @@ -442,8 +443,9 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, - ForwardBody: true, - MaxBodySize: pointer(int64(42)), + ForwardBody: true, + MaxBodySize: pointer(int64(42)), + PreserveLocationHeader: true, }, }, "Middleware06": {