From 27912e38496391a2acb43ce6c694748a6b9529f4 Mon Sep 17 00:00:00 2001 From: kyounghoonJang Date: Mon, 26 Jan 2026 18:12:05 +0900 Subject: [PATCH] Add authSignInURL in forward auth middleware --- .../kubernetes-crd-definition-v1.yml | 4 + .../traefik.io_middlewares.yaml | 4 + .../http/middlewares/forwardauth.md | 39 ++++----- .../other-providers/file.toml | 1 + .../other-providers/file.yaml | 1 + integration/fixtures/k8s/01-traefik-crd.yml | 4 + pkg/config/dynamic/middlewares.go | 2 + pkg/middlewares/auth/forward.go | 11 +++ pkg/middlewares/auth/forward_test.go | 85 +++++++++++++++++++ .../traefikio/v1alpha1/forwardauth.go | 9 ++ pkg/provider/kubernetes/crd/kubernetes.go | 1 + .../crd/traefikio/v1alpha1/middleware.go | 2 + 12 files changed, 144 insertions(+), 19 deletions(-) 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 5997a58cd..d392b67a5 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1434,6 +1434,10 @@ 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.6/reference/routing-configuration/http/middlewares/forwardauth/#authresponseheadersregex type: string + authSigninURL: + description: AuthSigninURL specifies the URL to redirect to when + the authentication server returns 401 Unauthorized. + type: string forwardBody: description: ForwardBody defines whether to send the request body to the authentication server. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 107d814f9..7ca937bb1 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -589,6 +589,10 @@ 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.6/reference/routing-configuration/http/middlewares/forwardauth/#authresponseheadersregex type: string + authSigninURL: + description: AuthSigninURL specifies the URL to redirect to when + the authentication server returns 401 Unauthorized. + type: string forwardBody: description: ForwardBody defines whether to send the request body to the authentication server. diff --git a/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md b/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md index 30bc78ec7..8d30db62f 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md +++ b/docs/content/reference/routing-configuration/http/middlewares/forwardauth.md @@ -53,25 +53,26 @@ spec: ## Configuration Options -| Field | Description | Default | Required | -|:-----------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------| -| `address` | Authentication server address. | "" | Yes | -| `trustForwardHeader` | Trust all `X-Forwarded-*` headers. | false | No | -| `authResponseHeaders` | List of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers. | [] | No | -| `authResponseHeadersRegex` | Regex to match by the headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
More information [here](#authresponseheadersregex). | "" | No | -| `authRequestHeaders` | List of the headers to copy from the request to the authentication server.
It allows filtering headers that should not be passed to the authentication server.
If not set or empty, then all request headers are passed. | [] | No | -| `addAuthCookiesToResponse` | List of cookies to copy from the authentication server to the response, replacing any existing conflicting cookie from the forwarded response.
Please note that all backend cookies matching the configured list will not be added to the response. | [] | No | -| `forwardBody` | Sets the `forwardBody` option to `true` to send the Body. As body is read inside Traefik before forwarding, this breaks streaming. | false | No | -| `maxBodySize` | Set the `maxBodySize` to limit the body size in bytes. If body is bigger than this, it returns a 401 (unauthorized). If left unset, the request body size is unrestricted which can have performance or security implications. < br/>More information [here](#maxbodysize).| -1 | No | -| `headerField` | Defines a header field to store the authenticated user. | "" | No | -| `preserveLocationHeader` | Defines whether to forward the Location header to the client as is or prefix it with the domain name of the authentication server. | false | No | -| `preserveRequestMethod` | Defines whether to preserve the original request method while forwarding the request to the authentication server. | false | No | -| `tls.ca` | Sets the path to the certificate authority used for the secured connection to the authentication server, it defaults to the system bundle. | "" | No | -| `tls.cert` | Sets the path to the public certificate used for the secure connection to the authentication server. When using this option, setting the key option is required. | "" | No | -| `tls.key` | Sets the path to the private key used for the secure connection to the authentication server. When using this option, setting the `cert` option is required. | "" | No | -| `tls.caSecret` | Defines the secret that contains the certificate authority used for the secured connection to the authentication server, it defaults to the system bundle. **This option is only available for the Kubernetes CRD**. | | No | -| `tls.certSecret` | Defines the secret that contains both the private and public certificates used for the secure connection to the authentication server. **This option is only available for the Kubernetes CRD**. | | No | -| `tls.insecureSkipVerify` | During TLS connections, if this option is set to `true`, the authentication server will accept any certificate presented by the server regardless of the host names it covers. | false | No | +| Field | Description | Default | Required | +|:-----------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------| +| `address` | Authentication server address. | "" | Yes | +| `trustForwardHeader` | Trust all `X-Forwarded-*` headers. | false | No | +| `authResponseHeaders` | List of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers. | [] | No | +| `authResponseHeadersRegex` | Regex to match by the headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
More information [here](#authresponseheadersregex). | "" | No | +| `authRequestHeaders` | List of the headers to copy from the request to the authentication server.
It allows filtering headers that should not be passed to the authentication server.
If not set or empty, then all request headers are passed. | [] | No | +| `addAuthCookiesToResponse` | List of cookies to copy from the authentication server to the response, replacing any existing conflicting cookie from the forwarded response.
Please note that all backend cookies matching the configured list will not be added to the response. | [] | No | +| `forwardBody` | Sets the `forwardBody` option to `true` to send the Body. As body is read inside Traefik before forwarding, this breaks streaming. | false | No | +| `maxBodySize` | Set the `maxBodySize` to limit the body size in bytes. If body is bigger than this, it returns a 401 (unauthorized). If left unset, the request body size is unrestricted which can have performance or security implications. < br/>More information [here](#maxbodysize). | -1 | No | +| `headerField` | Defines a header field to store the authenticated user. | "" | No | +| `preserveLocationHeader` | Defines whether to forward the Location header to the client as is or prefix it with the domain name of the authentication server. | false | No | +| `preserveRequestMethod` | Defines whether to preserve the original request method while forwarding the request to the authentication server. | false | No | +| `authSigninURL` | Specifies the URL to redirect to when the authentication server returns 401 Unauthorized. | "" | No | +| `tls.ca` | Sets the path to the certificate authority used for the secured connection to the authentication server, it defaults to the system bundle. | "" | No | +| `tls.cert` | Sets the path to the public certificate used for the secure connection to the authentication server. When using this option, setting the key option is required. | "" | No | +| `tls.key` | Sets the path to the private key used for the secure connection to the authentication server. When using this option, setting the `cert` option is required. | "" | No | +| `tls.caSecret` | Defines the secret that contains the certificate authority used for the secured connection to the authentication server, it defaults to the system bundle. **This option is only available for the Kubernetes CRD**. | | No | +| `tls.certSecret` | Defines the secret that contains both the private and public certificates used for the secure connection to the authentication server. **This option is only available for the Kubernetes CRD**. | | No | +| `tls.insecureSkipVerify` | During TLS connections, if this option is set to `true`, the authentication server will accept any certificate presented by the server regardless of the host names it covers. | false | No | ### authResponseHeadersRegex diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 8e2d3db5e..9a6c18e59 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -220,6 +220,7 @@ maxBodySize = 42 preserveLocationHeader = true preserveRequestMethod = true + authSigninURL = "foobar" [http.middlewares.Middleware11.forwardAuth.tls] ca = "foobar" cert = "foobar" diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index dc56188e2..c15d2e423 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -248,6 +248,7 @@ http: maxBodySize: 42 preserveLocationHeader: true preserveRequestMethod: true + authSigninURL: foobar Middleware12: grpcWeb: allowOrigins: diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index a525c8d65..be4c1f2f4 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1435,6 +1435,10 @@ 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.6/reference/routing-configuration/http/middlewares/forwardauth/#authresponseheadersregex type: string + authSigninURL: + description: AuthSigninURL specifies the URL to redirect to when + the authentication server returns 401 Unauthorized. + type: string forwardBody: description: ForwardBody defines whether to send the request body to the authentication server. diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 1d03e47cc..0896d66aa 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -292,6 +292,8 @@ type ForwardAuth struct { PreserveLocationHeader bool `json:"preserveLocationHeader,omitempty" toml:"preserveLocationHeader,omitempty" yaml:"preserveLocationHeader,omitempty" export:"true"` // PreserveRequestMethod defines whether to preserve the original request method while forwarding the request to the authentication server. PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty" toml:"preserveRequestMethod,omitempty" yaml:"preserveRequestMethod,omitempty" export:"true"` + // AuthSigninURL specifies the URL to redirect to when the authentication server returns 401 Unauthorized. + AuthSigninURL string `json:"authSigninURL,omitempty" toml:"authSigninURL,omitempty" yaml:"authSigninURL,omitempty" export:"true"` } func (f *ForwardAuth) SetDefaults() { diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index dbaddef2c..2a84c581b 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -59,6 +59,7 @@ type forwardAuth struct { maxBodySize int64 preserveLocationHeader bool preserveRequestMethod bool + authSigninURL string } // NewForward creates a forward auth middleware. @@ -84,6 +85,7 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu maxBodySize: dynamic.ForwardAuthDefaultMaxBodySize, preserveLocationHeader: config.PreserveLocationHeader, preserveRequestMethod: config.PreserveRequestMethod, + authSigninURL: config.AuthSigninURL, } if config.MaxBodySize != nil { @@ -232,6 +234,15 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } + // If auth server returns 401 and AuthSigninURL is configured, redirect to signin URL. + if fa.authSigninURL != "" && forwardResponse.StatusCode == http.StatusUnauthorized { + logger.Debug().Msgf("Redirecting to signin URL: %s", fa.authSigninURL) + + tracer.CaptureResponse(forwardSpan, forwardResponse.Header, http.StatusFound, trace.SpanKindClient) + http.Redirect(rw, req, fa.authSigninURL, http.StatusFound) + return + } + // Pass the forward response's body and selected headers if it // didn't return a response within the range of [200, 300). if forwardResponse.StatusCode < http.StatusOK || forwardResponse.StatusCode >= http.StatusMultipleChoices { diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 95e3d7910..24ce2aaa3 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -872,6 +872,91 @@ func TestForwardAuthPreserveRequestMethod(t *testing.T) { } } +func TestForwardAuthAuthSigninURL(t *testing.T) { + testCases := []struct { + desc string + authSigninURL string + authServerStatus int + expectedStatus int + expectedLocation string + nextShouldBeCalled bool + }{ + { + desc: "redirects to signin URL on 401", + authSigninURL: "https://auth.example.com/login", + authServerStatus: http.StatusUnauthorized, + expectedStatus: http.StatusFound, + expectedLocation: "https://auth.example.com/login", + nextShouldBeCalled: false, + }, + { + desc: "no redirect on 401 without signin URL", + authServerStatus: http.StatusUnauthorized, + expectedStatus: http.StatusUnauthorized, + nextShouldBeCalled: false, + }, + { + desc: "no redirect on other error statuses with signin URL", + authSigninURL: "https://auth.example.com/login", + authServerStatus: http.StatusForbidden, + expectedStatus: http.StatusForbidden, + nextShouldBeCalled: false, + }, + { + desc: "no redirect on OK status with signin URL", + authSigninURL: "https://auth.example.com/login", + authServerStatus: http.StatusOK, + expectedStatus: http.StatusOK, + nextShouldBeCalled: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(test.authServerStatus), test.authServerStatus) + })) + t.Cleanup(authServer.Close) + + nextCalled := false + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + }) + + auth := dynamic.ForwardAuth{ + Address: authServer.URL, + AuthSigninURL: test.authSigninURL, + } + middleware, err := NewForward(t.Context(), next, auth, "authTest") + require.NoError(t, err) + + ts := httptest.NewServer(middleware) + t.Cleanup(ts.Close) + + client := &http.Client{ + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + res, err := client.Do(req) + require.NoError(t, err) + + assert.Equal(t, test.expectedStatus, res.StatusCode) + assert.Equal(t, test.nextShouldBeCalled, nextCalled) + + if test.expectedLocation != "" { + location, err := res.Location() + require.NoError(t, err) + assert.Equal(t, test.expectedLocation, location.String()) + } else { + assert.Empty(t, res.Header.Get("Location")) + } + }) + } +} + type mockTracer struct { embedded.Tracer diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go index ab9d603b3..6eaa35033 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/forwardauth.go @@ -41,6 +41,7 @@ type ForwardAuthApplyConfiguration struct { MaxBodySize *int64 `json:"maxBodySize,omitempty"` PreserveLocationHeader *bool `json:"preserveLocationHeader,omitempty"` PreserveRequestMethod *bool `json:"preserveRequestMethod,omitempty"` + AuthSigninURL *string `json:"authSigninURL,omitempty"` } // ForwardAuthApplyConfiguration constructs a declarative configuration of the ForwardAuth type for use with @@ -150,3 +151,11 @@ func (b *ForwardAuthApplyConfiguration) WithPreserveRequestMethod(value bool) *F b.PreserveRequestMethod = &value return b } + +// WithAuthSigninURL sets the AuthSigninURL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuthSigninURL field is set to the value of the last call. +func (b *ForwardAuthApplyConfiguration) WithAuthSigninURL(value string) *ForwardAuthApplyConfiguration { + b.AuthSigninURL = &value + return b +} diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 309e792be..e1bf9dbbb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -1011,6 +1011,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef ForwardBody: auth.ForwardBody, PreserveLocationHeader: auth.PreserveLocationHeader, PreserveRequestMethod: auth.PreserveRequestMethod, + AuthSigninURL: auth.AuthSigninURL, } forwardAuth.SetDefaults() diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index b237497f1..4f30d35fe 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go @@ -185,6 +185,8 @@ type ForwardAuth struct { PreserveLocationHeader bool `json:"preserveLocationHeader,omitempty"` // PreserveRequestMethod defines whether to preserve the original request method while forwarding the request to the authentication server. PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty"` + // AuthSigninURL specifies the URL to redirect to when the authentication server returns 401 Unauthorized. + AuthSigninURL string `json:"authSigninURL,omitempty"` } // +k8s:deepcopy-gen=true