mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-17 19:17:17 +02:00
forward-auth: use envoy's ext_authz check (#1482)
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
155213857e
commit
9b39deabd8
16 changed files with 248 additions and 406 deletions
|
@ -292,9 +292,17 @@ func getCheckRequestURL(req *envoy_service_auth_v2.CheckRequest) *url.URL {
|
||||||
u.Path = path
|
u.Path = path
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.GetHeaders() != nil {
|
// check to make sure this is _not_ a verify endpoint and that forwarding
|
||||||
if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok {
|
// headers are set. If so, infer the true authorization location from thos
|
||||||
u.Scheme = fwdProto
|
if u.Path != "/verify" && h.GetHeaders() != nil {
|
||||||
|
if val, ok := h.GetHeaders()["x-forwarded-proto"]; ok && val != "" {
|
||||||
|
u.Scheme = val
|
||||||
|
}
|
||||||
|
if val, ok := h.GetHeaders()["x-forwarded-host"]; ok && val != "" {
|
||||||
|
u.Host = val
|
||||||
|
}
|
||||||
|
if val, ok := h.GetHeaders()["x-forwarded-uri"]; ok && val != "" && val != "/" {
|
||||||
|
u.Path = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return u
|
return u
|
||||||
|
|
|
@ -93,6 +93,15 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
|
||||||
"name": "example.com",
|
"name": "example.com",
|
||||||
"domains": ["example.com"],
|
"domains": ["example.com"],
|
||||||
"routes": [
|
"routes": [
|
||||||
|
{
|
||||||
|
"name": "pomerium-protected-path-/.pomerium/jwt",
|
||||||
|
"match": {
|
||||||
|
"path": "/.pomerium/jwt"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"cluster": "pomerium-control-plane-http"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pomerium-path-/ping",
|
"name": "pomerium-path-/ping",
|
||||||
"match": {
|
"match": {
|
||||||
|
@ -204,6 +213,15 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
|
||||||
"name": "catch-all",
|
"name": "catch-all",
|
||||||
"domains": ["*"],
|
"domains": ["*"],
|
||||||
"routes": [
|
"routes": [
|
||||||
|
{
|
||||||
|
"name": "pomerium-protected-path-/.pomerium/jwt",
|
||||||
|
"match": {
|
||||||
|
"path": "/.pomerium/jwt"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"cluster": "pomerium-control-plane-http"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pomerium-path-/ping",
|
"name": "pomerium-path-/ping",
|
||||||
"match": {
|
"match": {
|
||||||
|
|
|
@ -16,6 +16,11 @@ import (
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpCluster = "pomerium-control-plane-http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildGRPCRoutes() []*envoy_config_route_v3.Route {
|
func buildGRPCRoutes() []*envoy_config_route_v3.Route {
|
||||||
|
@ -43,6 +48,9 @@ func buildGRPCRoutes() []*envoy_config_route_v3.Route {
|
||||||
|
|
||||||
func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route {
|
func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route {
|
||||||
routes := []*envoy_config_route_v3.Route{
|
routes := []*envoy_config_route_v3.Route{
|
||||||
|
// enable ext_authz
|
||||||
|
buildControlPlaneProtectedPathRoute("/.pomerium/jwt"),
|
||||||
|
// disable ext_authz and passthrough to proxy handlers
|
||||||
buildControlPlanePathRoute("/ping"),
|
buildControlPlanePathRoute("/ping"),
|
||||||
buildControlPlanePathRoute("/healthz"),
|
buildControlPlanePathRoute("/healthz"),
|
||||||
buildControlPlanePathRoute("/.pomerium"),
|
buildControlPlanePathRoute("/.pomerium"),
|
||||||
|
@ -60,11 +68,80 @@ func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_co
|
||||||
}
|
}
|
||||||
// if we're the proxy and this is the forward-auth url
|
// if we're the proxy and this is the forward-auth url
|
||||||
if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) {
|
if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) {
|
||||||
routes = append(routes, buildControlPlanePrefixRoute("/"))
|
routes = append(routes,
|
||||||
|
// disable ext_authz and pass request to proxy handlers that enable authN flow
|
||||||
|
buildControlPlanePathAndQueryRoute("/verify", []string{urlutil.QueryForwardAuthURI, urlutil.QuerySessionEncrypted, urlutil.QueryRedirectURI}),
|
||||||
|
buildControlPlanePathAndQueryRoute("/", []string{urlutil.QueryForwardAuthURI, urlutil.QuerySessionEncrypted, urlutil.QueryRedirectURI}),
|
||||||
|
buildControlPlanePathAndQueryRoute("/", []string{urlutil.QueryForwardAuthURI}),
|
||||||
|
// otherwise, enforce ext_authz; pass all other requests through to an upstream
|
||||||
|
// handler that will simply respond with http status 200 / OK indicating that
|
||||||
|
// the fronting forward-auth proxy can continue.
|
||||||
|
buildControlPlaneProtectedPrefixRoute("/"))
|
||||||
}
|
}
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildControlPlaneProtectedPrefixRoute(prefix string) *envoy_config_route_v3.Route {
|
||||||
|
return &envoy_config_route_v3.Route{
|
||||||
|
Name: "pomerium-protected-prefix-" + prefix,
|
||||||
|
Match: &envoy_config_route_v3.RouteMatch{
|
||||||
|
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix},
|
||||||
|
},
|
||||||
|
Action: &envoy_config_route_v3.Route_Route{
|
||||||
|
Route: &envoy_config_route_v3.RouteAction{
|
||||||
|
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||||
|
Cluster: httpCluster,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildControlPlaneProtectedPathRoute(path string) *envoy_config_route_v3.Route {
|
||||||
|
return &envoy_config_route_v3.Route{
|
||||||
|
Name: "pomerium-protected-path-" + path,
|
||||||
|
Match: &envoy_config_route_v3.RouteMatch{
|
||||||
|
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
|
||||||
|
},
|
||||||
|
Action: &envoy_config_route_v3.Route_Route{
|
||||||
|
Route: &envoy_config_route_v3.RouteAction{
|
||||||
|
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||||
|
Cluster: httpCluster,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envoy_config_route_v3.Route {
|
||||||
|
var queryParameterMatchers []*envoy_config_route_v3.QueryParameterMatcher
|
||||||
|
for _, q := range queryparams {
|
||||||
|
queryParameterMatchers = append(queryParameterMatchers,
|
||||||
|
&envoy_config_route_v3.QueryParameterMatcher{
|
||||||
|
Name: q,
|
||||||
|
QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{PresentMatch: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &envoy_config_route_v3.Route{
|
||||||
|
Name: "pomerium-path-and-query" + path,
|
||||||
|
Match: &envoy_config_route_v3.RouteMatch{
|
||||||
|
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
|
||||||
|
QueryParameters: queryParameterMatchers,
|
||||||
|
},
|
||||||
|
Action: &envoy_config_route_v3.Route_Route{
|
||||||
|
Route: &envoy_config_route_v3.RouteAction{
|
||||||
|
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||||
|
Cluster: httpCluster,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TypedPerFilterConfig: map[string]*any.Any{
|
||||||
|
"envoy.filters.http.ext_authz": disableExtAuthz,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route {
|
func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route {
|
||||||
return &envoy_config_route_v3.Route{
|
return &envoy_config_route_v3.Route{
|
||||||
Name: "pomerium-path-" + path,
|
Name: "pomerium-path-" + path,
|
||||||
|
@ -74,7 +151,7 @@ func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route {
|
||||||
Action: &envoy_config_route_v3.Route_Route{
|
Action: &envoy_config_route_v3.Route_Route{
|
||||||
Route: &envoy_config_route_v3.RouteAction{
|
Route: &envoy_config_route_v3.RouteAction{
|
||||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||||
Cluster: "pomerium-control-plane-http",
|
Cluster: httpCluster,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -93,7 +170,7 @@ func buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route {
|
||||||
Action: &envoy_config_route_v3.Route_Route{
|
Action: &envoy_config_route_v3.Route_Route{
|
||||||
Route: &envoy_config_route_v3.RouteAction{
|
Route: &envoy_config_route_v3.RouteAction{
|
||||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||||
Cluster: "pomerium-control-plane-http",
|
Cluster: httpCluster,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -59,7 +59,17 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
protectedRouteString := func(typ, name string) string {
|
||||||
|
return `{
|
||||||
|
"name": "pomerium-protected-` + typ + `-` + name + `",
|
||||||
|
"match": {
|
||||||
|
"` + typ + `": "` + name + `"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"cluster": "pomerium-control-plane-http"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
t.Run("authenticate", func(t *testing.T) {
|
t.Run("authenticate", func(t *testing.T) {
|
||||||
options := &config.Options{
|
options := &config.Options{
|
||||||
Services: "all",
|
Services: "all",
|
||||||
|
@ -70,6 +80,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
routes := buildPomeriumHTTPRoutes(options, "authenticate.example.com")
|
routes := buildPomeriumHTTPRoutes(options, "authenticate.example.com")
|
||||||
|
|
||||||
testutil.AssertProtoJSONEqual(t, `[
|
testutil.AssertProtoJSONEqual(t, `[
|
||||||
|
`+protectedRouteString("path", "/.pomerium/jwt")+`,
|
||||||
`+routeString("path", "/ping")+`,
|
`+routeString("path", "/ping")+`,
|
||||||
`+routeString("path", "/healthz")+`,
|
`+routeString("path", "/healthz")+`,
|
||||||
`+routeString("path", "/.pomerium")+`,
|
`+routeString("path", "/.pomerium")+`,
|
||||||
|
@ -96,6 +107,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
routes := buildPomeriumHTTPRoutes(options, "from.example.com")
|
routes := buildPomeriumHTTPRoutes(options, "from.example.com")
|
||||||
|
|
||||||
testutil.AssertProtoJSONEqual(t, `[
|
testutil.AssertProtoJSONEqual(t, `[
|
||||||
|
`+protectedRouteString("path", "/.pomerium/jwt")+`,
|
||||||
`+routeString("path", "/ping")+`,
|
`+routeString("path", "/ping")+`,
|
||||||
`+routeString("path", "/healthz")+`,
|
`+routeString("path", "/healthz")+`,
|
||||||
`+routeString("path", "/.pomerium")+`,
|
`+routeString("path", "/.pomerium")+`,
|
||||||
|
@ -122,6 +134,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
routes := buildPomeriumHTTPRoutes(options, "from.example.com")
|
routes := buildPomeriumHTTPRoutes(options, "from.example.com")
|
||||||
|
|
||||||
testutil.AssertProtoJSONEqual(t, `[
|
testutil.AssertProtoJSONEqual(t, `[
|
||||||
|
`+protectedRouteString("path", "/.pomerium/jwt")+`,
|
||||||
`+routeString("path", "/ping")+`,
|
`+routeString("path", "/ping")+`,
|
||||||
`+routeString("path", "/healthz")+`,
|
`+routeString("path", "/healthz")+`,
|
||||||
`+routeString("path", "/.pomerium")+`,
|
`+routeString("path", "/.pomerium")+`,
|
||||||
|
|
|
@ -15,6 +15,7 @@ const (
|
||||||
QuerySessionEncrypted = "pomerium_session_encrypted"
|
QuerySessionEncrypted = "pomerium_session_encrypted"
|
||||||
QueryRedirectURI = "pomerium_redirect_uri"
|
QueryRedirectURI = "pomerium_redirect_uri"
|
||||||
QueryProgrammaticToken = "pomerium_programmatic_token"
|
QueryProgrammaticToken = "pomerium_programmatic_token"
|
||||||
|
QueryForwardAuthURI = "uri"
|
||||||
)
|
)
|
||||||
|
|
||||||
// URL signature based query params used for verifying the authenticity of a URL.
|
// URL signature based query params used for verifying the authenticity of a URL.
|
||||||
|
|
|
@ -101,3 +101,18 @@ func GetDomainsForURL(u *url.URL) []string {
|
||||||
// for everything else we return two routes: 'example.com' and 'example.com:443'
|
// for everything else we return two routes: 'example.com' and 'example.com:443'
|
||||||
return []string{u.Hostname(), net.JoinHostPort(u.Hostname(), defaultPort)}
|
return []string{u.Hostname(), net.JoinHostPort(u.Hostname(), defaultPort)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseEnvoyQueryParams returns a new URL with queryparams parsed from envoy format.
|
||||||
|
func ParseEnvoyQueryParams(u *url.URL) *url.URL {
|
||||||
|
nu := &url.URL{
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Host: u.Host,
|
||||||
|
Path: u.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
path := u.Path
|
||||||
|
if idx := strings.Index(path, "?"); idx != -1 {
|
||||||
|
nu.Path, nu.RawQuery = path[:idx], path[idx+1:]
|
||||||
|
}
|
||||||
|
return nu
|
||||||
|
}
|
||||||
|
|
|
@ -157,3 +157,23 @@ func TestGetDomainsForURL(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseEnvoyQueryParams(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
u *url.URL
|
||||||
|
want *url.URL
|
||||||
|
}{
|
||||||
|
{"empty", &url.URL{}, &url.URL{}},
|
||||||
|
{"basic example", &url.URL{Host: "pomerium.io", Path: "/?uri=https://pomerium.com/"}, &url.URL{Host: "pomerium.io", Path: "/", RawQuery: "uri=https://pomerium.com/"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
got := ParseEnvoyQueryParams(tt.u)
|
||||||
|
if diff := cmp.Diff(got, tt.want); diff != "" {
|
||||||
|
t.Errorf("ParseEnvoyQueryParams() = %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,11 +16,6 @@ import (
|
||||||
// see : https://www.pomerium.io/configuration/#forward-auth
|
// see : https://www.pomerium.io/configuration/#forward-auth
|
||||||
func (p *Proxy) registerFwdAuthHandlers() http.Handler {
|
func (p *Proxy) registerFwdAuthHandlers() http.Handler {
|
||||||
r := httputil.NewRouter()
|
r := httputil.NewRouter()
|
||||||
r.StrictSlash(true)
|
|
||||||
r.Use(func(h http.Handler) http.Handler {
|
|
||||||
return sessions.RetrieveSession(p.state.Load().sessionStore)(h)
|
|
||||||
})
|
|
||||||
r.Use(p.jwtClaimMiddleware(true))
|
|
||||||
|
|
||||||
// NGNIX's forward-auth capabilities are split across two settings:
|
// NGNIX's forward-auth capabilities are split across two settings:
|
||||||
// `auth-url` and `auth-signin` which correspond to `verify` and `auth-url`
|
// `auth-url` and `auth-signin` which correspond to `verify` and `auth-url`
|
||||||
|
@ -31,21 +25,29 @@ func (p *Proxy) registerFwdAuthHandlers() http.Handler {
|
||||||
|
|
||||||
// nginx 3: save the returned session post authenticate flow
|
// nginx 3: save the returned session post authenticate flow
|
||||||
r.Handle("/verify", httputil.HandlerFunc(p.nginxCallback)).
|
r.Handle("/verify", httputil.HandlerFunc(p.nginxCallback)).
|
||||||
Queries("uri", "{uri}", urlutil.QuerySessionEncrypted, "", urlutil.QueryRedirectURI, "")
|
Queries(urlutil.QueryForwardAuthURI, "{uri}",
|
||||||
|
urlutil.QuerySessionEncrypted, "",
|
||||||
|
urlutil.QueryRedirectURI, "")
|
||||||
|
|
||||||
// nginx 1: verify. Return 401 if invalid and NGINX will call `auth-signin`
|
// nginx 1: verify, fronted by ext_authz
|
||||||
r.Handle("/verify", p.Verify(true)).Queries("uri", "{uri}")
|
r.Handle("/verify", httputil.HandlerFunc(p.allowUpstream)).
|
||||||
|
Queries(urlutil.QueryForwardAuthURI, "{uri}")
|
||||||
|
|
||||||
// nginx 4: redirect the user back to their originally requested location.
|
// nginx 4: redirect the user back to their originally requested location.
|
||||||
r.Handle("/", httputil.HandlerFunc(p.nginxPostCallbackRedirect)).
|
r.Handle("/", httputil.HandlerFunc(p.nginxPostCallbackRedirect)).
|
||||||
Queries("uri", "{uri}", urlutil.QuerySessionEncrypted, "", urlutil.QueryRedirectURI, "")
|
Queries(urlutil.QueryForwardAuthURI, "{uri}",
|
||||||
|
urlutil.QuerySessionEncrypted, "",
|
||||||
|
urlutil.QueryRedirectURI, "")
|
||||||
|
|
||||||
// traefik 2: save the returned session post authenticate flow
|
// traefik 2: save the returned session post authenticate flow
|
||||||
r.Handle("/", httputil.HandlerFunc(p.forwardedURIHeaderCallback)).
|
r.Handle("/", httputil.HandlerFunc(p.forwardedURIHeaderCallback)).
|
||||||
HeadersRegexp(httputil.HeaderForwardedURI, urlutil.QuerySessionEncrypted)
|
HeadersRegexp(httputil.HeaderForwardedURI, urlutil.QuerySessionEncrypted)
|
||||||
|
|
||||||
|
r.Handle("/", httputil.HandlerFunc(p.startAuthN)).
|
||||||
|
Queries(urlutil.QueryForwardAuthURI, "{uri}")
|
||||||
|
|
||||||
// nginx 2 / traefik 1: verify and then start authenticate flow
|
// nginx 2 / traefik 1: verify and then start authenticate flow
|
||||||
r.Handle("/", p.Verify(false))
|
r.Handle("/", httputil.HandlerFunc(p.allowUpstream))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -53,8 +55,15 @@ func (p *Proxy) registerFwdAuthHandlers() http.Handler {
|
||||||
// nginxPostCallbackRedirect redirects the user to their original destination
|
// nginxPostCallbackRedirect redirects the user to their original destination
|
||||||
// in order to drop the authenticate related query params
|
// in order to drop the authenticate related query params
|
||||||
func (p *Proxy) nginxPostCallbackRedirect(w http.ResponseWriter, r *http.Request) error {
|
func (p *Proxy) nginxPostCallbackRedirect(w http.ResponseWriter, r *http.Request) error {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
u, err := url.Parse(r.FormValue(urlutil.QueryRedirectURI))
|
||||||
httputil.Redirect(w, r, r.FormValue(urlutil.QueryRedirectURI), http.StatusFound)
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
u = urlutil.ParseEnvoyQueryParams(u)
|
||||||
|
q := u.Query()
|
||||||
|
q.Del(urlutil.QueryForwardAuthURI)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
httputil.Redirect(w, r, u.String(), http.StatusFound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,9 +76,7 @@ func (p *Proxy) nginxCallback(w http.ResponseWriter, r *http.Request) error {
|
||||||
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
|
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
return httputil.NewError(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
return httputil.NewError(http.StatusUnauthorized, errors.New("mock error to restart redirect flow"))
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// forwardedURIHeaderCallback handles the post-authentication callback from
|
// forwardedURIHeaderCallback handles the post-authentication callback from
|
||||||
|
@ -86,74 +93,42 @@ func (p *Proxy) forwardedURIHeaderCallback(w http.ResponseWriter, r *http.Reques
|
||||||
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
|
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
return httputil.NewError(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
httputil.Redirect(w, r, redirectURLString, http.StatusFound)
|
httputil.Redirect(w, r, redirectURLString, http.StatusFound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checks a user's credentials for an arbitrary host. If the user
|
// allowUpstream will return status 200 (OK) unless auth_status is set to forbidden.
|
||||||
// is properly authenticated and is authorized to access the supplied host,
|
// This handler is expected to be behind a routed protected by envoy's control plane (ext_authz).
|
||||||
// a `200` http status code is returned. If the user is not authenticated, they
|
func (p *Proxy) allowUpstream(w http.ResponseWriter, r *http.Request) error {
|
||||||
// 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 {
|
|
||||||
state := p.state.Load()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if status := r.FormValue("auth_status"); status == fmt.Sprint(http.StatusForbidden) {
|
if status := r.FormValue("auth_status"); status == fmt.Sprint(http.StatusForbidden) {
|
||||||
return httputil.NewError(http.StatusForbidden, errors.New(http.StatusText(http.StatusForbidden)))
|
return httputil.NewError(http.StatusForbidden, errors.New(http.StatusText(http.StatusForbidden)))
|
||||||
}
|
}
|
||||||
|
|
||||||
uri, err := getURIStringFromRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ar, err := p.isAuthorized(w, r)
|
|
||||||
if err != nil {
|
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ar.authorized {
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintf(w, "Access to %s is allowed.", uri.Host)
|
fmt.Fprintln(w, http.StatusText(http.StatusOK))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unAuthenticated := ar.statusCode == http.StatusUnauthorized
|
// startAuthN redirects an unauthenticated user to start forward-auth
|
||||||
if unAuthenticated {
|
// authentication flow
|
||||||
state.sessionStore.ClearSession(w, r)
|
func (p *Proxy) startAuthN(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sessions.FromContext(r.Context())
|
|
||||||
hasSession := err == nil
|
|
||||||
if hasSession && !unAuthenticated {
|
|
||||||
return httputil.NewError(http.StatusForbidden, errors.New("access denied"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if verifyOnly {
|
|
||||||
return httputil.NewError(http.StatusUnauthorized, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.forwardAuthRedirectToSignInWithURI(w, r, uri)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// forwardAuthRedirectToSignInWithURI redirects request to authenticate signin url,
|
|
||||||
// with all necessary information extracted from given input uri.
|
|
||||||
func (p *Proxy) forwardAuthRedirectToSignInWithURI(w http.ResponseWriter, r *http.Request, uri *url.URL) {
|
|
||||||
state := p.state.Load()
|
state := p.state.Load()
|
||||||
|
uriString := r.FormValue(urlutil.QueryForwardAuthURI)
|
||||||
// Traefik set the uri in the header, we must set it in redirect uri if present. Otherwise, request like
|
if uriString == "" {
|
||||||
// https://example.com/foo will be redirected to https://example.com after authentication.
|
uriString = "https://" + // always use HTTPS for external urls
|
||||||
|
r.Header.Get(httputil.HeaderForwardedHost) +
|
||||||
|
r.Header.Get(httputil.HeaderForwardedURI)
|
||||||
|
}
|
||||||
|
uri, err := urlutil.ParseAndValidateURL(uriString)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
// add any non-empty existing path from the forwarded URI
|
||||||
if xfu := r.Header.Get(httputil.HeaderForwardedURI); xfu != "" && xfu != "/" {
|
if xfu := r.Header.Get(httputil.HeaderForwardedURI); xfu != "" && xfu != "/" {
|
||||||
uri.Path = xfu
|
uri.Path = xfu
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to authenticate
|
|
||||||
authN := *state.authenticateSigninURL
|
authN := *state.authenticateSigninURL
|
||||||
q := authN.Query()
|
q := authN.Query()
|
||||||
q.Set(urlutil.QueryCallbackURI, uri.String())
|
q.Set(urlutil.QueryCallbackURI, uri.String())
|
||||||
|
@ -161,25 +136,5 @@ func (p *Proxy) forwardAuthRedirectToSignInWithURI(w http.ResponseWriter, r *htt
|
||||||
q.Set(urlutil.QueryForwardAuth, urlutil.StripPort(r.Host)) // add fwd auth to trusted audience
|
q.Set(urlutil.QueryForwardAuth, urlutil.StripPort(r.Host)) // add fwd auth to trusted audience
|
||||||
authN.RawQuery = q.Encode()
|
authN.RawQuery = q.Encode()
|
||||||
httputil.Redirect(w, r, urlutil.NewSignedURL(state.sharedKey, &authN).String(), http.StatusFound)
|
httputil.Redirect(w, r, urlutil.NewSignedURL(state.sharedKey, &authN).String(), http.StatusFound)
|
||||||
}
|
return nil
|
||||||
|
|
||||||
func getURIStringFromRequest(r *http.Request) (*url.URL, error) {
|
|
||||||
// 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.HeaderForwardedHost) == "" {
|
|
||||||
return nil, errors.New("no uri to validate")
|
|
||||||
}
|
|
||||||
// Always assume HTTPS for application callback
|
|
||||||
uriString = "https://" +
|
|
||||||
r.Header.Get(httputil.HeaderForwardedHost) +
|
|
||||||
r.Header.Get(httputil.HeaderForwardedURI)
|
|
||||||
}
|
|
||||||
|
|
||||||
uri, err := urlutil.ParseAndValidateURL(uriString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return uri, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,13 +63,10 @@ func TestProxy_ForwardAuth(t *testing.T) {
|
||||||
wantStatus int
|
wantStatus int
|
||||||
wantBody string
|
wantBody string
|
||||||
}{
|
}{
|
||||||
{"good redirect not required", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusOK, "Access to some.domain.example is allowed."},
|
|
||||||
{"good verify only, no redirect", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusOK, ""},
|
{"good verify only, no redirect", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusOK, ""},
|
||||||
{"bad empty domain uri", opts, nil, http.MethodGet, nil, map[string]string{"uri": ""}, "https://some.domain.example/", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: no uri to validate\"}\n"},
|
{"bad empty domain uri", opts, nil, http.MethodGet, nil, map[string]string{"uri": ""}, "https://some.domain.example/", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: https: url does contain a valid hostname\"}\n"},
|
||||||
{"bad naked domain uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"},
|
{"bad naked domain uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"},
|
||||||
{"bad naked domain uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"},
|
|
||||||
{"bad empty verification uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"},
|
{"bad empty verification uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"},
|
||||||
{"bad empty verification uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"},
|
|
||||||
// traefik
|
// traefik
|
||||||
{"good traefik callback", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusFound, ""},
|
{"good traefik callback", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusFound, ""},
|
||||||
{"bad traefik callback bad session", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString + "garbage"}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, ""},
|
{"bad traefik callback bad session", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString + "garbage"}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, allowClient, http.StatusBadRequest, ""},
|
||||||
|
@ -90,7 +87,6 @@ func TestProxy_ForwardAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
p.OnConfigChange(&config.Config{Options: tt.options})
|
p.OnConfigChange(&config.Config{Options: tt.options})
|
||||||
state := p.state.Load()
|
state := p.state.Load()
|
||||||
state.authzClient = tt.authorizer
|
|
||||||
state.sessionStore = tt.sessionStore
|
state.sessionStore = tt.sessionStore
|
||||||
signer, err := jws.NewHS256Signer(nil, "mock")
|
signer, err := jws.NewHS256Signer(nil, "mock")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -160,28 +161,19 @@ func (p *Proxy) ProgrammaticLogin(w http.ResponseWriter, r *http.Request) error
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(response))
|
_, _ = io.WriteString(w, response)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jwtAssertion returns the current user's/request's JWT (rfc7519#section-10.3.1) that should be
|
||||||
|
// added from the downstream request.
|
||||||
func (p *Proxy) jwtAssertion(w http.ResponseWriter, r *http.Request) error {
|
func (p *Proxy) jwtAssertion(w http.ResponseWriter, r *http.Request) error {
|
||||||
res, err := p.authorizeCheck(r)
|
assertionJWT := r.Header.Get(httputil.HeaderPomeriumJWTAssertion)
|
||||||
if err != nil {
|
if assertionJWT == "" {
|
||||||
return httputil.NewError(http.StatusInternalServerError, err)
|
return httputil.NewError(http.StatusNotFound, errors.New("jwt not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := append(res.GetOkResponse().GetHeaders(), res.GetDeniedResponse().GetHeaders()...)
|
|
||||||
for _, h := range headers {
|
|
||||||
if h.GetHeader().GetKey() == httputil.HeaderPomeriumJWTAssertion {
|
|
||||||
w.Header().Set("Content-Type", "application/jwt")
|
w.Header().Set("Content-Type", "application/jwt")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = io.WriteString(w, h.GetHeader().GetValue())
|
_, _ = io.WriteString(w, assertionJWT)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
_, _ = io.WriteString(w, "jwt not found")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,21 +11,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mstore "github.com/pomerium/pomerium/internal/sessions/mock"
|
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/encoding"
|
"github.com/pomerium/pomerium/internal/encoding"
|
||||||
"github.com/pomerium/pomerium/internal/encoding/mock"
|
"github.com/pomerium/pomerium/internal/encoding/mock"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
|
mstore "github.com/pomerium/pomerium/internal/sessions/mock"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -68,39 +64,6 @@ func TestProxy_Signout(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxy_jwt(t *testing.T) {
|
|
||||||
authzClient := &mockCheckClient{
|
|
||||||
response: &envoy_service_auth_v2.CheckResponse{
|
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
{Header: &envoy_api_v2_core.HeaderValue{
|
|
||||||
Key: httputil.HeaderPomeriumJWTAssertion,
|
|
||||||
Value: "MOCK_JWT",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "https://www.example.com/.pomerium/jwt", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
proxy := &Proxy{
|
|
||||||
state: newAtomicProxyState(&proxyState{
|
|
||||||
authzClient: authzClient,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
err := proxy.jwtAssertion(w, req)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "application/jwt", w.Header().Get("Content-Type"))
|
|
||||||
assert.Equal(t, w.Body.String(), "MOCK_JWT")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxy_UserDashboard(t *testing.T) {
|
func TestProxy_UserDashboard(t *testing.T) {
|
||||||
opts := testOptions(t)
|
opts := testOptions(t)
|
||||||
err := ValidateOptions(opts)
|
err := ValidateOptions(opts)
|
||||||
|
@ -532,3 +495,29 @@ func TestProxy_ProgrammaticCallback(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProxy_jwt(t *testing.T) {
|
||||||
|
|
||||||
|
// without downstream headers being set
|
||||||
|
req, _ := http.NewRequest("GET", "https://www.example.com/.pomerium/jwt", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
proxy := &Proxy{
|
||||||
|
state: newAtomicProxyState(&proxyState{}),
|
||||||
|
}
|
||||||
|
err := proxy.jwtAssertion(w, req)
|
||||||
|
if !assert.Error(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// with downstream request headers being set
|
||||||
|
req, _ = http.NewRequest("GET", "https://www.example.com/.pomerium/jwt", nil)
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req.Header.Set(httputil.HeaderPomeriumJWTAssertion, "MOCK_JWT")
|
||||||
|
err = proxy.jwtAssertion(w, req)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, "application/jwt", w.Header().Get("Content-Type"))
|
||||||
|
assert.Equal(t, w.Body.String(), "MOCK_JWT")
|
||||||
|
}
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
|
||||||
"github.com/golang/protobuf/ptypes"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type authorizeResponse struct {
|
|
||||||
authorized bool
|
|
||||||
statusCode int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) isAuthorized(w http.ResponseWriter, r *http.Request) (*authorizeResponse, error) {
|
|
||||||
res, err := p.authorizeCheck(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httputil.NewError(http.StatusInternalServerError, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ar := &authorizeResponse{}
|
|
||||||
switch res.HttpResponse.(type) {
|
|
||||||
case *envoy_service_auth_v2.CheckResponse_OkResponse:
|
|
||||||
for _, hdr := range res.GetOkResponse().GetHeaders() {
|
|
||||||
w.Header().Set(hdr.GetHeader().GetKey(), hdr.GetHeader().GetValue())
|
|
||||||
}
|
|
||||||
ar.authorized = true
|
|
||||||
ar.statusCode = res.GetStatus().Code
|
|
||||||
case *envoy_service_auth_v2.CheckResponse_DeniedResponse:
|
|
||||||
ar.statusCode = int32(res.GetDeniedResponse().GetStatus().Code)
|
|
||||||
default:
|
|
||||||
ar.statusCode = http.StatusInternalServerError
|
|
||||||
}
|
|
||||||
return ar, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Proxy) authorizeCheck(r *http.Request) (*envoy_service_auth_v2.CheckResponse, error) {
|
|
||||||
state := p.state.Load()
|
|
||||||
|
|
||||||
tm, err := ptypes.TimestampProto(time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return nil, httputil.NewError(http.StatusInternalServerError, fmt.Errorf("error creating protobuf timestamp from current time: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
httpAttrs := &envoy_service_auth_v2.AttributeContext_HttpRequest{
|
|
||||||
Method: "GET",
|
|
||||||
Headers: map[string]string{},
|
|
||||||
Path: r.URL.Path,
|
|
||||||
Host: r.Host,
|
|
||||||
Scheme: r.URL.Scheme,
|
|
||||||
Fragment: r.URL.Fragment,
|
|
||||||
}
|
|
||||||
for k := range r.Header {
|
|
||||||
httpAttrs.Headers[k] = r.Header.Get(k)
|
|
||||||
}
|
|
||||||
if r.URL.RawQuery != "" {
|
|
||||||
// envoy expects the query string in the path
|
|
||||||
httpAttrs.Path += "?" + r.URL.RawQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.authzClient.Check(r.Context(), &envoy_service_auth_v2.CheckRequest{
|
|
||||||
Attributes: &envoy_service_auth_v2.AttributeContext{
|
|
||||||
Request: &envoy_service_auth_v2.AttributeContext_Request{
|
|
||||||
Time: tm,
|
|
||||||
Http: httpAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwtClaimMiddleware logs and propagates JWT claim information via request headers
|
|
||||||
//
|
|
||||||
// if returnJWTInfo is set to true, it will also return JWT claim information in the response
|
|
||||||
func (p *Proxy) jwtClaimMiddleware(returnJWTInfo bool) mux.MiddlewareFunc {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
defer next.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
state := p.state.Load()
|
|
||||||
|
|
||||||
jwt, err := sessions.FromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("proxy: could not locate session from context")
|
|
||||||
return nil // best effort decoding
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedJWTClaims, err := p.getFormatedJWTClaims([]byte(jwt))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("proxy: failed to format jwt claims")
|
|
||||||
return nil // best effort formatting
|
|
||||||
}
|
|
||||||
|
|
||||||
// log group, email, user claims
|
|
||||||
l := log.Ctx(r.Context())
|
|
||||||
for _, claimName := range []string{"groups", "email", "user"} {
|
|
||||||
|
|
||||||
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
|
||||||
return c.Str(claimName, fmt.Sprintf("%v", formattedJWTClaims[claimName]))
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// set headers for any claims specified by config
|
|
||||||
for _, claimName := range state.jwtClaimHeaders {
|
|
||||||
if _, ok := formattedJWTClaims[claimName]; ok {
|
|
||||||
|
|
||||||
headerName := fmt.Sprintf("x-pomerium-claim-%s", claimName)
|
|
||||||
r.Header.Set(headerName, formattedJWTClaims[claimName])
|
|
||||||
if returnJWTInfo {
|
|
||||||
w.Header().Add(headerName, formattedJWTClaims[claimName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFormatJWTClaims reformats jwtClaims into something resembling map[string]string
|
|
||||||
func (p *Proxy) getFormatedJWTClaims(jwt []byte) (map[string]string, error) {
|
|
||||||
state := p.state.Load()
|
|
||||||
|
|
||||||
formattedJWTClaims := make(map[string]string)
|
|
||||||
|
|
||||||
var jwtClaims map[string]interface{}
|
|
||||||
if err := state.encoder.Unmarshal(jwt, &jwtClaims); err != nil {
|
|
||||||
return formattedJWTClaims, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for claim, value := range jwtClaims {
|
|
||||||
var formattedClaim string
|
|
||||||
if cv, ok := value.([]interface{}); ok {
|
|
||||||
elements := make([]string, len(cv))
|
|
||||||
|
|
||||||
for i, v := range cv {
|
|
||||||
elements[i] = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
formattedClaim = strings.Join(elements, ",")
|
|
||||||
} else {
|
|
||||||
formattedClaim = fmt.Sprintf("%v", value)
|
|
||||||
}
|
|
||||||
formattedJWTClaims[claim] = formattedClaim
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedJWTClaims, nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_jwtClaimMiddleware(t *testing.T) {
|
|
||||||
claimHeaders := []string{"email", "groups", "missing"}
|
|
||||||
sharedKey := "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
|
|
||||||
|
|
||||||
session := &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}
|
|
||||||
encoder, _ := jws.NewHS256Signer([]byte(sharedKey), "https://authenticate.pomerium.example")
|
|
||||||
state, err := encoder.Marshal(session)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to marshal state: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a := Proxy{
|
|
||||||
state: newAtomicProxyState(&proxyState{
|
|
||||||
sharedKey: sharedKey,
|
|
||||||
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
|
|
||||||
encoder: encoder,
|
|
||||||
jwtClaimHeaders: claimHeaders,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = sessions.NewContext(ctx, string(state), nil)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
proxyHandler := a.jwtClaimMiddleware(true)(handler)
|
|
||||||
proxyHandler.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
t.Run("missing claim", func(t *testing.T) {
|
|
||||||
absentHeader := r.Header.Get("x-pomerium-claim-missing")
|
|
||||||
if absentHeader != "" {
|
|
||||||
t.Errorf("found claim that should not exist, got=%q", absentHeader)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
|
@ -44,9 +44,6 @@ func ValidateOptions(o *config.Options) error {
|
||||||
return fmt.Errorf("proxy: invalid 'AUTHENTICATE_SERVICE_URL': %w", err)
|
return fmt.Errorf("proxy: invalid 'AUTHENTICATE_SERVICE_URL': %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := urlutil.ValidateURL(o.AuthorizeURL); err != nil {
|
|
||||||
return fmt.Errorf("proxy: invalid 'AUTHORIZE_SERVICE_URL': %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
func testOptions(t *testing.T) *config.Options {
|
func testOptions(t *testing.T) *config.Options {
|
||||||
opts := config.NewDefaultOptions()
|
opts := config.NewDefaultOptions()
|
||||||
opts.AuthenticateURLString = "https://authenticate.example"
|
opts.AuthenticateURLString = "https://authenticate.example"
|
||||||
opts.AuthorizeURLString = "https://authorize.example"
|
|
||||||
|
|
||||||
testPolicy := config.Policy{From: "https://corp.example.example", To: "https://example.example"}
|
testPolicy := config.Policy{From: "https://corp.example.example", To: "https://example.example"}
|
||||||
opts.Policies = []config.Policy{testPolicy}
|
opts.Policies = []config.Policy{testPolicy}
|
||||||
|
@ -38,10 +37,6 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
authurl, _ := url.Parse("authenticate.corp.beyondperimeter.com")
|
authurl, _ := url.Parse("authenticate.corp.beyondperimeter.com")
|
||||||
authenticateBadScheme := testOptions(t)
|
authenticateBadScheme := testOptions(t)
|
||||||
authenticateBadScheme.AuthenticateURL = authurl
|
authenticateBadScheme.AuthenticateURL = authurl
|
||||||
authorizeBadSCheme := testOptions(t)
|
|
||||||
authorizeBadSCheme.AuthorizeURL = authurl
|
|
||||||
authorizeNil := testOptions(t)
|
|
||||||
authorizeNil.AuthorizeURL = nil
|
|
||||||
emptyCookieSecret := testOptions(t)
|
emptyCookieSecret := testOptions(t)
|
||||||
emptyCookieSecret.CookieSecret = ""
|
emptyCookieSecret.CookieSecret = ""
|
||||||
invalidCookieSecret := testOptions(t)
|
invalidCookieSecret := testOptions(t)
|
||||||
|
@ -64,8 +59,6 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
{"nil options", &config.Options{}, true},
|
{"nil options", &config.Options{}, true},
|
||||||
{"authenticate service url", badAuthURL, true},
|
{"authenticate service url", badAuthURL, true},
|
||||||
{"authenticate service url no scheme", authenticateBadScheme, true},
|
{"authenticate service url no scheme", authenticateBadScheme, true},
|
||||||
{"authorize service url no scheme", authorizeBadSCheme, true},
|
|
||||||
{"authorize service cannot be nil", authorizeNil, true},
|
|
||||||
{"no cookie secret", emptyCookieSecret, true},
|
{"no cookie secret", emptyCookieSecret, true},
|
||||||
{"invalid cookie secret", invalidCookieSecret, true},
|
{"invalid cookie secret", invalidCookieSecret, true},
|
||||||
{"short cookie secret", shortCookieLength, true},
|
{"short cookie secret", shortCookieLength, true},
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/encoding"
|
"github.com/pomerium/pomerium/internal/encoding"
|
||||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||||
|
@ -19,14 +17,12 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/sessions/queryparam"
|
"github.com/pomerium/pomerium/internal/sessions/queryparam"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type proxyState struct {
|
type proxyState struct {
|
||||||
sharedKey string
|
sharedKey string
|
||||||
sharedCipher cipher.AEAD
|
sharedCipher cipher.AEAD
|
||||||
|
|
||||||
authorizeURL *url.URL
|
|
||||||
authenticateURL *url.URL
|
authenticateURL *url.URL
|
||||||
authenticateDashboardURL *url.URL
|
authenticateDashboardURL *url.URL
|
||||||
authenticateSigninURL *url.URL
|
authenticateSigninURL *url.URL
|
||||||
|
@ -39,7 +35,6 @@ type proxyState struct {
|
||||||
sessionStore sessions.SessionStore
|
sessionStore sessions.SessionStore
|
||||||
sessionLoaders []sessions.SessionLoader
|
sessionLoaders []sessions.SessionLoader
|
||||||
jwtClaimHeaders []string
|
jwtClaimHeaders []string
|
||||||
authzClient envoy_service_auth_v2.AuthorizationClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
|
func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
|
||||||
|
@ -63,7 +58,6 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
|
||||||
state.jwtClaimHeaders = cfg.Options.JWTClaimsHeaders
|
state.jwtClaimHeaders = cfg.Options.JWTClaimsHeaders
|
||||||
|
|
||||||
// errors checked in ValidateOptions
|
// errors checked in ValidateOptions
|
||||||
state.authorizeURL, _ = urlutil.DeepCopy(cfg.Options.AuthorizeURL)
|
|
||||||
state.authenticateURL, _ = urlutil.DeepCopy(cfg.Options.AuthenticateURL)
|
state.authenticateURL, _ = urlutil.DeepCopy(cfg.Options.AuthenticateURL)
|
||||||
state.authenticateDashboardURL = state.authenticateURL.ResolveReference(&url.URL{Path: dashboardPath})
|
state.authenticateDashboardURL = state.authenticateURL.ResolveReference(&url.URL{Path: dashboardPath})
|
||||||
state.authenticateSigninURL = state.authenticateURL.ResolveReference(&url.URL{Path: signinURL})
|
state.authenticateSigninURL = state.authenticateURL.ResolveReference(&url.URL{Path: signinURL})
|
||||||
|
@ -87,21 +81,6 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
|
||||||
header.NewStore(state.encoder, httputil.AuthorizationTypePomerium),
|
header.NewStore(state.encoder, httputil.AuthorizationTypePomerium),
|
||||||
queryparam.NewStore(state.encoder, "pomerium_session")}
|
queryparam.NewStore(state.encoder, "pomerium_session")}
|
||||||
|
|
||||||
authzConn, err := grpc.GetGRPCClientConn("authorize", &grpc.Options{
|
|
||||||
Addr: state.authorizeURL,
|
|
||||||
OverrideCertificateName: cfg.Options.OverrideCertificateName,
|
|
||||||
CA: cfg.Options.CA,
|
|
||||||
CAFile: cfg.Options.CAFile,
|
|
||||||
RequestTimeout: cfg.Options.GRPCClientTimeout,
|
|
||||||
ClientDNSRoundRobin: cfg.Options.GRPCClientDNSRoundRobin,
|
|
||||||
WithInsecure: cfg.Options.GRPCInsecure,
|
|
||||||
ServiceName: cfg.Options.Services,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
state.authzClient = envoy_service_auth_v2.NewAuthorizationClient(authzConn)
|
|
||||||
|
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue