From 3826f8e62e152dd1c50c3790539a75ec373e707b Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 4 Oct 2026 23:16:19 +0300 Subject: [PATCH 1/4] refactor: allow for multiple auth modules if they match --- internal/controller/proxy_controller.go | 62 +++++++------------- internal/controller/proxy_controller_test.go | 37 +++++++++++- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 7349a2ca..06ca9730 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,7 +7,9 @@ import ( "net/http" "net/url" "path" + "reflect" "regexp" + "slices" "strings" "github.com/tinyauthapp/tinyauth/internal/model" @@ -26,6 +28,7 @@ const ( AuthRequest AuthModuleType = iota ExtAuthz ForwardAuth + AuthModuleUnknown ) type ProxyType int @@ -529,37 +532,10 @@ func (controller *ProxyController) getContextFromAuthModule(c *gin.Context, modu return ProxyContext{}, fmt.Errorf("unsupported auth module: %v", module) } -func (controller *ProxyController) authModuleIdentifiersPresent(c *gin.Context, module AuthModuleType) bool { - switch module { - case ForwardAuth: - _, host := controller.getHeader(c, "x-forwarded-host") - _, uri := controller.getHeader(c, "x-forwarded-uri") - return host || uri - case AuthRequest: - _, ok := controller.getHeader(c, "x-original-url") - return ok - case ExtAuthz: - return strings.TrimSpace(c.Query("path")) != "" - default: - return false - } -} - -func (controller *ProxyController) ensureNoMultipleAuthModules(c *gin.Context, authModules []AuthModuleType) error { - present := 0 - - for _, module := range authModules { - if controller.authModuleIdentifiersPresent(c, module) { - present++ - } - } - - if present > 1 { - controller.log.App.Warn().Msg("Request carries headers for multiple auth modules, possible spoofing attempt, denying") - return fmt.Errorf("conflicting auth module headers") - } - - return nil +func (controller *ProxyController) compareProxyContext(ctx1, ctx2 ProxyContext) bool { + ctx1.Type = AuthModuleUnknown + ctx1.Type = AuthModuleUnknown + return reflect.DeepEqual(ctx1, ctx2) } func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext, error) { @@ -584,13 +560,7 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy) } - err = controller.ensureNoMultipleAuthModules(c, controller.determineAuthModules(proxy, true)) - - if err != nil { - return ProxyContext{}, err - } - - var ctx *ProxyContext + var ctxSlice []ProxyContext for _, module := range authModules { controller.log.App.Debug().Msgf("Trying to get context from auth module %v", module) @@ -600,14 +570,22 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext continue } controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module) - ctx = &authModuleCtx - break + ctxSlice = append(ctxSlice, authModuleCtx) } - if ctx == nil { + if len(ctxSlice) == 0 { return ProxyContext{}, fmt.Errorf("failed to get context from any auth module") } + if len(ctxSlice) > 1 { + if len(slices.CompactFunc(ctxSlice, controller.compareProxyContext)) > 1 { + controller.log.App.Warn().Msg("Request carries headers for multiple auth modules, possible spoofing attempt, denying") + return ProxyContext{}, fmt.Errorf("conflicting auth module headers") + } + } + + ctx := ctxSlice[0] + // Parse the raw path to populate the cleaned path used for ACLs upath, err := url.Parse(ctx.PathRaw) @@ -633,5 +611,5 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext ctx.IsBrowser = isBrowser ctx.ProxyType = proxy - return *ctx, nil + return ctx, nil } diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index fd06ae39..8bcecc0e 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -881,7 +881,7 @@ func TestProxyController(t *testing.T) { description: "Forward auth and auth request headers should fail for nginx", run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) - req.Header.Set("x-forwarded-host", "foo.example.com") + req.Header.Set("x-forwarded-host", "foobar.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/foo?bar=foo") req.Header.Set("x-original-url", "https://foo.example.com/foo?bar=foo") @@ -895,9 +895,44 @@ func TestProxyController(t *testing.T) { run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) req.Host = "foo.example.com" + req.Header.Set("x-forwarded-host", "foobar.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/foo?bar=foo") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + description: "Forward auth and auth request headers should succeed for nginx if they match", + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-forwarded-host", "foo.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/foo?bar=foo") + req.Header.Set("x-original-url", "https://foo.example.com/foo?bar=foo") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + description: "Forward auth and ext authz headers should succeed for envoy of they match", + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/foo?bar=foo", nil) + req.Host = "foo.example.com" + req.Header.Set("x-forwarded-host", "foo.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/foo?bar=foo") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + description: "Proxy without any modules matching should fail", + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) router.ServeHTTP(recorder, req) assert.Equal(t, http.StatusBadRequest, recorder.Code) From 15f089fb17a316a24942726e5932ec3702c7a868 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 5 Oct 2026 16:36:35 +0300 Subject: [PATCH 2/4] fix: rabbit comments --- internal/controller/proxy_controller.go | 2 +- internal/controller/proxy_controller_test.go | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 06ca9730..2d5f211b 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -534,7 +534,7 @@ func (controller *ProxyController) getContextFromAuthModule(c *gin.Context, modu func (controller *ProxyController) compareProxyContext(ctx1, ctx2 ProxyContext) bool { ctx1.Type = AuthModuleUnknown - ctx1.Type = AuthModuleUnknown + ctx2.Type = AuthModuleUnknown return reflect.DeepEqual(ctx1, ctx2) } diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index 8bcecc0e..3fdc4332 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -878,7 +878,7 @@ func TestProxyController(t *testing.T) { }, }, { - description: "Forward auth and auth request headers should fail for nginx", + description: "Forward auth and different auth request headers should fail for nginx", run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-forwarded-host", "foobar.example.com") @@ -891,7 +891,7 @@ func TestProxyController(t *testing.T) { }, }, { - description: "Forward auth and ext authz headers should fail for envoy", + description: "Forward auth and different ext authz headers should fail for envoy", run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) req.Host = "foo.example.com" @@ -904,7 +904,10 @@ func TestProxyController(t *testing.T) { }, }, { - description: "Forward auth and auth request headers should succeed for nginx if they match", + description: "Forward auth and same auth request headers should succeed for nginx", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-forwarded-host", "foo.example.com") @@ -913,11 +916,14 @@ func TestProxyController(t *testing.T) { req.Header.Set("x-original-url", "https://foo.example.com/foo?bar=foo") router.ServeHTTP(recorder, req) - assert.Equal(t, http.StatusBadRequest, recorder.Code) + assert.Equal(t, http.StatusOK, recorder.Code) }, }, { description: "Forward auth and ext authz headers should succeed for envoy of they match", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/foo?bar=foo", nil) req.Host = "foo.example.com" @@ -926,7 +932,7 @@ func TestProxyController(t *testing.T) { req.Header.Set("x-forwarded-uri", "/foo?bar=foo") router.ServeHTTP(recorder, req) - assert.Equal(t, http.StatusBadRequest, recorder.Code) + assert.Equal(t, http.StatusOK, recorder.Code) }, }, { From 1e4a24d8e3db58f29c96108c20679484be922229 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 5 Oct 2026 19:17:27 +0300 Subject: [PATCH 3/4] fix: drop envoy auth module when path query doesn't exist --- internal/controller/proxy_controller.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 4ab452d0..62a06efc 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -469,6 +469,10 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont } // The path is attached to the end of the /api/auth/envoy?path= string so we just strip it out + if !strings.HasPrefix(c.Request.RequestURI, "/api/auth/envoy?path=") { + return ProxyContext{}, errors.New("path not found") + } + path := strings.TrimPrefix(c.Request.RequestURI, "/api/auth/envoy?path=") if strings.TrimSpace(path) == "" { From 75551fb4de4ca32a348d6dec436144966a519919 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 7 Oct 2026 22:36:28 +0300 Subject: [PATCH 4/4] fix: do not fallback to user-provided headers --- internal/controller/proxy_controller.go | 75 +++++++++++++++++-------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 62a06efc..deb887bb 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "path" - "reflect" "regexp" "slices" "strings" @@ -28,7 +27,6 @@ const ( AuthRequest AuthModuleType = iota ExtAuthz ForwardAuth - AuthModuleUnknown ) type ProxyType int @@ -42,6 +40,8 @@ const ( var BrowserUserAgentRegex = regexp.MustCompile("Chrome|Gecko|AppleWebKit|Opera|Edge") +var envoyAuthPath = "/api/auth/envoy?path=" + type Proxy struct { Proxy string `uri:"proxy" binding:"required"` } @@ -424,19 +424,19 @@ func (controller *ProxyController) getAuthRequestContext(c *gin.Context) (ProxyC return ProxyContext{}, errors.New("x-original-url not found") } - url, err := url.Parse(xOriginalUrl) + u, err := url.Parse(xOriginalUrl) if err != nil { return ProxyContext{}, err } - host := url.Host + host := u.Host if strings.TrimSpace(host) == "" { return ProxyContext{}, errors.New("host not found") } - proto := url.Scheme + proto := u.Scheme if strings.TrimSpace(proto) == "" { return ProxyContext{}, errors.New("proto not found") @@ -447,7 +447,7 @@ func (controller *ProxyController) getAuthRequestContext(c *gin.Context) (ProxyC return ProxyContext{ Host: host, Proto: proto, - PathRaw: url.RequestURI(), + PathRaw: u.RequestURI(), Method: method, Type: AuthRequest, }, nil @@ -469,23 +469,23 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont } // The path is attached to the end of the /api/auth/envoy?path= string so we just strip it out - if !strings.HasPrefix(c.Request.RequestURI, "/api/auth/envoy?path=") { + if !strings.HasPrefix(c.Request.RequestURI, envoyAuthPath) { return ProxyContext{}, errors.New("path not found") } - path := strings.TrimPrefix(c.Request.RequestURI, "/api/auth/envoy?path=") + p := strings.TrimPrefix(c.Request.RequestURI, envoyAuthPath) - if strings.TrimSpace(path) == "" { + if strings.TrimSpace(p) == "" { return ProxyContext{}, errors.New("path not found") } - // For envoy we need to support every method + // For ext_authz we need to support every method method := c.Request.Method return ProxyContext{ Host: host, Proto: proto, - PathRaw: path, + PathRaw: p, Method: method, Type: ExtAuthz, }, nil @@ -537,9 +537,31 @@ func (controller *ProxyController) getContextFromAuthModule(c *gin.Context, modu } func (controller *ProxyController) compareProxyContext(ctx1, ctx2 ProxyContext) bool { - ctx1.Type = AuthModuleUnknown - ctx2.Type = AuthModuleUnknown - return reflect.DeepEqual(ctx1, ctx2) + return ctx1.Host == ctx2.Host && ctx1.Proto == ctx2.Proto && ctx1.PathRaw == ctx2.PathRaw && ctx1.Method == ctx2.Method +} + +func (controller *ProxyController) includedAuthModules(c *gin.Context) []AuthModuleType { + var modules []AuthModuleType + + if strings.HasPrefix(c.Request.RequestURI, envoyAuthPath) && + strings.TrimPrefix(c.Request.RequestURI, envoyAuthPath) != "" { + modules = append(modules, ExtAuthz) + } + + hasURI := c.GetHeader("x-forwarded-uri") != "" + hasHost := c.GetHeader("x-forwarded-host") != "" + + if hasURI && hasHost { + modules = append(modules, ForwardAuth) + } + + hasXOriginalUrl := c.GetHeader("x-original-url") != "" + + if hasXOriginalUrl { + modules = append(modules, AuthRequest) + } + + return modules } func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext, error) { @@ -564,7 +586,7 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy) } - var ctxSlice []ProxyContext + var extracted []ProxyContext for _, module := range authModules { controller.log.App.Debug().Msgf("Trying to get context from auth module %v", module) @@ -574,21 +596,28 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext continue } controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module) - ctxSlice = append(ctxSlice, authModuleCtx) + extracted = append(extracted, authModuleCtx) } - if len(ctxSlice) == 0 { + if len(extracted) == 0 { return ProxyContext{}, fmt.Errorf("failed to get context from any auth module") } - if len(ctxSlice) > 1 { - if len(slices.CompactFunc(ctxSlice, controller.compareProxyContext)) > 1 { - controller.log.App.Warn().Msg("Request carries headers for multiple auth modules, possible spoofing attempt, denying") - return ProxyContext{}, fmt.Errorf("conflicting auth module headers") - } + includedAuthModules := controller.includedAuthModules(c) + + if len(extracted) != len(includedAuthModules) { + controller.log.App.Warn(). + Msg("Request carries context for multiple auth modules but some failed to extract, cannot determine correct auth modules") + return ProxyContext{}, fmt.Errorf("cannot determine correct auth module") + } + + if s := slices.CompactFunc(extracted, controller.compareProxyContext); len(s) > 1 { + controller.log.App.Warn(). + Msg("Request carries context for multiple auth modules but they don't match, cannot determine correct auth modules") + return ProxyContext{}, fmt.Errorf("cannot determine correct auth module") } - ctx := ctxSlice[0] + ctx := extracted[0] // Parse the raw path to populate the cleaned path used for ACLs upath, err := url.Parse(ctx.PathRaw)