From 68ccc7e0b30b3269b03b87608bd759dd0e2bcb59 Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 20 Aug 2026 14:42:02 +0200 Subject: [PATCH] skip session cookie for header auth --- proxy/internal/auth/middleware.go | 10 ++++++ proxy/internal/auth/middleware_test.go | 44 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/proxy/internal/auth/middleware.go b/proxy/internal/auth/middleware.go index bba154c7b..0a7800926 100644 --- a/proxy/internal/auth/middleware.go +++ b/proxy/internal/auth/middleware.go @@ -325,6 +325,16 @@ func (mw *Middleware) forwardWithSessionCookie(w http.ResponseWriter, r *http.Re if err != nil { return false } + + // Header auth is checked per request against the mapping's hashes and mints + // no session, so a header-method token can only predate that. Honouring it + // would keep a rotated credential working until the token expired. + if method == auth.MethodHeader.String() { + mw.logger.WithField("host", host). + Debug("ignoring header-auth session cookie; the header is required on every request") + return false + } + if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil { cd.SetUserID(userID) cd.SetUserEmail(email) diff --git a/proxy/internal/auth/middleware_test.go b/proxy/internal/auth/middleware_test.go index 7d0f318ca..859d4de76 100644 --- a/proxy/internal/auth/middleware_test.go +++ b/proxy/internal/auth/middleware_test.go @@ -1173,6 +1173,50 @@ func TestProtect_HeaderAuth_SubsequentRequestRequiresHeader(t *testing.T) { assert.Equal(t, 1, backendCalls, "backend must not be reached without the header") } +// TestProtect_HeaderAuth_LegacySessionCookieIsIgnored covers the upgrade +// window. Header auth used to mint a session token, so cookies with +// method=header survive a proxy upgrade and stay signature-valid for their full +// lifetime. They must not stand in for the header, or a credential rotated +// right after the upgrade would keep working until every such token expired. +func TestProtect_HeaderAuth_LegacySessionCookieIsIgnored(t *testing.T) { + mw := NewMiddleware(log.StandardLogger(), nil, nil) + kp := generateTestKeyPair(t) + + hdr := newHeaderScheme(t, "X-API-Key", "secret-key") + require.NoError(t, mw.AddDomain("example.com", []Scheme{hdr}, kp.PublicKey, time.Hour, "acc1", "svc1", nil, false)) + + // A token management would have minted for header auth before the upgrade. + legacyToken, err := sessionkey.SignToken(kp.PrivateKey, auth.HeaderUserID, "", "example.com", auth.MethodHeader, nil, nil, time.Hour) + require.NoError(t, err) + + var backendCalls int + handler := mw.Protect(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + backendCalls++ + w.WriteHeader(http.StatusOK) + })) + + t.Run("cookie alone is rejected", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.AddCookie(&http.Cookie{Name: auth.SessionCookieName, Value: legacyToken}) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusUnauthorized, rec.Code, "a header-auth cookie must not authenticate on its own") + assert.Equal(t, 0, backendCalls, "backend must not be reached without the header") + }) + + t.Run("cookie does not block the header path", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.AddCookie(&http.Cookie{Name: auth.SessionCookieName, Value: legacyToken}) + req.Header.Set("X-API-Key", "secret-key") + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "a client sending both must still be admitted by the header") + assert.Equal(t, 1, backendCalls) + }) +} + // TestProtect_HeaderAuth_RepeatedValueIsMemoized verifies the KDF is run once // per distinct accepted value. argon2id is deliberately expensive, so a // credential that repeats on every request must not be re-derived each time.