Repository: traefik/traefik (language: go)

Issue:
Middleware of ForwardAuth returns incorrect location header
### Welcome!

- [x] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [x] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

I made an auth server that returns a relative url as the Location header in the response. I'm expecting the relative url will be passed to the downstream and then be resolved by the browser or other clients.

### What did you see instead?

ForwardAuth middleware resolves the location header and return the resolved url as the location header in the response. While the correct behavior should just return the original location header.

https://github.com/traefik/traefik/blob/33c1d700c02ad09ab793673a14674b39d78fe352/pkg/middlewares/auth/forward.go#L193

For example,
- Auth server endpoint address: http://forward-auth.server.local:8080/auth
- Auth server response Location header: /index.html
- Downstream received response Location header: http://forward-auth.server.local:8080/index.html
- Expected Location header: /index.html

Note, that the location should be resolved at the customer side, like the browser, which should be relative to the original forwarded domain, not the auth server.

### What version of Traefik are you using?

v3.1.6
I checked the latest code. It seems it's still the same behavior.

### What is your environment & configuration?

Not related. But can provide if being asked.


### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:
Hi @cirias thanks for reporting this.
If anyone can help contributing on a fix, this would be great :)
@emilevauge I made a PR to fix this, Please review

Fix patch:
diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md
index c09a00bda0..f6e30b8279 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 5f1810ddce..71e113f888 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 9196929cd8..1fc5bfebd0 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 078a65a744..796209b52d 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 9a13876f9a..a0b7ed5526 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 9bc672efc2..d5711cf03b 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 13145ee21a..d0473dfc04 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 9a13876f9a..a0b7ed5526 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 07b22a10a4..81f6533a11 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/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go
index abdde3b6b3..b8fa9ac62f 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/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index 1183eb3ac7..7f004d8095 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 0872a5a9e3..fd39ecb7e2 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.


Test patch:
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index 408f31ae05..963fa6c876 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_test.go b/pkg/middlewares/auth/forward_test.go
index 52abe1ea64..9d09617f75 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/kv/kv_test.go b/pkg/provider/kv/kv_test.go
index 57e115dd9f..492bf59360 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": {