authorize: honor X-Forwarded-Uri in forward auth mode

Some ingress like traefik set the X-Forwarded-Uri header instead
of passing the actual path in request, we should hornor and use
that header in forward auth mode.

While at it, refactoring the handleForwardAuth to return earlier instead
of nested condition, and add more tests to cover all cases.
This commit is contained in:
Cuong Manh Le 2020-07-01 11:09:59 +07:00
parent e482fef247
commit 48639a48fb
2 changed files with 188 additions and 49 deletions

View file

@ -177,25 +177,33 @@ func (a *Authorize) handleForwardAuth(req *envoy_service_auth_v2.CheckRequest) b
} }
checkURL := getCheckRequestURL(req) checkURL := getCheckRequestURL(req)
if urlutil.StripPort(checkURL.Host) == urlutil.StripPort(opts.GetForwardAuthURL().Host) { if urlutil.StripPort(checkURL.Host) != urlutil.StripPort(opts.GetForwardAuthURL().Host) {
if (checkURL.Path == "/" || checkURL.Path == "/verify") && checkURL.Query().Get("uri") != "" { return false
verifyURL, err := url.Parse(checkURL.Query().Get("uri"))
if err != nil {
log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication")
return false
}
req.Attributes.Request.Http.Scheme = verifyURL.Scheme
req.Attributes.Request.Http.Host = verifyURL.Host
req.Attributes.Request.Http.Path = verifyURL.Path
// envoy sends the query string as part of the path
if verifyURL.RawQuery != "" {
req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery
}
return true
}
} }
return false uriQuery := checkURL.Query().Get("uri")
if (checkURL.Path != "/" && checkURL.Path != "/verify") || uriQuery == "" {
return false
}
verifyURL, err := url.Parse(uriQuery)
if err != nil {
log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication")
return false
}
req.Attributes.Request.Http.Scheme = verifyURL.Scheme
req.Attributes.Request.Http.Host = verifyURL.Host
req.Attributes.Request.Http.Path = verifyURL.Path
if headers := req.GetAttributes().GetRequest().GetHttp().GetHeaders(); headers != nil {
if xfu := headers[http.CanonicalHeaderKey("x-forwarded-uri")]; xfu != "" {
req.Attributes.Request.Http.Path += xfu
}
}
// envoy sends the query string as part of the path
if verifyURL.RawQuery != "" {
req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery
}
return true
} }
func (a *Authorize) getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, sessionState *sessions.State) *evaluator.Request { func (a *Authorize) getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, sessionState *sessions.State) *evaluator.Request {

View file

@ -78,44 +78,175 @@ func Test_getEvaluatorRequest(t *testing.T) {
} }
func Test_handleForwardAuth(t *testing.T) { func Test_handleForwardAuth(t *testing.T) {
checkReq := &envoy_service_auth_v2.CheckRequest{ tests := []struct {
Attributes: &envoy_service_auth_v2.AttributeContext{ name string
Source: &envoy_service_auth_v2.AttributeContext_Peer{ checkReq *envoy_service_auth_v2.CheckRequest
Certificate: url.QueryEscape(certPEM), attrCtxHTTPReq *envoy_service_auth_v2.AttributeContext_HttpRequest
}, forwardAuthURL string
Request: &envoy_service_auth_v2.AttributeContext_Request{ isForwardAuth bool
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{ }{
Method: "GET", {
Path: "/verify?uri=" + url.QueryEscape("https://example.com/some/path?qs=1"), name: "enabled",
Host: "forward-auth.example.com", checkReq: &envoy_service_auth_v2.CheckRequest{
Scheme: "https", Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com/some/path?qs=1"),
Host: "forward-auth.example.com",
Scheme: "https",
},
},
}, },
}, },
attrCtxHTTPReq: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/some/path?qs=1",
Host: "example.com",
Scheme: "https",
},
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: true,
},
{
name: "disabled",
checkReq: nil,
attrCtxHTTPReq: nil,
forwardAuthURL: "",
isForwardAuth: false,
},
{
name: "honor x-forwarded-uri set",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com?q=foo"),
Host: "forward-auth.example.com",
Scheme: "https",
Headers: map[string]string{"X-Forwarded-Uri": "/foo/bar"},
},
},
},
},
attrCtxHTTPReq: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/foo/bar?q=foo",
Host: "example.com",
Scheme: "https",
Headers: map[string]string{"X-Forwarded-Uri": "/foo/bar"},
},
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: true,
},
{
name: "request with invalid forward auth url",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com?q=foo"),
Host: "fake-forward-auth.example.com",
Scheme: "https",
},
},
},
},
attrCtxHTTPReq: nil,
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: false,
},
{
name: "request with invalid path",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/foo?uri=" + url.QueryEscape("https://example.com?q=foo"),
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
attrCtxHTTPReq: nil,
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: false,
},
{
name: "request with empty uri",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=",
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
attrCtxHTTPReq: nil,
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: false,
},
{
name: "request with invalid uri",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri= http://example.com/foo",
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
attrCtxHTTPReq: nil,
forwardAuthURL: "https://forward-auth.example.com",
isForwardAuth: false,
}, },
} }
t.Run("enabled", func(t *testing.T) { for _, tc := range tests {
a := new(Authorize) tc := tc
a.currentOptions.Store(config.Options{ t.Run(tc.name, func(t *testing.T) {
ForwardAuthURL: mustParseURL("https://forward-auth.example.com"), a := new(Authorize)
fau := new(url.URL)
if tc.forwardAuthURL != "" {
fau = mustParseURL(tc.forwardAuthURL)
}
a.currentOptions.Store(config.Options{ForwardAuthURL: fau})
assert.Equal(t, tc.isForwardAuth, a.handleForwardAuth(tc.checkReq))
if tc.attrCtxHTTPReq != nil {
assert.Equal(t, tc.attrCtxHTTPReq, tc.checkReq.Attributes.Request.Http)
}
}) })
isForwardAuth := a.handleForwardAuth(checkReq) }
assert.True(t, isForwardAuth)
assert.Equal(t, &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/some/path?qs=1",
Host: "example.com",
Scheme: "https",
}, checkReq.Attributes.Request.Http)
})
t.Run("disabled", func(t *testing.T) {
a := new(Authorize)
a.currentOptions.Store(config.Options{
ForwardAuthURL: nil,
})
isForwardAuth := a.handleForwardAuth(checkReq)
assert.False(t, isForwardAuth)
})
} }
func mustParseURL(str string) *url.URL { func mustParseURL(str string) *url.URL {