pomerium/proxy/forward_auth.go
Cuong Manh Le 846d709ba4 proxy: fix wrong forward auth request
When proxy receives forward auth request, it should forward the request
as-is to authorize for verification. Currently, it composes the check
request with actual path, then send the request to authorize service.

It makes the request works accidently, because the composed check
request will satisfy the policy un-intentionally. Example, for forward
auth request:

	http://pomerium/?uri=https://httpbin.localhost.pomerium.io

the composed request will look like:

	&envoy_service_auth_v2.AttributeContext_HttpRequest{
		Method:   "GET",
		Headers:  map[string]string{},
		Path:     "",
		Host:     "httpbin.localhost.pomerium.io",
		Scheme:   "https",
	}

This check request has at least two problems.

First, it will make authorize.handleForwardAuth always returns false,
even though this is a real forward auth request. Because the "Host"
field in check request is not the forward auth host, which is "pomerium"
in this case.

Second, it will accidently matches rule like:

	policy:
	  - from: https://httpbin.localhost.pomerium.io
	    to: https://httpbin
	    allowed_domains:
	      - pomerium.io

If the rule contains other conditions, like "prefix", or "regex":

	policy:
	  - from: https://httpbin.localhost.pomerium.io
	    prefix: /headers
	    to: https://httpbin
	    allowed_domains:
	      - pomerium.io

Then the rule will never be triggered, because the "/headers" path can
be passed in request via "X-Forwarded-Uri" (traefik), instead of
directly from the path (nginx).

To fix this, we just pass the forward auth request as-is to authorize.

Fixes #873
2020-07-03 00:00:20 +07:00

153 lines
5.9 KiB
Go

package proxy
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/urlutil"
)
// registerFwdAuthHandlers returns a set of handlers that support using pomerium
// as a "forward-auth" provider with other reverse proxies like nginx, traefik.
//
// see : https://www.pomerium.io/configuration/#forward-auth
func (p *Proxy) registerFwdAuthHandlers() http.Handler {
r := httputil.NewRouter()
r.StrictSlash(true)
r.Use(sessions.RetrieveSession(p.sessionStore))
r.Use(p.jwtClaimMiddleware(true))
// NGNIX's forward-auth capabilities are split across two settings:
// `auth-url` and `auth-signin` which correspond to `verify` and `auth-url`
//
// NOTE: Route order matters here which makes the request flow confusing
// to reason about so each step has a postfix order step.
// nginx 3: save the returned session post authenticate flow
r.Handle("/verify", httputil.HandlerFunc(p.nginxCallback)).
Queries("uri", "{uri}", urlutil.QuerySessionEncrypted, "", urlutil.QueryRedirectURI, "")
// nginx 1: verify. Return 401 if invalid and NGINX will call `auth-signin`
r.Handle("/verify", p.Verify(true)).Queries("uri", "{uri}")
// nginx 4: redirect the user back to their originally requested location.
r.Handle("/", httputil.HandlerFunc(p.nginxPostCallbackRedirect)).
Queries("uri", "{uri}", urlutil.QuerySessionEncrypted, "", urlutil.QueryRedirectURI, "")
// traefik 2: save the returned session post authenticate flow
r.Handle("/", httputil.HandlerFunc(p.forwardedURIHeaderCallback)).
HeadersRegexp(httputil.HeaderForwardedURI, urlutil.QuerySessionEncrypted)
// nginx 2 / traefik 1: verify and then start authenticate flow
r.Handle("/", p.Verify(false))
return r
}
// nginxPostCallbackRedirect redirects the user to their original destination
// in order to drop the authenticate related query params
func (p *Proxy) nginxPostCallbackRedirect(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
httputil.Redirect(w, r, r.FormValue(urlutil.QueryRedirectURI), http.StatusFound)
return nil
}
// nginxCallback saves the returned session post callback and then returns an
// unauthorized status in order to restart the request flow process. Strangely
// we need to throw a 401 after saving the session to redirect the user
// to their originally desired location.
func (p *Proxy) nginxCallback(w http.ResponseWriter, r *http.Request) error {
encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
return nil
}
// forwardedURIHeaderCallback handles the post-authentication callback from
// forwarding proxies that support the `X-Forwarded-Uri`.
func (p *Proxy) forwardedURIHeaderCallback(w http.ResponseWriter, r *http.Request) error {
forwardedURL, err := url.Parse(r.Header.Get(httputil.HeaderForwardedURI))
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
q := forwardedURL.Query()
redirectURLString := q.Get(urlutil.QueryRedirectURI)
encryptedSession := q.Get(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
httputil.Redirect(w, r, redirectURLString, http.StatusFound)
return nil
}
// Verify checks a user's credentials for an arbitrary host. If the user
// is properly authenticated and is authorized to access the supplied host,
// a `200` http status code is returned. If the user is not authenticated, they
// will be redirected to the authenticate service to sign in with their identity
// provider. If the user is unauthorized, a `401` error is returned.
func (p *Proxy) Verify(verifyOnly bool) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
var err error
if status := r.FormValue("auth_status"); status == fmt.Sprint(http.StatusForbidden) {
return httputil.NewError(http.StatusForbidden, errors.New(http.StatusText(http.StatusForbidden)))
}
// the route to validate will be pulled from the uri queryparam
// or inferred from forwarding headers
uriString := r.FormValue("uri")
if uriString == "" {
if r.Header.Get(httputil.HeaderForwardedProto) == "" || r.Header.Get(httputil.HeaderForwardedHost) == "" {
return httputil.NewError(http.StatusBadRequest, errors.New("no uri to validate"))
}
uriString = r.Header.Get(httputil.HeaderForwardedProto) + "://" +
r.Header.Get(httputil.HeaderForwardedHost) +
r.Header.Get(httputil.HeaderForwardedURI)
}
uri, err := urlutil.ParseAndValidateURL(uriString)
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
authorized, err := p.isAuthorized(w, r)
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
if authorized {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Access to %s is allowed.", uri.Host)
return nil
}
_, err = sessions.FromContext(r.Context())
hasSession := err == nil
if hasSession {
return httputil.NewError(http.StatusForbidden, errors.New("access denied"))
}
if verifyOnly {
return httputil.NewError(http.StatusUnauthorized, err)
}
// redirect to authenticate
authN := *p.authenticateSigninURL
q := authN.Query()
q.Set(urlutil.QueryCallbackURI, uri.String())
q.Set(urlutil.QueryRedirectURI, uri.String()) // final destination
q.Set(urlutil.QueryForwardAuth, urlutil.StripPort(r.Host)) // add fwd auth to trusted audience
authN.RawQuery = q.Encode()
httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &authN).String(), http.StatusFound)
return nil
})
}