authenticate: validate signature on /.pomerium, /.pomerium/sign_in and /.pomerium/sign_out (#2048)

Co-authored-by: Caleb Doxsey <cdoxsey@pomerium.com>
This commit is contained in:
Travis Groth 2021-04-01 10:04:16 -04:00 committed by GitHub
parent c96ff595e5
commit 0635c838c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 18 deletions

View file

@ -70,7 +70,12 @@ func (a *Authenticate) Mount(r *mux.Router) {
// Identity Provider (IdP) endpoints // Identity Provider (IdP) endpoints
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet) 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{ c := cors.New(cors.Options{
AllowOriginRequestFunc: func(r *http.Request, _ string) bool { AllowOriginRequestFunc: func(r *http.Request, _ string) bool {
state := a.state.Load() state := a.state.Load()
@ -83,13 +88,15 @@ func (a *Authenticate) Mount(r *mux.Router) {
AllowCredentials: true, AllowCredentials: true,
AllowedHeaders: []string{"*"}, AllowedHeaders: []string{"*"},
}) })
v.Use(c.Handler) sr.Use(c.Handler)
v.Use(a.RetrieveSession) sr.Use(a.RetrieveSession)
v.Use(a.VerifySession) sr.Use(a.VerifySession)
v.Path("/").Handler(httputil.HandlerFunc(a.userInfo)) sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
v.Path("/sign_in").Handler(httputil.HandlerFunc(a.SignIn)) sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
v.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
}
func (a *Authenticate) mountWellKnown(r *mux.Router) {
wk := r.PathPrefix("/.well-known/pomerium").Subrouter() wk := r.PathPrefix("/.well-known/pomerium").Subrouter()
wk.Path("/jwks.json").Handler(httputil.HandlerFunc(a.jwks)).Methods(http.MethodGet) wk.Path("/jwks.json").Handler(httputil.HandlerFunc(a.jwks)).Methods(http.MethodGet)
wk.Path("/").Handler(httputil.HandlerFunc(a.wellKnown)).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) groups = append(groups, pbDirectoryGroup)
} }
signoutURL, err := a.getSignOutURL(r)
if err != nil {
return fmt.Errorf("invalid signout url: %w", err)
}
input := map[string]interface{}{ input := map[string]interface{}{
"State": s, // local session state (cookie, header, etc) "State": s, // local session state (cookie, header, etc)
"Session": pbSession, // current access, refresh, id token, & impersonation state "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 "DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
"DirectoryGroups": groups, // user's groups inferred from idp directory "DirectoryGroups": groups, // user's groups inferred from idp directory
"csrfField": csrf.TemplateField(r), "csrfField": csrf.TemplateField(r),
"RedirectURL": r.URL.Query().Get(urlutil.QueryRedirectURI), "SignOutURL": signoutURL,
"SignOutURL": "/.pomerium/sign_out",
} }
return a.templates.ExecuteTemplate(w, "userInfo.html", input) return a.templates.ExecuteTemplate(w, "userInfo.html", input)
} }
@ -579,3 +591,20 @@ func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter,
return rawIDToken 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
}

View file

@ -597,12 +597,36 @@ func TestAuthenticate_userInfo(t *testing.T) {
pbNow, _ := ptypes.TimestampProto(now) pbNow, _ := ptypes.TimestampProto(now)
tests := []struct { tests := []struct {
name string name string
url *url.URL
method string method string
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
wantCode int wantCode int
wantBody string 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -613,8 +637,13 @@ func TestAuthenticate_userInfo(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
o := config.NewAtomicOptions()
o.Store(&config.Options{
AuthenticateURLString: "https://authenticate.localhost.pomerium.io",
SharedKey: "SHARED KEY",
})
a := &Authenticate{ a := &Authenticate{
options: config.NewAtomicOptions(), options: o,
state: newAtomicAuthenticateState(&authenticateState{ state: newAtomicAuthenticateState(&authenticateState{
sessionStore: tt.sessionStore, sessionStore: tt.sessionStore,
encryptedEncoder: signer, encryptedEncoder: signer,
@ -644,8 +673,7 @@ func TestAuthenticate_userInfo(t *testing.T) {
}), }),
templates: template.Must(frontend.NewTemplates()), templates: template.Must(frontend.NewTemplates()),
} }
u, _ := url.Parse("/") r := httptest.NewRequest(tt.method, tt.url.String(), nil)
r := httptest.NewRequest(tt.method, u.String(), nil)
state, err := tt.sessionStore.LoadSession(r) state, err := tt.sessionStore.LoadSession(r)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -656,7 +684,7 @@ func TestAuthenticate_userInfo(t *testing.T) {
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder() w := httptest.NewRecorder()
httputil.HandlerFunc(a.userInfo).ServeHTTP(w, r) a.requireValidSignatureOnRedirect(a.userInfo).ServeHTTP(w, r)
if status := w.Code; status != tt.wantCode { if status := w.Code; status != tt.wantCode {
t.Errorf("handler returned wrong status code: got %v want %v", 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
}

View file

@ -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)
})
}

View file

@ -15,7 +15,6 @@
<span> <span>
<form action="{{.SignOutURL}}" method="post"> <form action="{{.SignOutURL}}" method="post">
{{.csrfField}} {{.csrfField}}
<input type="hidden" name="pomerium_redirect_uri" value="{{.RedirectURL}}">
<input class="button" type="submit" value="Logout"/> <input class="button" type="submit" value="Logout"/>
</form> </form>
</span> </span>

View file

@ -87,12 +87,13 @@ func (p *Proxy) userInfo(w http.ResponseWriter, r *http.Request) {
redirectURL = ref redirectURL = ref
} }
url := state.authenticateDashboardURL.ResolveReference(&url.URL{ uri := state.authenticateDashboardURL.ResolveReference(&url.URL{
RawQuery: url.Values{ RawQuery: url.Values{
urlutil.QueryRedirectURI: {redirectURL}, urlutil.QueryRedirectURI: {redirectURL},
}.Encode(), }.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 // Callback handles the result of a successful call to the authenticate service

View file

@ -60,7 +60,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
if err != nil { if err != nil {
return nil, err 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.authenticateSigninURL = state.authenticateURL.ResolveReference(&url.URL{Path: signinURL})
state.authenticateRefreshURL = state.authenticateURL.ResolveReference(&url.URL{Path: refreshURL}) state.authenticateRefreshURL = state.authenticateURL.ResolveReference(&url.URL{Path: refreshURL})