diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index b9682b842..c09a00bda 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 63b86f2f0..5f1810ddc 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 0a0c6ec25..9196929cd 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 05908bb18..078a65a74 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 86ccec173..9a13876f9 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 46d27bea8..9bc672efc 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 fc9c48b96..13145ee21 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 86ccec173..9a13876f9 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 18552294d..7b7e200af 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 bf364543e..07b22a10a 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 01c464096..54fc4bc1b 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/config/label/label_test.go b/pkg/config/label/label_test.go index 553689adc..408f31ae0 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.go b/pkg/middlewares/auth/forward.go index 97a84994e..abdde3b6b 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/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 0b41506bb..52abe1ea6 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.go b/pkg/provider/kubernetes/crd/kubernetes.go index 41b4eff08..1183eb3ac 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.MaxBodySize != nil { + forwardAuth.MaxBodySize = auth.MaxBodySize } - if auth.TLS == nil { - return forwardAuth, 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 auth.TLS != nil { + forwardAuth.TLS = &dynamic.ClientTLS{ + InsecureSkipVerify: auth.TLS.InsecureSkipVerify, } - 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.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.Cert = authSecretCert - forwardAuth.TLS.Key = authSecretKey - } - forwardAuth.TLS.CAOptional = auth.TLS.CAOptional + 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.CAOptional = auth.TLS.CAOptional + } return forwardAuth, nil } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index a7ceb391e..e354d3cf7 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/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index fe102fab1..0872a5a9e 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 45287c4c9..57adcf658 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/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 4826a9408..57e115dd9 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": { diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index a4c5135e4..83e4f8d37 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 }