diff --git a/cmd/oidcgate/success.go b/cmd/oidcgate/success.go new file mode 100644 index 0000000..4b78cec --- /dev/null +++ b/cmd/oidcgate/success.go @@ -0,0 +1,43 @@ +package main + +import ( + "net/http" + "strings" +) + +// mirrorAllowedHeaders is the set of NON-X-prefixed request headers that the +// success handler copies onto the response. The traefikoidc middleware sets +// "Authorization: Bearer ..." via the templated-header feature when operators +// configure it, and proxies need that to flow upstream. +var mirrorAllowedHeaders = map[string]struct{}{ + "Authorization": {}, +} + +// successHandler is the http.Handler installed as the middleware's `next`. +// When the middleware reaches this handler the request is authenticated; we +// mirror the X-* (and a small allow-list of non-X-*) headers the middleware +// stamped onto req.Header back onto the response so upstream proxies can +// capture them via auth_request_set / authResponseHeaders / copy_headers, +// then write 200 with an empty body. +func newSuccessHandler() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + for name, values := range req.Header { + if !shouldMirror(name) { + continue + } + for _, v := range values { + rw.Header().Add(name, v) + } + } + rw.WriteHeader(http.StatusOK) + }) +} + +func shouldMirror(name string) bool { + if strings.HasPrefix(name, "X-") || strings.HasPrefix(name, "x-") { + return true + } + canonical := http.CanonicalHeaderKey(name) + _, ok := mirrorAllowedHeaders[canonical] + return ok +} diff --git a/cmd/oidcgate/success_test.go b/cmd/oidcgate/success_test.go new file mode 100644 index 0000000..30bbe43 --- /dev/null +++ b/cmd/oidcgate/success_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestSuccessHandler_Writes200(t *testing.T) { + h := newSuccessHandler() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/x", nil) + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status: want 200, got %d", rec.Code) + } +} + +func TestSuccessHandler_MirrorsForwardedHeaders(t *testing.T) { + h := newSuccessHandler() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/x", nil) + req.Header.Set("X-Forwarded-User", "alice@example.com") + req.Header.Set("X-Forwarded-Email", "alice@example.com") + req.Header.Set("X-Custom-Templated", "value") + req.Header.Set("Authorization", "Bearer token-from-template") + req.Header.Set("Cookie", "session=should-NOT-mirror") + + h.ServeHTTP(rec, req) + + if got := rec.Header().Get("X-Forwarded-User"); got != "alice@example.com" { + t.Errorf("X-Forwarded-User: want mirrored, got %q", got) + } + if got := rec.Header().Get("X-Forwarded-Email"); got != "alice@example.com" { + t.Errorf("X-Forwarded-Email: want mirrored, got %q", got) + } + if got := rec.Header().Get("X-Custom-Templated"); got != "value" { + t.Errorf("X-Custom-Templated: want mirrored (X- prefix), got %q", got) + } + if got := rec.Header().Get("Authorization"); got != "Bearer token-from-template" { + t.Errorf("Authorization: want mirrored (templated bearer), got %q", got) + } + if got := rec.Header().Get("Cookie"); got != "" { + t.Errorf("Cookie must NOT be mirrored, got %q", got) + } +} + +func TestSuccessHandler_EmptyBody(t *testing.T) { + h := newSuccessHandler() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/x", nil) + h.ServeHTTP(rec, req) + if body := strings.TrimSpace(rec.Body.String()); body != "" { + t.Fatalf("body: want empty, got %q", body) + } +}