diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 0e2558390..c5f6beb40 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -70,7 +70,12 @@ func (a *Authenticate) Mount(r *mux.Router) { // Identity Provider (IdP) endpoints r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet) - v := r.PathPrefix("/.pomerium").Subrouter() + a.mountDashboard(r) + a.mountWellKnown(r) +} + +func (a *Authenticate) mountDashboard(r *mux.Router) { + sr := r.PathPrefix("/.pomerium").Subrouter() c := cors.New(cors.Options{ AllowOriginRequestFunc: func(r *http.Request, _ string) bool { state := a.state.Load() @@ -83,13 +88,15 @@ func (a *Authenticate) Mount(r *mux.Router) { AllowCredentials: true, AllowedHeaders: []string{"*"}, }) - v.Use(c.Handler) - v.Use(a.RetrieveSession) - v.Use(a.VerifySession) - v.Path("/").Handler(httputil.HandlerFunc(a.userInfo)) - v.Path("/sign_in").Handler(httputil.HandlerFunc(a.SignIn)) - v.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) + sr.Use(c.Handler) + sr.Use(a.RetrieveSession) + sr.Use(a.VerifySession) + sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo)) + sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn)) + sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut)) +} +func (a *Authenticate) mountWellKnown(r *mux.Router) { wk := r.PathPrefix("/.well-known/pomerium").Subrouter() wk.Path("/jwks.json").Handler(httputil.HandlerFunc(a.jwks)).Methods(http.MethodGet) wk.Path("/").Handler(httputil.HandlerFunc(a.wellKnown)).Methods(http.MethodGet) @@ -476,6 +483,12 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error { } groups = append(groups, pbDirectoryGroup) } + + signoutURL, err := a.getSignOutURL(r) + if err != nil { + return fmt.Errorf("invalid signout url: %w", err) + } + input := map[string]interface{}{ "State": s, // local session state (cookie, header, etc) "Session": pbSession, // current access, refresh, id token, & impersonation state @@ -483,8 +496,7 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error { "DirectoryUser": pbDirectoryUser, // user details inferred from idp directory "DirectoryGroups": groups, // user's groups inferred from idp directory "csrfField": csrf.TemplateField(r), - "RedirectURL": r.URL.Query().Get(urlutil.QueryRedirectURI), - "SignOutURL": "/.pomerium/sign_out", + "SignOutURL": signoutURL, } return a.templates.ExecuteTemplate(w, "userInfo.html", input) } @@ -579,3 +591,20 @@ func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter, return rawIDToken } + +func (a *Authenticate) getSignOutURL(r *http.Request) (*url.URL, error) { + uri, err := a.options.Load().GetAuthenticateURL() + if err != nil { + return nil, err + } + + uri.ResolveReference(&url.URL{ + Path: "/.pomerium/sign_out", + }) + if redirectURI := r.FormValue(urlutil.QueryRedirectURI); redirectURI != "" { + uri.RawQuery = (&url.Values{ + urlutil.QueryRedirectURI: {redirectURI}, + }).Encode() + } + return urlutil.NewSignedURL(a.options.Load().SharedKey, uri).Sign(), nil +} diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index 6f884dd43..00cbc39d8 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -597,12 +597,36 @@ func TestAuthenticate_userInfo(t *testing.T) { pbNow, _ := ptypes.TimestampProto(now) tests := []struct { name string + url *url.URL method string sessionStore sessions.SessionStore wantCode int wantBody string }{ - {"good", http.MethodGet, &mstore.Store{Encrypted: true, Session: &sessions.State{ID: "SESSION_ID", IssuedAt: jwt.NewNumericDate(now)}}, http.StatusOK, ""}, + { + "good", + mustParseURL("/"), + http.MethodGet, + &mstore.Store{Encrypted: true, Session: &sessions.State{ID: "SESSION_ID", IssuedAt: jwt.NewNumericDate(now)}}, + http.StatusOK, + "", + }, + { + "missing signature", + mustParseURL("/?pomerium_redirect_uri=http://example.com"), + http.MethodGet, + &mstore.Store{Encrypted: true, Session: &sessions.State{ID: "SESSION_ID", IssuedAt: jwt.NewNumericDate(now)}}, + http.StatusBadRequest, + "", + }, + { + "bad signature", + urlutil.NewSignedURL("BAD KEY", mustParseURL("/?pomerium_redirect_uri=http://example.com")).Sign(), + http.MethodGet, + &mstore.Store{Encrypted: true, Session: &sessions.State{ID: "SESSION_ID", IssuedAt: jwt.NewNumericDate(now)}}, + http.StatusBadRequest, + "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -613,8 +637,13 @@ func TestAuthenticate_userInfo(t *testing.T) { if err != nil { t.Fatal(err) } + o := config.NewAtomicOptions() + o.Store(&config.Options{ + AuthenticateURLString: "https://authenticate.localhost.pomerium.io", + SharedKey: "SHARED KEY", + }) a := &Authenticate{ - options: config.NewAtomicOptions(), + options: o, state: newAtomicAuthenticateState(&authenticateState{ sessionStore: tt.sessionStore, encryptedEncoder: signer, @@ -644,8 +673,7 @@ func TestAuthenticate_userInfo(t *testing.T) { }), templates: template.Must(frontend.NewTemplates()), } - u, _ := url.Parse("/") - r := httptest.NewRequest(tt.method, u.String(), nil) + r := httptest.NewRequest(tt.method, tt.url.String(), nil) state, err := tt.sessionStore.LoadSession(r) if err != nil { t.Fatal(err) @@ -656,7 +684,7 @@ func TestAuthenticate_userInfo(t *testing.T) { r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() - httputil.HandlerFunc(a.userInfo).ServeHTTP(w, r) + a.requireValidSignatureOnRedirect(a.userInfo).ServeHTTP(w, r) if status := w.Code; status != tt.wantCode { t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode) } @@ -779,3 +807,11 @@ func TestAuthenticate_SignOut_CSRF(t *testing.T) { }) } } + +func mustParseURL(rawurl string) *url.URL { + u, err := url.Parse(rawurl) + if err != nil { + panic(err) + } + return u +} diff --git a/authenticate/middleware.go b/authenticate/middleware.go new file mode 100644 index 000000000..37f4e196f --- /dev/null +++ b/authenticate/middleware.go @@ -0,0 +1,34 @@ +package authenticate + +import ( + "net/http" + + "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/internal/middleware" + "github.com/pomerium/pomerium/internal/urlutil" +) + +// requireValidSignatureOnRedirect validates the pomerium_signature if a redirect_uri or pomerium_signature +// is present on the query string. +func (a *Authenticate) requireValidSignatureOnRedirect(next httputil.HandlerFunc) http.Handler { + return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + if r.FormValue(urlutil.QueryRedirectURI) != "" || r.FormValue(urlutil.QueryHmacSignature) != "" { + err := middleware.ValidateRequestURL(r, a.options.Load().SharedKey) + if err != nil { + return httputil.NewError(http.StatusBadRequest, err) + } + } + return next(w, r) + }) +} + +// requireValidSignature validates the pomerium_signature. +func (a *Authenticate) requireValidSignature(next httputil.HandlerFunc) http.Handler { + return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + err := middleware.ValidateRequestURL(r, a.options.Load().SharedKey) + if err != nil { + return err + } + return next(w, r) + }) +} diff --git a/internal/frontend/assets/html/userInfo.html b/internal/frontend/assets/html/userInfo.html index 58ca42b63..17a4d84b2 100644 --- a/internal/frontend/assets/html/userInfo.html +++ b/internal/frontend/assets/html/userInfo.html @@ -15,7 +15,6 @@
{{.csrfField}} -
diff --git a/proxy/handlers.go b/proxy/handlers.go index 1a925d780..23f6fc33c 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -87,12 +87,13 @@ func (p *Proxy) userInfo(w http.ResponseWriter, r *http.Request) { redirectURL = ref } - url := state.authenticateDashboardURL.ResolveReference(&url.URL{ + uri := state.authenticateDashboardURL.ResolveReference(&url.URL{ RawQuery: url.Values{ urlutil.QueryRedirectURI: {redirectURL}, }.Encode(), }) - httputil.Redirect(w, r, url.String(), http.StatusFound) + uri = urlutil.NewSignedURL(state.sharedKey, uri).Sign() + httputil.Redirect(w, r, uri.String(), http.StatusFound) } // Callback handles the result of a successful call to the authenticate service diff --git a/proxy/state.go b/proxy/state.go index 927565499..4da0de393 100644 --- a/proxy/state.go +++ b/proxy/state.go @@ -60,7 +60,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) { if err != nil { return nil, err } - state.authenticateDashboardURL = state.authenticateURL.ResolveReference(&url.URL{Path: dashboardPath}) + state.authenticateDashboardURL = state.authenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/"}) state.authenticateSigninURL = state.authenticateURL.ResolveReference(&url.URL{Path: signinURL}) state.authenticateRefreshURL = state.authenticateURL.ResolveReference(&url.URL{Path: refreshURL})