mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-01 19:36:32 +02:00
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
153 lines
5.9 KiB
Go
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
|
|
})
|
|
}
|