Repository: traefik/traefik (language: go)

Issue:
Send body from forwardauth middleware
### 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 expect to see?

## Summary

We'd like to send http body to a forward auth external server optionally.

## Background

* I understand sending body could be an overhead. However, there are some situations which we'd like to entitle against an element of body.
* To support the scenario, there is [a community question](https://community.traefik.io/t/forward-auth-middleware-is-there-any-way-to-pass-whole-request-body/20011/2) which suggested to make a feature request and a PR
* We will make a PR for this

## Feature

* Allow `forwardauth` middleware to send body to the configured external server (e.g. example.com/auth in the case) if `forwardBody` is true
* 
```yaml
  middlewares:
    test-auth:
      forwardauth:
        address: "https://example.com/auth"
        forwardBody: true
```




Discussion / hints:
Hey @sokoide.

Thanks for your suggestion.

We are interested in this issue, but we’re unsure about the use case and the traction it will receive. We are going to leave the status as kind/proposal to give the community time to let us know if they would like this idea.

We will reevaluate as people respond.

Conversation is time-boxed to 6 months.
Hi @nmengin, thank you for the info. Our use case is as follows. I hope there are other use cases from the community.

- We put Traefik in front of a backend server (e.g. alertmanager) which doesn't support auth/z
- Traefik `forwardauth` middleware sends the request to `forwardauth` server to check the entitlement
- `forwardauth` server needs the body, for example, when the backend server takes the following POST request and it allows/denies per userid in one of the headers and `ldapgroup` in `labels` in body
```
curl https://$host:$port/api/v1/rules/foo -X POST -d"
rules:
  - alert: test-alert
    labels:
      ldapgroup: foogroup
      severity: critical
" --key key.pem --cert cert.pem --cacert ca.pem
```

Fix patch:
diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md
index b9682b842f..c09a00bda0 100644
--- a/docs/content/middlewares/http/forwardauth.md
+++ b/docs/content/middlewares/http/forwardauth.md
@@ -334,6 +334,98 @@ http:
     addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"]
 ```
 
+### `forwardBody`
+
+_Optional, Default=false_
+
+Set the `forwardBody` option to `true` to send Body.
+
+!!! info
+
+    As body is read inside Traefik before forwarding, this breaks streaming.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-auth.forwardauth.forwardBody=true"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-auth
+spec:
+  forwardAuth:
+    address: https://example.com/auth
+    forwardBody: true
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-auth.forwardauth.forwardBody=true"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-auth:
+      forwardAuth:
+        address: "https://example.com/auth"
+        forwardBody: true
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-auth.forwardAuth]
+    address = "https://example.com/auth"
+    forwardBody = true
+```
+
+### `maxBodySize`
+
+_Optional, Default=-1_
+
+Set the `maxBodySize` to limit the body size in bytes.
+If body is bigger than this, it returns a 401 (unauthorized).
+Default is `-1`, which means no limit.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-auth.forwardauth.maxBodySize=1000"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-auth
+spec:
+  forwardAuth:
+    address: https://example.com/auth
+    forwardBody: true
+    maxBodySize: 1000
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-auth.forwardauth.maxBodySize=1000"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-auth:
+      forwardAuth:
+        address: "https://example.com/auth"
+        maxBodySize: 1000
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-auth.forwardAuth]
+    address = "https://example.com/auth"
+    forwardBody = true
+    maxBodySize = 1000
+```
+
 ### `tls`
 
 _Optional_
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index 63b86f2f0f..5f1810ddce 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -38,7 +38,9 @@
 - "traefik.http.middlewares.middleware10.forwardauth.authrequestheaders=foobar, foobar"
 - "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders=foobar, foobar"
 - "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex=foobar"
+- "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.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 0a0c6ec25b..9196929cd8 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -182,6 +182,8 @@
         authRequestHeaders = ["foobar", "foobar"]
         addAuthCookiesToResponse = ["foobar", "foobar"]
         headerField = "foobar"
+        forwardBody = true
+        maxBodySize = 42
         [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 05908bb184..078a65a744 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -209,6 +209,8 @@ http:
           - foobar
           - foobar
         headerField: foobar
+        forwardBody: true
+        maxBodySize: 42
     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 86ccec1736..9a13876f9a 100644
--- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
+++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
@@ -1234,6 +1234,15 @@ spec:
                       AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
                       More info: https://doc.traefik.io/traefik/v3.2/middlewares/http/forwardauth/#authresponseheadersregex
                     type: string
+                  forwardBody:
+                    description: ForwardBody defines whether to send the request body
+                      to the authentication server.
+                    type: boolean
+                  maxBodySize:
+                    description: MaxBodySize defines the maximum body size in bytes
+                      allowed to be forwarded to the authentication server.
+                    format: int64
+                    type: integer
                   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 46d27bea8f..9bc672efc2 100644
--- a/docs/content/reference/dynamic-configuration/kv-ref.md
+++ b/docs/content/reference/dynamic-configuration/kv-ref.md
@@ -48,7 +48,9 @@ THIS FILE MUST NOT BE EDITED BY HAND
 | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/0` | `foobar` |
 | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/1` | `foobar` |
 | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeadersRegex` | `foobar` |
+| `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/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 fc9c48b965..13145ee21a 100644
--- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
+++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
@@ -492,6 +492,15 @@ spec:
                       AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
                       More info: https://doc.traefik.io/traefik/v3.2/middlewares/http/forwardauth/#authresponseheadersregex
                     type: string
+                  forwardBody:
+                    description: ForwardBody defines whether to send the request body
+                      to the authentication server.
+                    type: boolean
+                  maxBodySize:
+                    description: MaxBodySize defines the maximum body size in bytes
+                      allowed to be forwarded to the authentication server.
+                    format: int64
+                    type: integer
                   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 86ccec1736..9a13876f9a 100644
--- a/integration/fixtures/k8s/01-traefik-crd.yml
+++ b/integration/fixtures/k8s/01-traefik-crd.yml
@@ -1234,6 +1234,15 @@ spec:
                       AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
                       More info: https://doc.traefik.io/traefik/v3.2/middlewares/http/forwardauth/#authresponseheadersregex
                     type: string
+                  forwardBody:
+                    description: ForwardBody defines whether to send the request body
+                      to the authentication server.
+                    type: boolean
+                  maxBodySize:
+                    description: MaxBodySize defines the maximum body size in bytes
+                      allowed to be forwarded to the authentication server.
+                    format: int64
+                    type: integer
                   tls:
                     description: TLS defines the configuration used to secure the
                       connection to the authentication server.
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index 18552294db..7b7e200af0 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -21,6 +21,11 @@ const (
 
 	// DefaultFlushInterval is the default value for the ResponseForwarding flush interval.
 	DefaultFlushInterval = ptypes.Duration(100 * time.Millisecond)
+
+	// MirroringDefaultMirrorBody is the Mirroring.MirrorBody option default value.
+	MirroringDefaultMirrorBody = true
+	// MirroringDefaultMaxBodySize is the Mirroring.MaxBodySize option default value.
+	MirroringDefaultMaxBodySize int64 = -1
 )
 
 // +k8s:deepcopy-gen=true
@@ -100,9 +105,9 @@ type Mirroring struct {
 
 // SetDefaults Default values for a WRRService.
 func (m *Mirroring) SetDefaults() {
-	defaultMirrorBody := true
+	defaultMirrorBody := MirroringDefaultMirrorBody
 	m.MirrorBody = &defaultMirrorBody
-	var defaultMaxBodySize int64 = -1
+	defaultMaxBodySize := MirroringDefaultMaxBodySize
 	m.MaxBodySize = &defaultMaxBodySize
 }
 
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index bf364543e4..07b22a10a4 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -9,6 +9,9 @@ import (
 	"github.com/traefik/traefik/v3/pkg/ip"
 )
 
+// ForwardAuthDefaultMaxBodySize is the ForwardAuth.MaxBodySize option default value.
+const ForwardAuthDefaultMaxBodySize int64 = -1
+
 // +k8s:deepcopy-gen=true
 
 // Middleware holds the Middleware configuration.
@@ -251,6 +254,15 @@ type ForwardAuth struct {
 	// HeaderField defines a header field to store the authenticated user.
 	// More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/#headerfield
 	HeaderField string `json:"headerField,omitempty" toml:"headerField,omitempty" yaml:"headerField,omitempty" export:"true"`
+	// ForwardBody defines whether to send the request body to the authentication server.
+	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"`
+}
+
+func (f *ForwardAuth) SetDefaults() {
+	defaultMaxBodySize := ForwardAuthDefaultMaxBodySize
+	f.MaxBodySize = &defaultMaxBodySize
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go
index 01c4640968..54fc4bc1b9 100644
--- a/pkg/config/dynamic/zz_generated.deepcopy.go
+++ b/pkg/config/dynamic/zz_generated.deepcopy.go
@@ -370,6 +370,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.MaxBodySize != nil {
+		in, out := &in.MaxBodySize, &out.MaxBodySize
+		*out = new(int64)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go
index 97a84994ea..abdde3b6b3 100644
--- a/pkg/middlewares/auth/forward.go
+++ b/pkg/middlewares/auth/forward.go
@@ -1,6 +1,7 @@
 package auth
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"fmt"
@@ -22,13 +23,13 @@ import (
 	"go.opentelemetry.io/otel/trace"
 )
 
+const typeNameForward = "ForwardAuth"
+
 const (
 	xForwardedURI    = "X-Forwarded-Uri"
 	xForwardedMethod = "X-Forwarded-Method"
 )
 
-const typeNameForward = "ForwardAuth"
-
 // hopHeaders Hop-by-hop headers to be removed in the authentication request.
 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
 // Proxy-Authorization header is forwarded to the authentication server (see https://tools.ietf.org/html/rfc7235#section-4.4).
@@ -52,6 +53,8 @@ type forwardAuth struct {
 	authRequestHeaders       []string
 	addAuthCookiesToResponse map[string]struct{}
 	headerField              string
+	forwardBody              bool
+	maxBodySize              int64
 }
 
 // NewForward creates a forward auth middleware.
@@ -73,6 +76,12 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
 		authRequestHeaders:       config.AuthRequestHeaders,
 		addAuthCookiesToResponse: addAuthCookiesToResponse,
 		headerField:              config.HeaderField,
+		forwardBody:              config.ForwardBody,
+		maxBodySize:              dynamic.ForwardAuthDefaultMaxBodySize,
+	}
+
+	if config.MaxBodySize != nil {
+		fa.maxBodySize = *config.MaxBodySize
 	}
 
 	// Ensure our request client does not follow redirects
@@ -125,13 +134,37 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	forwardReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, fa.address, nil)
 	if err != nil {
-		logger.Debug().Msgf("Error calling %s. Cause %s", fa.address, err)
+		logger.Debug().Err(err).Msgf("Error calling %s", fa.address)
 		observability.SetStatusErrorf(req.Context(), "Error calling %s. Cause %s", fa.address, err)
 
 		rw.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
+	if fa.forwardBody {
+		bodyBytes, err := fa.readBodyBytes(req)
+		if errors.Is(err, errBodyTooLarge) {
+			logger.Debug().Msgf("Request body is too large, maxBodySize: %d", fa.maxBodySize)
+
+			observability.SetStatusErrorf(req.Context(), "Request body is too large, maxBodySize: %d", fa.maxBodySize)
+			rw.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		if err != nil {
+			logger.Debug().Err(err).Msg("Error while reading body")
+
+			observability.SetStatusErrorf(req.Context(), "Error while reading Body: %s", err)
+			rw.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+
+		// bodyBytes is nil when the request has no body.
+		if bodyBytes != nil {
+			req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
+			forwardReq.Body = io.NopCloser(bytes.NewReader(bodyBytes))
+		}
+	}
+
 	writeHeader(req, forwardReq, fa.trustForwardHeader, fa.authRequestHeaders)
 
 	var forwardSpan trace.Span
@@ -149,7 +182,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	forwardResponse, forwardErr := fa.client.Do(forwardReq)
 	if forwardErr != nil {
-		logger.Debug().Msgf("Error calling %s. Cause: %s", fa.address, forwardErr)
+		logger.Debug().Err(forwardErr).Msgf("Error calling %s", fa.address)
 		observability.SetStatusErrorf(req.Context(), "Error calling %s. Cause: %s", fa.address, forwardErr)
 
 		rw.WriteHeader(http.StatusInternalServerError)
@@ -159,7 +192,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 	body, readError := io.ReadAll(forwardResponse.Body)
 	if readError != nil {
-		logger.Debug().Msgf("Error reading body %s. Cause: %s", fa.address, readError)
+		logger.Debug().Err(readError).Msgf("Error reading body %s", fa.address)
 		observability.SetStatusErrorf(req.Context(), "Error reading body %s. Cause: %s", fa.address, readError)
 
 		rw.WriteHeader(http.StatusInternalServerError)
@@ -194,7 +227,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 
 		if err != nil {
 			if !errors.Is(err, http.ErrNoLocation) {
-				logger.Debug().Msgf("Error reading response location header %s. Cause: %s", fa.address, err)
+				logger.Debug().Err(err).Msgf("Error reading response location header %s", fa.address)
 				observability.SetStatusErrorf(req.Context(), "Error reading response location header %s. Cause: %s", fa.address, err)
 
 				rw.WriteHeader(http.StatusInternalServerError)
@@ -270,6 +303,27 @@ func (fa *forwardAuth) buildModifier(authCookies []*http.Cookie) func(res *http.
 	}
 }
 
+var errBodyTooLarge = errors.New("request body too large")
+
+func (fa *forwardAuth) readBodyBytes(req *http.Request) ([]byte, error) {
+	if fa.maxBodySize < 0 {
+		return io.ReadAll(req.Body)
+	}
+
+	body := make([]byte, fa.maxBodySize+1)
+	n, err := io.ReadFull(req.Body, body)
+	if errors.Is(err, io.EOF) {
+		return nil, nil
+	}
+	if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
+		return nil, fmt.Errorf("reading body bytes: %w", err)
+	}
+	if errors.Is(err, io.ErrUnexpectedEOF) {
+		return body[:n], nil
+	}
+	return nil, errBodyTooLarge
+}
+
 func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) {
 	utils.CopyHeaders(forwardReq.Header, req.Header)
 
diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index 41b4eff08d..1183eb3ac7 100644
--- a/pkg/provider/kubernetes/crd/kubernetes.go
+++ b/pkg/provider/kubernetes/crd/kubernetes.go
@@ -789,34 +789,38 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
 		AuthResponseHeadersRegex: auth.AuthResponseHeadersRegex,
 		AuthRequestHeaders:       auth.AuthRequestHeaders,
 		AddAuthCookiesToResponse: auth.AddAuthCookiesToResponse,
+		ForwardBody:              auth.ForwardBody,
 	}
+	forwardAuth.SetDefaults()
 
-	if auth.TLS == nil {
-		return forwardAuth, nil
+	if auth.MaxBodySize != nil {
+		forwardAuth.MaxBodySize = auth.MaxBodySize
 	}
 
-	forwardAuth.TLS = &dynamic.ClientTLS{
-		InsecureSkipVerify: auth.TLS.InsecureSkipVerify,
-	}
+	if auth.TLS != nil {
+		forwardAuth.TLS = &dynamic.ClientTLS{
+			InsecureSkipVerify: auth.TLS.InsecureSkipVerify,
+		}
 
-	if len(auth.TLS.CASecret) > 0 {
-		caSecret, err := loadCASecret(namespace, auth.TLS.CASecret, k8sClient)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load auth ca secret: %w", err)
+		if len(auth.TLS.CASecret) > 0 {
+			caSecret, err := loadCASecret(namespace, auth.TLS.CASecret, k8sClient)
+			if err != nil {
+				return nil, fmt.Errorf("failed to load auth ca secret: %w", err)
+			}
+			forwardAuth.TLS.CA = caSecret
 		}
-		forwardAuth.TLS.CA = caSecret
-	}
 
-	if len(auth.TLS.CertSecret) > 0 {
-		authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, auth.TLS.CertSecret, k8sClient)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load auth secret: %w", err)
+		if len(auth.TLS.CertSecret) > 0 {
+			authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, auth.TLS.CertSecret, k8sClient)
+			if err != nil {
+				return nil, fmt.Errorf("failed to load auth secret: %w", err)
+			}
+			forwardAuth.TLS.Cert = authSecretCert
+			forwardAuth.TLS.Key = authSecretKey
 		}
-		forwardAuth.TLS.Cert = authSecretCert
-		forwardAuth.TLS.Key = authSecretKey
-	}
 
-	forwardAuth.TLS.CAOptional = auth.TLS.CAOptional
+		forwardAuth.TLS.CAOptional = auth.TLS.CAOptional
+	}
 
 	return forwardAuth, nil
 }
diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
index fe102fab10..0872a5a9e3 100644
--- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
+++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
@@ -161,6 +161,10 @@ type ForwardAuth struct {
 	TLS *ClientTLS `json:"tls,omitempty"`
 	// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
 	AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
+	// ForwardBody defines whether to send the request body to the authentication server.
+	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"`
 }
 
 // ClientTLS holds the client TLS configuration.
diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
index 45287c4c98..57adcf6586 100644
--- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
@@ -266,6 +266,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.MaxBodySize != nil {
+		in, out := &in.MaxBodySize, &out.MaxBodySize
+		*out = new(int64)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go
index a4c5135e49..83e4f8d37d 100644
--- a/pkg/server/service/service.go
+++ b/pkg/server/service/service.go
@@ -35,11 +35,6 @@ import (
 	"google.golang.org/grpc/status"
 )
 
-const (
-	defaultMirrorBody        = true
-	defaultMaxBodySize int64 = -1
-)
-
 // ProxyBuilder builds reverse proxy handlers.
 type ProxyBuilder interface {
 	Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)
@@ -221,12 +216,12 @@ func (m *Manager) getMirrorServiceHandler(ctx context.Context, config *dynamic.M
 		return nil, err
 	}
 
-	mirrorBody := defaultMirrorBody
+	mirrorBody := dynamic.MirroringDefaultMirrorBody
 	if config.MirrorBody != nil {
 		mirrorBody = *config.MirrorBody
 	}
 
-	maxBodySize := defaultMaxBodySize
+	maxBodySize := dynamic.MirroringDefaultMaxBodySize
 	if config.MaxBodySize != nil {
 		maxBodySize = *config.MaxBodySize
 	}


Test patch:
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index 553689adc0..408f31ae05 100644
--- a/pkg/config/label/label_test.go
+++ b/pkg/config/label/label_test.go
@@ -51,6 +51,8 @@ func TestDecodeConfiguration(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.forwardbody":                             "true",
+		"traefik.http.middlewares.Middleware7.forwardauth.maxbodysize":                             "42",
 		"traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials":               "true",
 		"traefik.http.middlewares.Middleware8.headers.allowedhosts":                                "foobar, fiibar",
 		"traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders":                   "X-foobar, X-fiibar",
@@ -572,6 +574,8 @@ func TestDecodeConfiguration(t *testing.T) {
 							"foobar",
 							"fiibar",
 						},
+						ForwardBody: true,
+						MaxBodySize: pointer(int64(42)),
 					},
 				},
 				"Middleware8": {
@@ -1114,6 +1118,8 @@ func TestEncodeConfiguration(t *testing.T) {
 							"foobar",
 							"fiibar",
 						},
+						ForwardBody: true,
+						MaxBodySize: pointer(int64(42)),
 					},
 				},
 				"Middleware8": {
@@ -1315,6 +1321,8 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.Address":                                 "foobar",
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthResponseHeaders":                     "foobar, fiibar",
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthRequestHeaders":                      "foobar, fiibar",
+		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.ForwardBody":                             "true",
+		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.MaxBodySize":                             "42",
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CA":                                  "foobar",
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CAOptional":                          "true",
 		"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Cert":                                "foobar",
diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go
index 0b41506bbf..52abe1ea64 100644
--- a/pkg/middlewares/auth/forward_test.go
+++ b/pkg/middlewares/auth/forward_test.go
@@ -1,6 +1,7 @@
 package auth
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -112,6 +113,154 @@ func TestForwardAuthSuccess(t *testing.T) {
 	assert.Equal(t, "traefik\n", string(body))
 }
 
+func TestForwardAuthForwardBody(t *testing.T) {
+	data := []byte("forwardBodyTest")
+
+	var serverCallCount int
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		serverCallCount++
+
+		forwardedData, err := io.ReadAll(req.Body)
+		require.NoError(t, err)
+
+		assert.Equal(t, data, forwardedData)
+	}))
+	t.Cleanup(server.Close)
+
+	var nextCallCount int
+	next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		nextCallCount++
+	})
+
+	maxBodySize := int64(len(data))
+	auth := dynamic.ForwardAuth{Address: server.URL, ForwardBody: true, MaxBodySize: &maxBodySize}
+
+	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, bytes.NewReader(data))
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+
+	assert.Equal(t, http.StatusOK, res.StatusCode)
+	assert.Equal(t, 1, serverCallCount)
+	assert.Equal(t, 1, nextCallCount)
+}
+
+func TestForwardAuthForwardBodyEmptyBody(t *testing.T) {
+	var serverCallCount int
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		serverCallCount++
+
+		forwardedData, err := io.ReadAll(req.Body)
+		require.NoError(t, err)
+
+		assert.Empty(t, forwardedData)
+	}))
+	t.Cleanup(server.Close)
+
+	var nextCallCount int
+	next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		nextCallCount++
+	})
+
+	auth := dynamic.ForwardAuth{Address: server.URL, ForwardBody: 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, http.NoBody)
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+
+	assert.Equal(t, http.StatusOK, res.StatusCode)
+	assert.Equal(t, 1, serverCallCount)
+	assert.Equal(t, 1, nextCallCount)
+}
+
+func TestForwardAuthForwardBodySizeLimit(t *testing.T) {
+	data := []byte("forwardBodyTest")
+
+	var serverCallCount int
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		serverCallCount++
+
+		forwardedData, err := io.ReadAll(req.Body)
+		require.NoError(t, err)
+
+		assert.Equal(t, data, forwardedData)
+	}))
+	t.Cleanup(server.Close)
+
+	var nextCallCount int
+	next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		nextCallCount++
+	})
+
+	maxBodySize := int64(len(data)) - 1
+	auth := dynamic.ForwardAuth{Address: server.URL, ForwardBody: true, MaxBodySize: &maxBodySize}
+
+	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, bytes.NewReader(data))
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+
+	assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
+	assert.Equal(t, 0, serverCallCount)
+	assert.Equal(t, 0, nextCallCount)
+}
+
+func TestForwardAuthNotForwardBody(t *testing.T) {
+	data := []byte("forwardBodyTest")
+
+	var serverCallCount int
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		serverCallCount++
+
+		forwardedData, err := io.ReadAll(req.Body)
+		require.NoError(t, err)
+
+		assert.Empty(t, forwardedData)
+	}))
+	t.Cleanup(server.Close)
+
+	var nextCallCount int
+	next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		nextCallCount++
+	})
+
+	auth := dynamic.ForwardAuth{Address: server.URL}
+
+	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, bytes.NewReader(data))
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+
+	assert.Equal(t, http.StatusOK, res.StatusCode)
+	assert.Equal(t, 1, serverCallCount)
+	assert.Equal(t, 1, nextCallCount)
+}
+
 func TestForwardAuthRedirect(t *testing.T) {
 	authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, "http://example.com/redirect-test", http.StatusFound)
diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go
index a7ceb391e8..e354d3cf74 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_test.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_test.go
@@ -3915,7 +3915,8 @@ func TestLoadIngressRoutes(t *testing.T) {
 						},
 						"default-forwardauth": {
 							ForwardAuth: &dynamic.ForwardAuth{
-								Address: "test.com",
+								Address:     "test.com",
+								MaxBodySize: pointer(int64(-1)),
 								TLS: &dynamic.ClientTLS{
 									CA:   "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
 									Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go
index 4826a94081..57e115dd9f 100644
--- a/pkg/provider/kv/kv_test.go
+++ b/pkg/provider/kv/kv_test.go
@@ -88,6 +88,8 @@ func Test_buildConfiguration(t *testing.T) {
 		"traefik/http/middlewares/Middleware08/forwardAuth/tls/cert":                                 "foobar",
 		"traefik/http/middlewares/Middleware08/forwardAuth/address":                                  "foobar",
 		"traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader":                       "true",
+		"traefik/http/middlewares/Middleware08/forwardAuth/forwardBody":                              "true",
+		"traefik/http/middlewares/Middleware08/forwardAuth/maxBodySize":                              "42",
 		"traefik/http/middlewares/Middleware15/redirectScheme/scheme":                                "foobar",
 		"traefik/http/middlewares/Middleware15/redirectScheme/port":                                  "foobar",
 		"traefik/http/middlewares/Middleware15/redirectScheme/permanent":                             "true",
@@ -440,6 +442,8 @@ func Test_buildConfiguration(t *testing.T) {
 							"foobar",
 							"foobar",
 						},
+						ForwardBody: true,
+						MaxBodySize: pointer(int64(42)),
 					},
 				},
 				"Middleware06": {