diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 5f68d81d9..db0a0b5d1 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) @@ -479,8 +486,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": a.getSignOutURL(r), } return a.templates.ExecuteTemplate(w, "userInfo.html", input) } @@ -575,3 +581,15 @@ func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter, return rawIDToken } + +func (a *Authenticate) getSignOutURL(r *http.Request) *url.URL { + uri := a.options.Load().AuthenticateURL.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() +} diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index aa8b166d1..afcd0a69b 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -598,12 +598,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) { @@ -614,8 +638,13 @@ func TestAuthenticate_userInfo(t *testing.T) { if err != nil { t.Fatal(err) } + o := config.NewAtomicOptions() + o.Store(&config.Options{ + AuthenticateURL: mustParseURL("https://authenticate.localhost.pomerium.io"), + SharedKey: "SHARED KEY", + }) a := &Authenticate{ - options: config.NewAtomicOptions(), + options: o, state: newAtomicAuthenticateState(&authenticateState{ sessionStore: tt.sessionStore, encryptedEncoder: signer, @@ -645,8 +674,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) @@ -657,7 +685,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) } @@ -785,3 +813,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 5f4a8d61f..1d0b21d3f 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -82,12 +82,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 02ff78d31..9b3c87dba 100644 --- a/proxy/state.go +++ b/proxy/state.go @@ -58,7 +58,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) { // errors checked in ValidateOptions state.authenticateURL, _ = urlutil.DeepCopy(cfg.Options.AuthenticateURL) - 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})