traefik/pkg/middlewares/auth/forward_test.go

529 lines
16 KiB
Go
Raw Normal View History

package auth
import (
2018-11-14 09:18:03 +00:00
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
2024-01-08 08:10:06 +00:00
"github.com/containous/alice"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/config/dynamic"
2024-01-08 08:10:06 +00:00
"github.com/traefik/traefik/v3/pkg/config/static"
2023-02-03 14:24:05 +00:00
tracingMiddleware "github.com/traefik/traefik/v3/pkg/middlewares/tracing"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tracing"
2024-01-08 08:10:06 +00:00
"github.com/traefik/traefik/v3/pkg/tracing/opentelemetry"
"github.com/traefik/traefik/v3/pkg/version"
2022-11-21 17:36:05 +00:00
"github.com/vulcand/oxy/v2/forward"
2024-01-08 08:10:06 +00:00
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func TestForwardAuthFail(t *testing.T) {
2018-11-14 09:18:03 +00:00
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(forward.ProxyAuthenticate, "test")
http.Error(w, "Forbidden", http.StatusForbidden)
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(server.Close)
middleware, err := NewForward(context.Background(), next, dynamic.ForwardAuth{
2018-11-14 09:18:03 +00:00
Address: server.URL,
}, "authTest")
require.NoError(t, err)
2018-11-14 09:18:03 +00:00
ts := httptest.NewServer(middleware)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
2018-06-30 05:54:03 +00:00
res, err := http.DefaultClient.Do(req)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
2021-03-04 19:08:03 +00:00
body, err := io.ReadAll(res.Body)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
err = res.Body.Close()
require.NoError(t, err)
assert.Equal(t, "test", res.Header.Get(forward.ProxyAuthenticate))
2018-11-14 09:18:03 +00:00
assert.Equal(t, "Forbidden\n", string(body))
}
func TestForwardAuthSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2018-06-30 05:54:03 +00:00
w.Header().Set("X-Auth-User", "user@example.com")
w.Header().Set("X-Auth-Secret", "secret")
w.Header().Add("X-Auth-Group", "group1")
w.Header().Add("X-Auth-Group", "group2")
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")
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(server.Close)
2018-11-14 09:18:03 +00:00
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2018-06-30 05:54:03 +00:00
assert.Equal(t, "user@example.com", r.Header.Get("X-Auth-User"))
assert.Empty(t, r.Header.Get("X-Auth-Secret"))
assert.Equal(t, []string{"group1", "group2"}, r.Header["X-Auth-Group"])
assert.Equal(t, "auth-value", r.Header.Get("Foo-Bar"))
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")
})
2018-11-14 09:18:03 +00:00
auth := dynamic.ForwardAuth{
Address: server.URL,
AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Group"},
AuthResponseHeadersRegex: "^Foo-",
AddAuthCookiesToResponse: []string{"authCookie"},
2018-11-14 09:18:03 +00:00
}
middleware, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
ts := httptest.NewServer(middleware)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
req.Header.Set("X-Auth-Group", "admin_group")
req.Header.Set("Foo-Bar", "client-value")
req.Header.Set("Foo-Baz", "client-value")
2018-06-30 05:54:03 +00:00
res, err := http.DefaultClient.Do(req)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
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"])
2021-03-04 19:08:03 +00:00
body, err := io.ReadAll(res.Body)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
err = res.Body.Close()
require.NoError(t, err)
assert.Equal(t, "traefik\n", string(body))
}
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)
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(authTs.Close)
2018-11-14 09:18:03 +00:00
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
2018-11-14 09:18:03 +00:00
2020-08-17 10:02:03 +00:00
auth := dynamic.ForwardAuth{Address: authTs.URL}
2018-11-14 09:18:03 +00:00
authMiddleware, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
ts := httptest.NewServer(authMiddleware)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
client := &http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
2018-11-14 09:18:03 +00:00
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
2018-11-14 09:18:03 +00:00
res, err := client.Do(req)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
assert.Equal(t, http.StatusFound, res.StatusCode)
location, err := res.Location()
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
assert.Equal(t, "http://example.com/redirect-test", location.String())
2021-03-04 19:08:03 +00:00
body, err := io.ReadAll(res.Body)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
err = res.Body.Close()
require.NoError(t, err)
assert.NotEmpty(t, string(body))
}
func TestForwardAuthRemoveHopByHopHeaders(t *testing.T) {
authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
for _, header := range hopHeaders {
if header == forward.TransferEncoding {
2020-08-17 10:02:03 +00:00
headers.Set(header, "chunked")
} else {
headers.Add(header, "test")
}
}
http.Redirect(w, r, "http://example.com/redirect-test", http.StatusFound)
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(authTs.Close)
2018-11-14 09:18:03 +00:00
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
2018-11-14 09:18:03 +00:00
2020-08-17 10:02:03 +00:00
auth := dynamic.ForwardAuth{Address: authTs.URL}
authMiddleware, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
2018-11-14 09:18:03 +00:00
ts := httptest.NewServer(authMiddleware)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
client := &http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
2020-08-17 10:02:03 +00:00
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := client.Do(req)
2020-08-17 10:02:03 +00:00
require.NoError(t, err)
assert.Equal(t, http.StatusFound, res.StatusCode, "they should be equal")
for _, header := range forward.HopHeaders {
assert.Equal(t, "", res.Header.Get(header), "hop-by-hop header '%s' mustn't be set", header)
}
location, err := res.Location()
2020-08-17 10:02:03 +00:00
require.NoError(t, err)
assert.Equal(t, "http://example.com/redirect-test", location.String(), "they should be equal")
2021-03-04 19:08:03 +00:00
body, err := io.ReadAll(res.Body)
2020-08-17 10:02:03 +00:00
require.NoError(t, err)
assert.NotEmpty(t, string(body), "there should be something in the body")
}
func TestForwardAuthFailResponseHeaders(t *testing.T) {
authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{Name: "example", Value: "testing", Path: "/"}
http.SetCookie(w, cookie)
w.Header().Add("X-Foo", "bar")
http.Error(w, "Forbidden", http.StatusForbidden)
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(authTs.Close)
2018-11-14 09:18:03 +00:00
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
2018-11-14 09:18:03 +00:00
auth := dynamic.ForwardAuth{
2018-11-14 09:18:03 +00:00
Address: authTs.URL,
}
authMiddleware, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
ts := httptest.NewServer(authMiddleware)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
2018-11-14 09:18:03 +00:00
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.Len(t, res.Cookies(), 1)
for _, cookie := range res.Cookies() {
2018-11-14 09:18:03 +00:00
assert.Equal(t, "testing", cookie.Value)
}
expectedHeaders := http.Header{
"Content-Length": []string{"10"},
"Content-Type": []string{"text/plain; charset=utf-8"},
"X-Foo": []string{"bar"},
"Set-Cookie": []string{"example=testing; Path=/"},
"X-Content-Type-Options": []string{"nosniff"},
}
assert.Len(t, res.Header, 6)
for key, value := range expectedHeaders {
assert.Equal(t, value, res.Header[key])
}
2021-03-04 19:08:03 +00:00
body, err := io.ReadAll(res.Body)
2018-11-14 09:18:03 +00:00
require.NoError(t, err)
err = res.Body.Close()
require.NoError(t, err)
assert.Equal(t, "Forbidden\n", string(body))
}
func Test_writeHeader(t *testing.T) {
testCases := []struct {
2018-09-25 13:06:03 +00:00
name string
headers map[string]string
2020-10-07 14:36:04 +00:00
authRequestHeaders []string
2018-09-25 13:06:03 +00:00
trustForwardHeader bool
emptyHost bool
expectedHeaders map[string]string
checkForUnexpectedHeaders bool
}{
{
name: "trust Forward Header",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
trustForwardHeader: true,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
},
{
name: "not trust Forward Header",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "foo.bar",
},
},
{
name: "trust Forward Header with empty Host",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
trustForwardHeader: true,
emptyHost: true,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
},
{
name: "not trust Forward Header with empty Host",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
},
trustForwardHeader: false,
emptyHost: true,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "",
},
},
{
name: "trust Forward Header with forwarded URI",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
"X-Forwarded-Uri": "/forward?q=1",
},
trustForwardHeader: true,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
"X-Forwarded-Uri": "/forward?q=1",
},
},
{
name: "not trust Forward Header with forward requested URI",
headers: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "fii.bir",
"X-Forwarded-Uri": "/forward?q=1",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"Accept": "application/json",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
},
2020-07-07 12:42:03 +00:00
},
{
2018-06-13 13:14:03 +00:00
name: "trust Forward Header with forwarded request Method",
headers: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
trustForwardHeader: true,
expectedHeaders: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
},
{
name: "not trust Forward Header with forward request Method",
headers: map[string]string{
"X-Forwarded-Method": "OPTIONS",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"X-Forwarded-Method": "GET",
},
},
2018-09-25 13:06:03 +00:00
{
name: "remove hop-by-hop headers",
headers: map[string]string{
forward.Connection: "Connection",
forward.KeepAlive: "KeepAlive",
forward.ProxyAuthenticate: "ProxyAuthenticate",
forward.ProxyAuthorization: "ProxyAuthorization",
forward.Te: "Te",
forward.Trailers: "Trailers",
forward.TransferEncoding: "TransferEncoding",
forward.Upgrade: "Upgrade",
"X-CustomHeader": "CustomHeader",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
forward.ProxyAuthenticate: "ProxyAuthenticate",
forward.ProxyAuthorization: "ProxyAuthorization",
2018-09-25 13:06:03 +00:00
},
checkForUnexpectedHeaders: true,
},
2020-10-07 14:36:04 +00:00
{
name: "filter forward request headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
},
authRequestHeaders: []string{
"X-CustomHeader",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"x-customHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
},
checkForUnexpectedHeaders: true,
},
{
name: "filter forward request headers doesn't add new headers",
headers: map[string]string{
"X-CustomHeader": "CustomHeader",
"Content-Type": "multipart/form-data; boundary=---123456",
},
authRequestHeaders: []string{
"X-CustomHeader",
"X-Non-Exists-Header",
},
trustForwardHeader: false,
expectedHeaders: map[string]string{
"X-CustomHeader": "CustomHeader",
"X-Forwarded-Proto": "http",
"X-Forwarded-Host": "foo.bar",
"X-Forwarded-Uri": "/path?q=1",
"X-Forwarded-Method": "GET",
},
checkForUnexpectedHeaders: true,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
for key, value := range test.headers {
req.Header.Set(key, value)
}
if test.emptyHost {
req.Host = ""
}
forwardReq := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/path?q=1", nil)
2020-10-07 14:36:04 +00:00
writeHeader(req, forwardReq, test.trustForwardHeader, test.authRequestHeaders)
2018-09-25 13:06:03 +00:00
actualHeaders := forwardReq.Header
2020-10-07 14:36:04 +00:00
2018-09-25 13:06:03 +00:00
expectedHeaders := test.expectedHeaders
for key, value := range expectedHeaders {
assert.Equal(t, value, actualHeaders.Get(key))
actualHeaders.Del(key)
}
if test.checkForUnexpectedHeaders {
for key := range actualHeaders {
assert.Fail(t, "Unexpected header found", key)
}
}
})
}
}
func TestForwardAuthUsesTracing(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2024-01-08 08:10:06 +00:00
if r.Header.Get("Traceparent") == "" {
t.Errorf("expected Traceparent header to be present in request")
}
}))
2020-08-17 10:02:03 +00:00
t.Cleanup(server.Close)
next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
auth := dynamic.ForwardAuth{
Address: server.URL,
}
2024-01-08 08:10:06 +00:00
exporter := tracetest.NewInMemoryExporter()
2024-01-08 08:10:06 +00:00
tres, err := resource.New(context.Background(),
resource.WithAttributes(semconv.ServiceNameKey.String("traefik")),
resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version)),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
)
require.NoError(t, err)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(tres),
sdktrace.WithBatcher(exporter),
)
otel.SetTracerProvider(tracerProvider)
config := &static.Tracing{
ServiceName: "testApp",
SampleRate: 1,
OTLP: &opentelemetry.Config{
HTTP: &opentelemetry.HTTP{
Endpoint: "http://127.0.0.1:8080",
},
},
}
tr, closer, err := tracing.NewTracing(config)
require.NoError(t, err)
t.Cleanup(func() {
_ = closer.Close()
})
2024-01-08 08:10:06 +00:00
next, err = NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
2024-01-08 08:10:06 +00:00
chain := alice.New(tracingMiddleware.WrapEntryPointHandler(context.Background(), tr, "tracingTest"))
next, err = chain.Then(next)
require.NoError(t, err)
ts := httptest.NewServer(next)
2020-08-17 10:02:03 +00:00
t.Cleanup(ts.Close)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}