feat(oidcgate): synthetic success handler mirrors X-* headers to response

This commit is contained in:
2026-05-19 13:41:51 +01:00
parent 8f458b4f6e
commit 0c092a5a22
2 changed files with 100 additions and 0 deletions
+43
View File
@@ -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
}
+57
View File
@@ -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", "[email protected]")
req.Header.Set("X-Forwarded-Email", "[email protected]")
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 != "[email protected]" {
t.Errorf("X-Forwarded-User: want mirrored, got %q", got)
}
if got := rec.Header().Get("X-Forwarded-Email"); got != "[email protected]" {
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)
}
}