refactor(oidcgate): idempotent Finalize; document and test 307/308 intercept

This commit is contained in:
2026-05-19 13:57:15 +01:00
parent c465fc888b
commit 91966c1bec
2 changed files with 45 additions and 2 deletions
+7 -2
View File
@@ -8,8 +8,8 @@ import "net/http"
// silent-probe contracts cannot follow redirects. authInterceptor buffers
// the header/body and, at Finalize() time:
//
// - if status was 302 or 303 (redirect class we care about), rewrites
// it to 401, moves the original Location header to X-Auth-Redirect
// - if status was a redirect class (302, 303, 307, 308), rewrites it
// to 401, moves the original Location header to X-Auth-Redirect
// (advisory), strips Location, preserves Set-Cookie headers (state,
// PKCE, nonce — the browser will carry them into the next request),
// and writes an empty body.
@@ -20,6 +20,7 @@ type authInterceptor struct {
status int
body []byte
wroteHeader bool
finalized bool
}
func newAuthInterceptor(inner http.ResponseWriter) *authInterceptor {
@@ -51,6 +52,10 @@ func (w *authInterceptor) Write(b []byte) (int, error) { //nolint:unparam // sig
// Finalize flushes the buffered response, applying the 302/303 → 401 rewrite.
// Must be called exactly once after the wrapped handler returns.
func (w *authInterceptor) Finalize() {
if w.finalized {
return
}
w.finalized = true
switch w.status {
case http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
// Move Location → X-Auth-Redirect, strip Location, force 401, drop body.
+38
View File
@@ -68,3 +68,41 @@ func TestInterceptor_303SeeOtherAlsoIntercepted(t *testing.T) {
t.Fatalf("303 should be intercepted to 401, got %d", rec.Code)
}
}
func TestInterceptor_307TemporaryRedirectIntercepted(t *testing.T) {
rec := httptest.NewRecorder()
w := newAuthInterceptor(rec)
w.Header().Set("Location", "/elsewhere")
w.WriteHeader(http.StatusTemporaryRedirect)
w.Finalize()
if rec.Code != http.StatusUnauthorized {
t.Fatalf("307 should be intercepted to 401, got %d", rec.Code)
}
}
func TestInterceptor_308PermanentRedirectIntercepted(t *testing.T) {
rec := httptest.NewRecorder()
w := newAuthInterceptor(rec)
w.Header().Set("Location", "/elsewhere")
w.WriteHeader(http.StatusPermanentRedirect)
w.Finalize()
if rec.Code != http.StatusUnauthorized {
t.Fatalf("308 should be intercepted to 401, got %d", rec.Code)
}
}
func TestInterceptor_DoubleFinalizeIsNoop(t *testing.T) {
rec := httptest.NewRecorder()
w := newAuthInterceptor(rec)
w.Header().Set("X-Forwarded-User", "alice")
w.WriteHeader(http.StatusOK)
w.Finalize()
// Second call must not panic, must not change anything observable.
w.Finalize()
if rec.Code != http.StatusOK {
t.Fatalf("double Finalize must not change status, got %d", rec.Code)
}
if got := rec.Header().Get("X-Forwarded-User"); got != "alice" {
t.Errorf("double Finalize must not duplicate headers, got %q", got)
}
}