Add forwardAuth.addAuthCookiesToResponse

This commit is contained in:
Thomas Gunsch 2024-01-15 16:14:05 +01:00 committed by GitHub
parent 980dac4572
commit 81ce45271d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 155 additions and 19 deletions

View file

@ -285,6 +285,55 @@ http:
authRequestHeaders = "Accept,X-CustomHeader" authRequestHeaders = "Accept,X-CustomHeader"
``` ```
### `addAuthCookiesToResponse`
The `addAuthCookiesToResponse` option is the list of cookies to copy from the authentication server to the response,
replacing any existing conflicting cookie from the forwarded response.
!!! info
Please note that all backend cookies matching the configured list will not be added to the response.
```yaml tab="Docker"
labels:
- "traefik.http.middlewares.test-auth.forwardauth.addAuthCookiesToResponse=Session-Cookie,State-Cookie"
```
```yaml tab="Kubernetes"
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-auth
spec:
forwardAuth:
address: https://example.com/auth
addAuthCookiesToResponse:
- Session-Cookie
- State-Cookie
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-auth.forwardauth.addAuthCookiesToResponse=Session-Cookie,State-Cookie"
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-auth.forwardAuth]
address = "https://example.com/auth"
addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"]
```
```yaml tab="File (YAML)"
http:
middlewares:
test-auth:
forwardAuth:
address: "https://example.com/auth"
addAuthCookiesToResponse:
- "Session-Cookie"
- "State-Cookie"
```
### `tls` ### `tls`
_Optional_ _Optional_

View file

@ -30,6 +30,7 @@
- "traefik.http.middlewares.middleware09.forwardauth.authresponseheaders=foobar, foobar" - "traefik.http.middlewares.middleware09.forwardauth.authresponseheaders=foobar, foobar"
- "traefik.http.middlewares.middleware09.forwardauth.authresponseheadersregex=foobar" - "traefik.http.middlewares.middleware09.forwardauth.authresponseheadersregex=foobar"
- "traefik.http.middlewares.middleware09.forwardauth.authrequestheaders=foobar, foobar" - "traefik.http.middlewares.middleware09.forwardauth.authrequestheaders=foobar, foobar"
- "traefik.http.middlewares.middleware09.forwardauth.addauthcookiestoresponse=foobar, foobar"
- "traefik.http.middlewares.middleware09.forwardauth.tls.ca=foobar" - "traefik.http.middlewares.middleware09.forwardauth.tls.ca=foobar"
- "traefik.http.middlewares.middleware09.forwardauth.tls.cert=foobar" - "traefik.http.middlewares.middleware09.forwardauth.tls.cert=foobar"
- "traefik.http.middlewares.middleware09.forwardauth.tls.insecureskipverify=true" - "traefik.http.middlewares.middleware09.forwardauth.tls.insecureskipverify=true"

View file

@ -156,6 +156,7 @@
authResponseHeaders = ["foobar", "foobar"] authResponseHeaders = ["foobar", "foobar"]
authResponseHeadersRegex = "foobar" authResponseHeadersRegex = "foobar"
authRequestHeaders = ["foobar", "foobar"] authRequestHeaders = ["foobar", "foobar"]
addAuthCookiesToResponse = ["foobar", "foobar"]
[http.middlewares.Middleware09.forwardAuth.tls] [http.middlewares.Middleware09.forwardAuth.tls]
ca = "foobar" ca = "foobar"
cert = "foobar" cert = "foobar"

View file

@ -174,6 +174,9 @@ http:
authRequestHeaders: authRequestHeaders:
- foobar - foobar
- foobar - foobar
addAuthCookiesToResponse:
- foobar
- foobar
Middleware10: Middleware10:
headers: headers:
customRequestHeaders: customRequestHeaders:

View file

@ -912,6 +912,12 @@ spec:
This middleware delegates the request authentication to a Service. This middleware delegates the request authentication to a Service.
More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/' More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/'
properties: properties:
addAuthCookiesToResponse:
description: AddAuthCookiesToResponse defines the list of cookies
to copy from the authentication server response to the response.
items:
type: string
type: array
address: address:
description: Address defines the authentication server address. description: Address defines the authentication server address.
type: string type: string

View file

@ -30,6 +30,8 @@
| `traefik/http/middlewares/Middleware08/errors/service` | `foobar` | | `traefik/http/middlewares/Middleware08/errors/service` | `foobar` |
| `traefik/http/middlewares/Middleware08/errors/status/0` | `foobar` | | `traefik/http/middlewares/Middleware08/errors/status/0` | `foobar` |
| `traefik/http/middlewares/Middleware08/errors/status/1` | `foobar` | | `traefik/http/middlewares/Middleware08/errors/status/1` | `foobar` |
| `traefik/http/middlewares/Middleware09/forwardAuth/addAuthCookiesToResponse/0` | `foobar` |
| `traefik/http/middlewares/Middleware09/forwardAuth/addAuthCookiesToResponse/1` | `foobar` |
| `traefik/http/middlewares/Middleware09/forwardAuth/address` | `foobar` | | `traefik/http/middlewares/Middleware09/forwardAuth/address` | `foobar` |
| `traefik/http/middlewares/Middleware09/forwardAuth/authRequestHeaders/0` | `foobar` | | `traefik/http/middlewares/Middleware09/forwardAuth/authRequestHeaders/0` | `foobar` |
| `traefik/http/middlewares/Middleware09/forwardAuth/authRequestHeaders/1` | `foobar` | | `traefik/http/middlewares/Middleware09/forwardAuth/authRequestHeaders/1` | `foobar` |

View file

@ -337,6 +337,12 @@ spec:
This middleware delegates the request authentication to a Service. This middleware delegates the request authentication to a Service.
More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/' More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/'
properties: properties:
addAuthCookiesToResponse:
description: AddAuthCookiesToResponse defines the list of cookies
to copy from the authentication server response to the response.
items:
type: string
type: array
address: address:
description: Address defines the authentication server address. description: Address defines the authentication server address.
type: string type: string

View file

@ -912,6 +912,12 @@ spec:
This middleware delegates the request authentication to a Service. This middleware delegates the request authentication to a Service.
More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/' More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/forwardauth/'
properties: properties:
addAuthCookiesToResponse:
description: AddAuthCookiesToResponse defines the list of cookies
to copy from the authentication server response to the response.
items:
type: string
type: array
address: address:
description: Address defines the authentication server address. description: Address defines the authentication server address.
type: string type: string

View file

@ -223,6 +223,8 @@ type ForwardAuth struct {
// AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server. // AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server.
// If not set or empty then all request headers are passed. // If not set or empty then all request headers are passed.
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"` AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"`
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty" toml:"addAuthCookiesToResponse,omitempty" yaml:"addAuthCookiesToResponse,omitempty" export:"true"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true

View file

@ -324,6 +324,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AddAuthCookiesToResponse != nil {
in, out := &in.AddAuthCookiesToResponse, &out.AddAuthCookiesToResponse
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }

View file

@ -48,19 +48,26 @@ type forwardAuth struct {
client http.Client client http.Client
trustForwardHeader bool trustForwardHeader bool
authRequestHeaders []string authRequestHeaders []string
addAuthCookiesToResponse map[string]struct{}
} }
// NewForward creates a forward auth middleware. // NewForward creates a forward auth middleware.
func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) { func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, typeNameForward).Debug().Msg("Creating middleware") middlewares.GetLogger(ctx, name, typeNameForward).Debug().Msg("Creating middleware")
addAuthCookiesToResponse := make(map[string]struct{})
for _, cookieName := range config.AddAuthCookiesToResponse {
addAuthCookiesToResponse[cookieName] = struct{}{}
}
fa := &forwardAuth{ fa := &forwardAuth{
address: config.Address, address: config.Address,
authResponseHeaders: config.AuthResponseHeaders, authResponseHeaders: config.AuthResponseHeaders,
next: next, next: next,
name: name, name: name,
trustForwardHeader: config.TrustForwardHeader, trustForwardHeader: config.TrustForwardHeader,
authRequestHeaders: config.AuthRequestHeaders, authRequestHeaders: config.AuthRequestHeaders,
addAuthCookiesToResponse: addAuthCookiesToResponse,
} }
// Ensure our request client does not follow redirects // Ensure our request client does not follow redirects
@ -211,7 +218,35 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
tracing.LogResponseCode(forwardSpan, forwardResponse.StatusCode, trace.SpanKindClient) tracing.LogResponseCode(forwardSpan, forwardResponse.StatusCode, trace.SpanKindClient)
req.RequestURI = req.URL.RequestURI() req.RequestURI = req.URL.RequestURI()
fa.next.ServeHTTP(rw, req)
authCookies := forwardResponse.Cookies()
if len(authCookies) == 0 {
fa.next.ServeHTTP(rw, req)
return
}
fa.next.ServeHTTP(middlewares.NewResponseModifier(rw, req, fa.buildModifier(authCookies)), req)
}
func (fa *forwardAuth) buildModifier(authCookies []*http.Cookie) func(res *http.Response) error {
return func(res *http.Response) error {
cookies := res.Cookies()
res.Header.Del("Set-Cookie")
for _, cookie := range cookies {
if _, found := fa.addAuthCookiesToResponse[cookie.Name]; !found {
res.Header.Add("Set-Cookie", cookie.String())
}
}
for _, cookie := range authCookies {
if _, found := fa.addAuthCookiesToResponse[cookie.Name]; found {
res.Header.Add("Set-Cookie", cookie.String())
}
}
return nil
}
} }
func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) { func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) {

View file

@ -66,6 +66,8 @@ func TestForwardAuthSuccess(t *testing.T) {
w.Header().Add("X-Auth-Group", "group1") w.Header().Add("X-Auth-Group", "group1")
w.Header().Add("X-Auth-Group", "group2") w.Header().Add("X-Auth-Group", "group2")
w.Header().Add("Foo-Bar", "auth-value") w.Header().Add("Foo-Bar", "auth-value")
w.Header().Add("Set-Cookie", "authCookie=Auth")
w.Header().Add("Set-Cookie", "authCookieNotAdded=Auth")
fmt.Fprintln(w, "Success") fmt.Fprintln(w, "Success")
})) }))
t.Cleanup(server.Close) t.Cleanup(server.Close)
@ -76,6 +78,9 @@ func TestForwardAuthSuccess(t *testing.T) {
assert.Equal(t, []string{"group1", "group2"}, r.Header["X-Auth-Group"]) assert.Equal(t, []string{"group1", "group2"}, r.Header["X-Auth-Group"])
assert.Equal(t, "auth-value", r.Header.Get("Foo-Bar")) assert.Equal(t, "auth-value", r.Header.Get("Foo-Bar"))
assert.Empty(t, r.Header.Get("Foo-Baz")) assert.Empty(t, r.Header.Get("Foo-Baz"))
w.Header().Add("Set-Cookie", "authCookie=Backend")
w.Header().Add("Set-Cookie", "backendCookie=Backend")
w.Header().Add("Other-Header", "BackendHeaderValue")
fmt.Fprintln(w, "traefik") fmt.Fprintln(w, "traefik")
}) })
@ -83,6 +88,7 @@ func TestForwardAuthSuccess(t *testing.T) {
Address: server.URL, Address: server.URL,
AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Group"}, AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Group"},
AuthResponseHeadersRegex: "^Foo-", AuthResponseHeadersRegex: "^Foo-",
AddAuthCookiesToResponse: []string{"authCookie"},
} }
middleware, err := NewForward(context.Background(), next, auth, "authTest") middleware, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err) require.NoError(t, err)
@ -97,6 +103,8 @@ func TestForwardAuthSuccess(t *testing.T) {
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode) assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, []string{"backendCookie=Backend", "authCookie=Auth"}, res.Header["Set-Cookie"])
assert.Equal(t, []string{"BackendHeaderValue"}, res.Header["Other-Header"])
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
require.NoError(t, err) require.NoError(t, err)

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/vulcand/oxy/v2/forward" "github.com/vulcand/oxy/v2/forward"
) )
@ -58,7 +59,7 @@ func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// If there is a next, call it. // If there is a next, call it.
if s.next != nil { if s.next != nil {
s.next.ServeHTTP(newResponseModifier(rw, req, s.PostRequestModifyResponseHeaders), req) s.next.ServeHTTP(middlewares.NewResponseModifier(rw, req, s.PostRequestModifyResponseHeaders), req)
} }
} }

View file

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/unrolled/secure" "github.com/unrolled/secure"
) )
@ -45,6 +46,6 @@ func newSecure(next http.Handler, cfg dynamic.Headers, contextKey string) *secur
func (s secureHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (s secureHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.secure.HandlerFuncWithNextForRequestOnly(rw, req, func(writer http.ResponseWriter, request *http.Request) { s.secure.HandlerFuncWithNextForRequestOnly(rw, req, func(writer http.ResponseWriter, request *http.Request) {
s.next.ServeHTTP(newResponseModifier(writer, request, s.secure.ModifyResponseHeaders), request) s.next.ServeHTTP(middlewares.NewResponseModifier(writer, request, s.secure.ModifyResponseHeaders), request)
}) })
} }

View file

@ -1,4 +1,4 @@
package headers package middlewares
import ( import (
"bufio" "bufio"
@ -9,7 +9,8 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type responseModifier struct { // ResponseModifier is a ResponseWriter to modify the response headers before sending them.
type ResponseModifier struct {
req *http.Request req *http.Request
rw http.ResponseWriter rw http.ResponseWriter
@ -21,9 +22,10 @@ type responseModifier struct {
modifierErr error // returned by modifier call modifierErr error // returned by modifier call
} }
// modifier can be nil. // NewResponseModifier returns a new ResponseModifier instance.
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) http.ResponseWriter { // The given modifier can be nil.
return &responseModifier{ func NewResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) http.ResponseWriter {
return &ResponseModifier{
req: r, req: r,
rw: w, rw: w,
modifier: modifier, modifier: modifier,
@ -33,7 +35,7 @@ func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*
// WriteHeader is, in the specific case of 1xx status codes, a direct call to the wrapped ResponseWriter, without marking headers as sent, // WriteHeader is, in the specific case of 1xx status codes, a direct call to the wrapped ResponseWriter, without marking headers as sent,
// allowing so further calls. // allowing so further calls.
func (r *responseModifier) WriteHeader(code int) { func (r *ResponseModifier) WriteHeader(code int) {
if r.headersSent { if r.headersSent {
return return
} }
@ -73,11 +75,11 @@ func (r *responseModifier) WriteHeader(code int) {
r.rw.WriteHeader(code) r.rw.WriteHeader(code)
} }
func (r *responseModifier) Header() http.Header { func (r *ResponseModifier) Header() http.Header {
return r.rw.Header() return r.rw.Header()
} }
func (r *responseModifier) Write(b []byte) (int, error) { func (r *ResponseModifier) Write(b []byte) (int, error) {
r.WriteHeader(r.code) r.WriteHeader(r.code)
if r.modifierErr != nil { if r.modifierErr != nil {
return 0, r.modifierErr return 0, r.modifierErr
@ -87,7 +89,7 @@ func (r *responseModifier) Write(b []byte) (int, error) {
} }
// Hijack hijacks the connection. // Hijack hijacks the connection.
func (r *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (r *ResponseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := r.rw.(http.Hijacker); ok { if h, ok := r.rw.(http.Hijacker); ok {
return h.Hijack() return h.Hijack()
} }
@ -96,7 +98,7 @@ func (r *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
} }
// Flush sends any buffered data to the client. // Flush sends any buffered data to the client.
func (r *responseModifier) Flush() { func (r *ResponseModifier) Flush() {
if flusher, ok := r.rw.(http.Flusher); ok { if flusher, ok := r.rw.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
} }

View file

@ -728,6 +728,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
AuthResponseHeaders: auth.AuthResponseHeaders, AuthResponseHeaders: auth.AuthResponseHeaders,
AuthResponseHeadersRegex: auth.AuthResponseHeadersRegex, AuthResponseHeadersRegex: auth.AuthResponseHeadersRegex,
AuthRequestHeaders: auth.AuthRequestHeaders, AuthRequestHeaders: auth.AuthRequestHeaders,
AddAuthCookiesToResponse: auth.AddAuthCookiesToResponse,
} }
if auth.TLS == nil { if auth.TLS == nil {

View file

@ -157,6 +157,8 @@ type ForwardAuth struct {
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"` AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
// TLS defines the configuration used to secure the connection to the authentication server. // TLS defines the configuration used to secure the connection to the authentication server.
TLS *ClientTLS `json:"tls,omitempty"` 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"`
} }
// ClientTLS holds the client TLS configuration. // ClientTLS holds the client TLS configuration.

View file

@ -215,6 +215,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = new(ClientTLS) *out = new(ClientTLS)
**out = **in **out = **in
} }
if in.AddAuthCookiesToResponse != nil {
in, out := &in.AddAuthCookiesToResponse, &out.AddAuthCookiesToResponse
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }