mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 07:37:33 +02:00
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:
parent
c96ff595e5
commit
0635c838c9
6 changed files with 117 additions and 18 deletions
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
34
authenticate/middleware.go
Normal file
34
authenticate/middleware.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue