fix: auth module selection (#1089)

This commit is contained in:
Stavros
2026-08-25 17:07:56 +03:00
committed by GitHub
parent be48d712ee
commit 847d8325c7
11 changed files with 250 additions and 99 deletions
+3 -1
View File
@@ -294,7 +294,9 @@ func (controller *OAuthController) getCookieDomain() string {
func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
v := validators.NewDomainValidator(validators.DomainValidatorOptions{
WithPort: true,
WithPort: true,
WithScheme: true,
AllowedSchemes: []string{"https", "http"},
})
_, err := v.SafeHostname(controller.runtime.AppURL)
+72 -16
View File
@@ -57,6 +57,7 @@ type ProxyContext struct {
type ProxyController struct {
log *logger.Logger
runtime *model.RuntimeConfig
config *model.Config
acls *service.AccessControlsService
auth *service.AuthService
policyEngine *service.PolicyEngine
@@ -67,6 +68,7 @@ type ProxyControllerInput struct {
Log *logger.Logger
RuntimeConfig *model.RuntimeConfig
Config *model.Config
RouterGroup *gin.RouterGroup `name:"apiRouterGroup"`
ACLsService *service.AccessControlsService
AuthService *service.AuthService
@@ -77,6 +79,7 @@ func NewProxyController(i ProxyControllerInput) *ProxyController {
controller := &ProxyController{
log: i.Log,
runtime: i.RuntimeConfig,
config: i.Config,
acls: i.ACLsService,
auth: i.AuthService,
policyEngine: i.PolicyEngine,
@@ -465,6 +468,10 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
// We get the path from the query string
path := c.Query("path")
if strings.TrimSpace(path) == "" {
return ProxyContext{}, errors.New("path not found")
}
// For envoy we need to support every method
method := c.Request.Method
@@ -477,14 +484,22 @@ func (controller *ProxyController) getExtAuthzContext(c *gin.Context) (ProxyCont
}, nil
}
func (controller *ProxyController) determineAuthModules(proxy ProxyType) []AuthModuleType {
func (controller *ProxyController) determineAuthModules(proxy ProxyType, fallbacks bool) []AuthModuleType {
switch proxy {
case Traefik, Caddy:
return []AuthModuleType{ForwardAuth}
case Envoy:
return []AuthModuleType{ExtAuthz, ForwardAuth}
authModules := []AuthModuleType{ExtAuthz}
if fallbacks {
authModules = append(authModules, ForwardAuth)
}
return authModules
case Nginx:
return []AuthModuleType{AuthRequest, ForwardAuth}
authModules := []AuthModuleType{AuthRequest}
if fallbacks {
authModules = append(authModules, ForwardAuth)
}
return authModules
default:
return []AuthModuleType{}
}
@@ -514,6 +529,39 @@ 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) getProxyContext(c *gin.Context) (ProxyContext, error) {
var req Proxy
@@ -530,28 +578,36 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
controller.log.App.Debug().Msgf("Determined proxy type: %v", proxy)
authModules := controller.determineAuthModules(proxy)
authModules := controller.determineAuthModules(proxy, !controller.config.Experimental.DisableAuthModuleFallback)
if len(authModules) == 0 {
return ProxyContext{}, fmt.Errorf("no auth modules supported for proxy: %v", req.Proxy)
}
var ctx ProxyContext
for _, module := range authModules {
controller.log.App.Debug().Msgf("Trying to get context from auth module %v", module)
ctx, err = controller.getContextFromAuthModule(c, module)
if err == nil {
controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module)
break
}
controller.log.App.Debug().Msgf("Failed to get context from auth module %v: %v", module, err)
}
err = controller.ensureNoMultipleAuthModules(c, controller.determineAuthModules(proxy, true))
if err != nil {
return ProxyContext{}, err
}
var ctx *ProxyContext
for _, module := range authModules {
controller.log.App.Debug().Msgf("Trying to get context from auth module %v", module)
authModuleCtx, err := controller.getContextFromAuthModule(c, module)
if err != nil {
controller.log.App.Debug().Msgf("Failed to get context from auth module %v: %v", module, err)
continue
}
controller.log.App.Debug().Msgf("Successfully got context from auth module %v", module)
ctx = &authModuleCtx
break
}
if ctx == nil {
return ProxyContext{}, fmt.Errorf("failed to get context from any auth module")
}
// Parse the raw path to populate the cleaned path used for ACLs
upath, err := url.Parse(ctx.PathRaw)
@@ -577,5 +633,5 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext
ctx.IsBrowser = isBrowser
ctx.ProxyType = proxy
return ctx, nil
return *ctx, nil
}
+30 -2
View File
@@ -213,7 +213,7 @@ func TestProxyController(t *testing.T) {
description: "Ensure forward auth fallback for envoy",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req := httptest.NewRequest("HEAD", "/api/auth/envoy", nil)
req.Host = ""
req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
@@ -261,7 +261,7 @@ func TestProxyController(t *testing.T) {
description: "Ensure extauthz with envoy non browser returns json",
middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req := httptest.NewRequest("HEAD", "/api/auth/envoy", nil)
req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/hello")
@@ -877,6 +877,32 @@ func TestProxyController(t *testing.T) {
assert.Equal(t, "bar", recorder.Header().Get("x-foo"))
},
},
{
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-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 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"
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)
},
},
}
store := memory.New()
@@ -892,6 +918,7 @@ func TestProxyController(t *testing.T) {
aclsService := service.NewAccessControlsService(service.AccessControlServiceInput{
Log: log,
Config: &cfg,
Runtime: &runtime,
LabelProvider: nil,
})
@@ -952,6 +979,7 @@ func TestProxyController(t *testing.T) {
NewProxyController(ProxyControllerInput{
Log: log,
RuntimeConfig: &runtime,
Config: &cfg,
RouterGroup: group,
ACLsService: aclsService,
AuthService: authService,