diff --git a/Makefile b/Makefile
index a199015be..d78fbcb53 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ CTIMEVAR=-X $(PKG)/internal/version.GitCommit=$(GITCOMMIT) \
-X $(PKG)/internal/version.ProjectURL=$(PKG)
GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)"
GOOSARCHES = linux/amd64 darwin/amd64 windows/amd64
-GOLANGCI_VERSION = v1.21.0 # .... for some reason v1.18.0 misses?
+GOLANGCI_VERSION = v1.21.0
.PHONY: all
all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, lint, test, and vet.
diff --git a/authenticate/handlers.go b/authenticate/handlers.go
index 143a8e7b6..6111afa4d 100644
--- a/authenticate/handlers.go
+++ b/authenticate/handlers.go
@@ -74,20 +74,28 @@ func (a *Authenticate) Handler() http.Handler {
// session state is attached to the users's request context.
func (a *Authenticate) VerifySession(next http.Handler) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
- state, _, err := sessions.FromContext(r.Context())
- if errors.Is(err, sessions.ErrExpired) {
- ctx, err := a.refresh(w, r, state)
+ ctx := r.Context()
+ jwt, err := sessions.FromContext(ctx)
+ if err != nil {
+ log.FromRequest(r).Info().Err(err).Msg("authenticate: session load error")
+ return a.reauthenticateOrFail(w, r, err)
+ }
+ var s sessions.State
+ if err := a.encryptedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+
+ if err := s.Verify(r.Host); errors.Is(err, sessions.ErrExpired) {
+ ctx, err = a.refresh(w, r, &s)
if err != nil {
log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session, refresh")
return a.reauthenticateOrFail(w, r, err)
}
- next.ServeHTTP(w, r.WithContext(ctx))
- return nil
} else if err != nil {
log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session")
return a.reauthenticateOrFail(w, r, err)
}
- next.ServeHTTP(w, r)
+ next.ServeHTTP(w, r.WithContext(ctx))
return nil
})
}
@@ -102,8 +110,13 @@ func (a *Authenticate) refresh(w http.ResponseWriter, r *http.Request, s *sessio
if err := a.sessionStore.SaveSession(w, r, newSession); err != nil {
return nil, fmt.Errorf("authenticate: refresh save failed: %w", err)
}
+ newSession = newSession.NewSession(s.Issuer, s.Audience)
+ encSession, err := a.encryptedEncoder.Marshal(newSession)
+ if err != nil {
+ return nil, err
+ }
// return the new session and add it to the current request context
- return sessions.NewContext(ctx, newSession, "", err), nil
+ return sessions.NewContext(ctx, string(encSession), err), nil
}
// RobotsTxt handles the /robots.txt route.
@@ -142,18 +155,24 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
jwtAudience = append(jwtAudience, fwdAuth)
}
- s, _, err := sessions.FromContext(r.Context())
+ jwt, err := sessions.FromContext(r.Context())
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
-
+ var s sessions.State
+ if err := a.encryptedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+ if err := s.Verify(r.Host); err != nil && !errors.Is(err, sessions.ErrExpired) {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
// user impersonation
if impersonate := r.FormValue(urlutil.QueryImpersonateAction); impersonate != "" {
s.SetImpersonation(r.FormValue(urlutil.QueryImpersonateEmail), r.FormValue(urlutil.QueryImpersonateGroups))
}
// re-persist the session, useful when session was evicted from session
- if err := a.sessionStore.SaveSession(w, r, s); err != nil {
+ if err := a.sessionStore.SaveSession(w, r, &s); err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
@@ -197,12 +216,17 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
// SignOut signs the user out and attempts to revoke the user's identity session
// Handles both GET and POST.
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
- session, _, err := sessions.FromContext(r.Context())
+ jwt, err := sessions.FromContext(r.Context())
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
+ var s sessions.State
+ if err := a.encryptedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+
a.sessionStore.ClearSession(w, r)
- err = a.provider.Revoke(r.Context(), session.AccessToken)
+ err = a.provider.Revoke(r.Context(), s.AccessToken)
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
@@ -318,11 +342,19 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
// tokens and state with the identity provider. If successful, a new signed JWT
// and refresh token (`refresh_token`) are returned as JSON
func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error {
- s, _, err := sessions.FromContext(r.Context())
+ jwt, err := sessions.FromContext(r.Context())
+ if err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+ var s sessions.State
+ if err := a.encryptedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+ err = s.Verify(r.Host)
if err != nil && !errors.Is(err, sessions.ErrExpired) {
return httputil.NewError(http.StatusBadRequest, err)
}
- newSession, err := a.provider.Refresh(r.Context(), s)
+ newSession, err := a.provider.Refresh(r.Context(), &s)
if err != nil {
return err
}
@@ -359,12 +391,19 @@ func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error
// middleware. This handler is responsible for creating a new route scoped
// session and returning it.
func (a *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) error {
- s, _, err := sessions.FromContext(r.Context())
+ jwt, err := sessions.FromContext(r.Context())
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
-
- routeSession := s.NewSession(r.Host, []string{r.Host, r.FormValue(urlutil.QueryAudience)})
+ var s sessions.State
+ if err := a.encryptedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+ if err := s.Verify(r.Host); err != nil && !errors.Is(err, sessions.ErrExpired) {
+ return httputil.NewError(http.StatusBadRequest, err)
+ }
+ aud := strings.Split(r.FormValue(urlutil.QueryAudience), ",")
+ routeSession := s.NewSession(r.Host, aud)
routeSession.AccessTokenID = s.AccessTokenID
signedJWT, err := a.sharedEncoder.Marshal(routeSession.RouteSession())
diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go
index 53c7d49a4..4a995371d 100644
--- a/authenticate/handlers_test.go
+++ b/authenticate/handlers_test.go
@@ -13,6 +13,7 @@ import (
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding"
+ "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/encoding/mock"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil"
@@ -151,9 +152,9 @@ func TestAuthenticate_SignIn(t *testing.T) {
uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(http.MethodGet, uri.String(), nil)
r.Header.Set("Accept", "application/json")
- state, _, err := tt.session.LoadSession(r)
+ state, err := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", err)
+ ctx = sessions.NewContext(ctx, state, err)
r = r.WithContext(ctx)
w := httptest.NewRecorder()
@@ -187,17 +188,19 @@ func TestAuthenticate_SignOut(t *testing.T) {
wantCode int
wantBody string
}{
- {"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &mstore.Store{Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusFound, ""},
- {"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: OH NO\"}\n"},
- {"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: error\"}\n"},
- {"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &mstore.Store{Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: corp.pomerium.io/ url does contain a valid scheme\"}\n"},
+ {"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusFound, ""},
+ {"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: OH NO\"}\n"},
+ {"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: error\"}\n"},
+ {"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: corp.pomerium.io/ url does contain a valid scheme\"}\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+
a := &Authenticate{
- sessionStore: tt.sessionStore,
- provider: tt.provider,
- templates: template.Must(frontend.NewTemplates()),
+ sessionStore: tt.sessionStore,
+ provider: tt.provider,
+ encryptedEncoder: mock.Encoder{},
+ templates: template.Must(frontend.NewTemplates()),
}
u, _ := url.Parse("/sign_out")
params, _ := url.ParseQuery(u.RawQuery)
@@ -206,9 +209,12 @@ func TestAuthenticate_SignOut(t *testing.T) {
params.Add(urlutil.QueryRedirectURI, tt.redirectURL)
u.RawQuery = params.Encode()
r := httptest.NewRequest(tt.method, u.String(), nil)
- state, _, _ := tt.sessionStore.LoadSession(r)
+ state, err := tt.sessionStore.LoadSession(r)
+ if err != nil {
+ t.Fatal(err)
+ }
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
@@ -329,7 +335,7 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) {
}{
{"good", nil, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, nil, identity.MockProvider{RefreshResponse: sessions.State{AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Minute)}}}, http.StatusOK},
{"invalid session", nil, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, errors.New("hi"), identity.MockProvider{}, http.StatusFound},
- {"good refresh expired", nil, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{RefreshResponse: sessions.State{AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Minute)}}}, http.StatusOK},
+ {"good refresh expired", nil, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, nil, identity.MockProvider{RefreshResponse: sessions.State{AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Minute)}}}, http.StatusOK},
{"expired,refresh error", nil, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{RefreshError: errors.New("error")}, http.StatusFound},
{"expired,save error", nil, &mstore.Store{SaveError: errors.New("error"), Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{RefreshResponse: sessions.State{AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Minute)}}}, http.StatusFound},
{"expired XHR,refresh error", map[string]string{"X-Requested-With": "XmlHttpRequest"}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{RefreshError: errors.New("error")}, http.StatusUnauthorized},
@@ -340,18 +346,26 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ signer, err := jws.NewHS256Signer(nil, "mock")
+ if err != nil {
+ t.Fatal(err)
+ }
a := Authenticate{
- sharedKey: cryptutil.NewBase64Key(),
- cookieSecret: cryptutil.NewKey(),
- RedirectURL: uriParseHelper("https://authenticate.corp.beyondperimeter.com"),
- sessionStore: tt.session,
- provider: tt.provider,
- cookieCipher: aead,
+ sharedKey: cryptutil.NewBase64Key(),
+ cookieSecret: cryptutil.NewKey(),
+ RedirectURL: uriParseHelper("https://authenticate.corp.beyondperimeter.com"),
+ sessionStore: tt.session,
+ provider: tt.provider,
+ cookieCipher: aead,
+ encryptedEncoder: signer,
}
r := httptest.NewRequest("GET", "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, err := tt.session.LoadSession(r)
+ if err != nil {
+ t.Fatal(err)
+ }
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
@@ -408,9 +422,9 @@ func TestAuthenticate_RefreshAPI(t *testing.T) {
cookieCipher: aead,
}
r := httptest.NewRequest("GET", "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, _ := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
@@ -459,9 +473,9 @@ func TestAuthenticate_Refresh(t *testing.T) {
cookieCipher: aead,
}
r := httptest.NewRequest("GET", "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, _ := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
diff --git a/authorize/authorize.go b/authorize/authorize.go
index ca2f24df1..fe43e207a 100644
--- a/authorize/authorize.go
+++ b/authorize/authorize.go
@@ -4,8 +4,11 @@ package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"context"
+ "encoding/base64"
"fmt"
+ "gopkg.in/square/go-jose.v2"
+
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/authorize/evaluator/opa"
"github.com/pomerium/pomerium/config"
@@ -48,12 +51,37 @@ func newPolicyEvaluator(opts *config.Options) (evaluator.Evaluator, error) {
ctx := context.Background()
ctx, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator")
defer span.End()
+ var jwk jose.JSONWebKey
+ if opts.SigningKey == "" {
+ key, err := cryptutil.NewSigningKey()
+ if err != nil {
+ return nil, fmt.Errorf("authorize: couldn't generate signing key: %w", err)
+ }
+ jwk.Key = key
+ pubKeyBytes, err := cryptutil.EncodePublicKey(&key.PublicKey)
+ if err != nil {
+ return nil, fmt.Errorf("authorize: encode public key: %w", err)
+ }
+ log.Info().Interface("PublicKey", pubKeyBytes).Msg("authorize: ecdsa public key")
+ } else {
+ decodedCert, err := base64.StdEncoding.DecodeString(opts.SigningKey)
+ if err != nil {
+ return nil, fmt.Errorf("authorize: failed to decode certificate cert %v: %w", decodedCert, err)
+ }
+ keyBytes, err := cryptutil.DecodePrivateKey((decodedCert))
+ if err != nil {
+ return nil, fmt.Errorf("authorize: couldn't generate signing key: %w", err)
+ }
+ jwk.Key = keyBytes
+ }
data := map[string]interface{}{
"shared_key": opts.SharedKey,
"route_policies": opts.Policies,
"admins": opts.Administrators,
+ "signing_key": jwk,
}
+
return opa.New(ctx, &opa.Options{Data: data})
}
diff --git a/authorize/authorize_test.go b/authorize/authorize_test.go
index b701abddb..c8895169a 100644
--- a/authorize/authorize_test.go
+++ b/authorize/authorize_test.go
@@ -3,8 +3,6 @@ package authorize
import (
"testing"
- "github.com/pomerium/pomerium/authorize/evaluator"
- "github.com/pomerium/pomerium/authorize/evaluator/mock"
"github.com/pomerium/pomerium/config"
)
@@ -50,25 +48,3 @@ func testPolicies(t *testing.T) []config.Policy {
}
return policies
}
-
-func TestAuthorize_UpdateOptions(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- pe evaluator.Evaluator
- opts config.Options
- wantErr bool
- }{
- {"good", &mock.PolicyEvaluator{}, config.Options{}, false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- a := &Authorize{
- pe: tt.pe,
- }
- if err := a.UpdateOptions(tt.opts); (err != nil) != tt.wantErr {
- t.Errorf("Authorize.UpdateOptions() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go
index 398692be7..4a3122fec 100644
--- a/authorize/evaluator/evaluator.go
+++ b/authorize/evaluator/evaluator.go
@@ -1,15 +1,18 @@
+//go:generate mockgen -destination mock_evaluator/mock.go github.com/pomerium/pomerium/authorize/evaluator Evaluator
+
// Package evaluator defines a Evaluator interfaces that can be implemented by
// a policy evaluator framework.
package evaluator
import (
"context"
+
+ pb "github.com/pomerium/pomerium/internal/grpc/authorize"
)
// Evaluator specifies the interface for a policy engine.
type Evaluator interface {
- IsAuthorized(ctx context.Context, input interface{}) (bool, error)
- IsAdmin(ctx context.Context, input interface{}) (bool, error)
+ IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error)
PutData(ctx context.Context, data map[string]interface{}) error
}
@@ -45,7 +48,5 @@ type Request struct {
// Device context
//
- // todo(bdd):
- // Use the peer TLS certificate as the basis for binding device
- // identity with a request context !
+ // todo(bdd): Use the peer TLS certificate to bind device state with a request
}
diff --git a/authorize/evaluator/mock/mock.go b/authorize/evaluator/mock/mock.go
deleted file mode 100644
index f17ae6943..000000000
--- a/authorize/evaluator/mock/mock.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Package mock implements the policy evaluator interface to make authorization
-// decisions.
-package mock
-
-import (
- "context"
-
- "github.com/pomerium/pomerium/authorize/evaluator"
-)
-
-var _ evaluator.Evaluator = &PolicyEvaluator{}
-
-// PolicyEvaluator is the mock implementation of Evaluator
-type PolicyEvaluator struct {
- IsAuthorizedResponse bool
- IsAuthorizedErr error
- IsAdminResponse bool
- IsAdminErr error
- PutDataErr error
-}
-
-// IsAuthorized is the mock implementation of IsAuthorized
-func (pe PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) {
- return pe.IsAuthorizedResponse, pe.IsAuthorizedErr
-}
-
-// IsAdmin is the mock implementation of IsAdmin
-func (pe PolicyEvaluator) IsAdmin(ctx context.Context, input interface{}) (bool, error) {
- return pe.IsAdminResponse, pe.IsAdminErr
-}
-
-// PutData is the mock implementation of PutData
-func (pe PolicyEvaluator) PutData(ctx context.Context, data map[string]interface{}) error {
- return pe.PutDataErr
-}
diff --git a/authorize/evaluator/mock_evaluator/mock.go b/authorize/evaluator/mock_evaluator/mock.go
new file mode 100644
index 000000000..05990e712
--- /dev/null
+++ b/authorize/evaluator/mock_evaluator/mock.go
@@ -0,0 +1,64 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/pomerium/pomerium/authorize/evaluator (interfaces: Evaluator)
+
+// Package mock_evaluator is a generated GoMock package.
+package mock_evaluator
+
+import (
+ context "context"
+ gomock "github.com/golang/mock/gomock"
+ authorize "github.com/pomerium/pomerium/internal/grpc/authorize"
+ reflect "reflect"
+)
+
+// MockEvaluator is a mock of Evaluator interface
+type MockEvaluator struct {
+ ctrl *gomock.Controller
+ recorder *MockEvaluatorMockRecorder
+}
+
+// MockEvaluatorMockRecorder is the mock recorder for MockEvaluator
+type MockEvaluatorMockRecorder struct {
+ mock *MockEvaluator
+}
+
+// NewMockEvaluator creates a new mock instance
+func NewMockEvaluator(ctrl *gomock.Controller) *MockEvaluator {
+ mock := &MockEvaluator{ctrl: ctrl}
+ mock.recorder = &MockEvaluatorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockEvaluator) EXPECT() *MockEvaluatorMockRecorder {
+ return m.recorder
+}
+
+// IsAuthorized mocks base method
+func (m *MockEvaluator) IsAuthorized(arg0 context.Context, arg1 interface{}) (*authorize.IsAuthorizedReply, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsAuthorized", arg0, arg1)
+ ret0, _ := ret[0].(*authorize.IsAuthorizedReply)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// IsAuthorized indicates an expected call of IsAuthorized
+func (mr *MockEvaluatorMockRecorder) IsAuthorized(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthorized", reflect.TypeOf((*MockEvaluator)(nil).IsAuthorized), arg0, arg1)
+}
+
+// PutData mocks base method
+func (m *MockEvaluator) PutData(arg0 context.Context, arg1 map[string]interface{}) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PutData", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// PutData indicates an expected call of PutData
+func (mr *MockEvaluatorMockRecorder) PutData(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutData", reflect.TypeOf((*MockEvaluator)(nil).PutData), arg0, arg1)
+}
diff --git a/authorize/evaluator/opa/opa.go b/authorize/evaluator/opa/opa.go
index 9bf148d67..afc27c737 100644
--- a/authorize/evaluator/opa/opa.go
+++ b/authorize/evaluator/opa/opa.go
@@ -1,3 +1,5 @@
+//go:generate statik -src=./policy -include=*.rego -ns rego -p policy
+
// Package opa implements the policy evaluator interface to make authorization
// decisions.
package opa
@@ -6,15 +8,22 @@ import (
"context"
"errors"
"fmt"
+ "io/ioutil"
"sync"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/storage/inmem"
+ "github.com/rakyll/statik/fs"
+
"github.com/pomerium/pomerium/authorize/evaluator"
+ _ "github.com/pomerium/pomerium/authorize/evaluator/opa/policy" // load static assets
+ pb "github.com/pomerium/pomerium/internal/grpc/authorize"
"github.com/pomerium/pomerium/internal/telemetry/trace"
)
+const statikNamespace = "rego"
+
var _ evaluator.Evaluator = &PolicyEvaluator{}
// PolicyEvaluator implements the evaluator interface using the open policy
@@ -28,7 +37,6 @@ type PolicyEvaluator struct {
mu sync.RWMutex
store storage.Store
isAuthorized rego.PreparedEvalQuery
- isAdmin rego.PreparedEvalQuery
}
// Options represent OPA's evaluator configurations.
@@ -37,10 +45,6 @@ type Options struct {
// apply custom authorization policy.
// Defaults to authorization policy defined in config.yaml's policy.
AuthorizationPolicy string
- // PAMPolicy accepts custom rego code which can be used to
- // apply custom privileged access management policy.
- // Defaults to users whose emails match those defined in config.yaml.
- PAMPolicy string
// Data maps data that will be bound and
Data map[string]interface{}
}
@@ -49,19 +53,21 @@ type Options struct {
func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) {
var pe PolicyEvaluator
pe.store = inmem.New()
+
if opts.Data == nil {
return nil, errors.New("opa: cannot create new evaluator without data")
}
if opts.AuthorizationPolicy == "" {
- opts.AuthorizationPolicy = defaultAuthorization
- }
- if opts.PAMPolicy == "" {
- opts.PAMPolicy = defaultPAM
+ b, err := readPolicy("/authz.rego")
+ if err != nil {
+ return nil, err
+ }
+ opts.AuthorizationPolicy = string(b)
}
if err := pe.PutData(ctx, opts.Data); err != nil {
return nil, err
}
- if err := pe.UpdatePolicy(ctx, opts.AuthorizationPolicy, opts.PAMPolicy); err != nil {
+ if err := pe.UpdatePolicy(ctx, opts.AuthorizationPolicy); err != nil {
return nil, err
}
return &pe, nil
@@ -69,7 +75,7 @@ func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) {
// UpdatePolicy takes authorization and privilege access management rego code
// as an input and updates the prepared policy evaluator.
-func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz, pam string) error {
+func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz string) error {
ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.UpdatePolicy")
defer span.End()
@@ -80,38 +86,20 @@ func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz, pam string)
r := rego.New(
rego.Store(pe.store),
rego.Module("pomerium.authz", authz),
- // rego.Query("data.pomerium.authz"),
- rego.Query("result = data.pomerium.authz.allow"),
+ rego.Query("result = data.pomerium.authz"),
)
pe.isAuthorized, err = r.PrepareForEval(ctx)
if err != nil {
return fmt.Errorf("opa: prepare policy: %w", err)
}
- r = rego.New(
- rego.Store(pe.store),
- rego.Module("pomerium.pam", pam),
- rego.Query("result = data.pomerium.pam.is_admin"),
- )
- pe.isAdmin, err = r.PrepareForEval(ctx)
- if err != nil {
- return fmt.Errorf("opa: prepare policy: %w", err)
- }
return nil
}
// IsAuthorized determines if a given request input is authorized.
-func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) {
- ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.PutData")
+func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error) {
+ ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAuthorized")
defer span.End()
return pe.runBoolQuery(ctx, input, pe.isAuthorized)
-
-}
-
-// IsAdmin determines if a given input user has super user privleges.
-func (pe *PolicyEvaluator) IsAdmin(ctx context.Context, input interface{}) (bool, error) {
- ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAdmin")
- defer span.End()
- return pe.runBoolQuery(ctx, input, pe.isAdmin)
}
// PutData adds (or replaces if the mapping key is the same) contextual data
@@ -136,20 +124,78 @@ func (pe *PolicyEvaluator) PutData(ctx context.Context, data map[string]interfac
return nil
}
-func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, input interface{}, q rego.PreparedEvalQuery) (bool, error) {
+func decisionFromInterface(i interface{}) (*pb.IsAuthorizedReply, error) {
+ var d pb.IsAuthorizedReply
+ var ok bool
+ m, ok := i.(map[string]interface{})
+ if !ok {
+ return nil, errors.New("interface must be a map")
+ }
+ if d.Allow, ok = m["allow"].(bool); !ok {
+ return nil, errors.New("allow should be bool")
+ }
+ if d.SessionExpired, ok = m["expired"].(bool); !ok {
+ return nil, errors.New("expired should be bool")
+ }
+
+ switch v := m["deny"].(type) {
+ case []interface{}:
+ for _, cause := range v {
+ if c, ok := cause.(string); ok {
+ d.DenyReasons = append(d.DenyReasons, c)
+ }
+ }
+ case string:
+ d.DenyReasons = []string{v}
+ }
+
+ if v, ok := m["user"].(string); ok {
+ d.User = v
+ }
+
+ if v, ok := m["email"].(string); ok {
+ d.Email = v
+ }
+
+ switch v := m["groups"].(type) {
+ case []interface{}:
+ for _, cause := range v {
+ if c, ok := cause.(string); ok {
+ d.Groups = append(d.Groups, c)
+ }
+ }
+ case string:
+ d.Groups = []string{v}
+ }
+
+ if v, ok := m["signed_jwt"].(string); ok {
+ d.SignedJwt = v
+ }
+ return &d, nil
+}
+
+func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, input interface{}, q rego.PreparedEvalQuery) (*pb.IsAuthorizedReply, error) {
pe.mu.RLock()
defer pe.mu.RUnlock()
- rs, err := q.Eval(
- ctx,
- rego.EvalInput(input),
- )
+ rs, err := q.Eval(ctx, rego.EvalInput(input))
if err != nil {
- return false, fmt.Errorf("opa: eval query: %w", err)
- } else if len(rs) != 1 {
- return false, fmt.Errorf("opa: eval result set: %v, expected len 1", rs)
- } else if result, ok := rs[0].Bindings["result"].(bool); !ok {
- return false, fmt.Errorf("opa: expected bool, got: %v", rs)
- } else {
- return result, nil
+ return nil, fmt.Errorf("eval query: %w", err)
+ } else if len(rs) == 0 {
+ return nil, fmt.Errorf("empty eval result set %v", rs)
}
+ bindings := rs[0].Bindings.WithoutWildcards()["result"]
+ return decisionFromInterface(bindings)
+}
+
+func readPolicy(fn string) ([]byte, error) {
+ statikFS, err := fs.NewWithNamespace(statikNamespace)
+ if err != nil {
+ return nil, err
+ }
+ r, err := statikFS.Open(fn)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ return ioutil.ReadAll(r)
}
diff --git a/authorize/evaluator/opa/opa_test.go b/authorize/evaluator/opa/opa_test.go
index 4d45cca3a..7bf7566a0 100644
--- a/authorize/evaluator/opa/opa_test.go
+++ b/authorize/evaluator/opa/opa_test.go
@@ -100,8 +100,8 @@ func Test_Eval(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if got != tt.want {
- t.Errorf("pe.Eval() = %v, want %v", got, tt.want)
+ if got.GetAllow() != tt.want {
+ t.Errorf("pe.Eval() = %v, want %v", got.GetAllow(), tt.want)
}
})
}
diff --git a/authorize/evaluator/opa/policy/authz.rego b/authorize/evaluator/opa/policy/authz.rego
new file mode 100644
index 000000000..c19c2991d
--- /dev/null
+++ b/authorize/evaluator/opa/policy/authz.rego
@@ -0,0 +1,116 @@
+package pomerium.authz
+
+import data.route_policies
+import data.shared_key
+
+default allow = false
+
+# allow by email
+allow {
+ some route
+ input.host = route_policies[route].source
+ token.payload.email = route_policies[route].allowed_users[_]
+ token.valid
+ count(deny)==0
+}
+
+# allow group
+allow {
+ some route
+ input.host = route_policies[route].source
+ token.payload.groups[_] = route_policies[route].allowed_groups[_]
+ token.valid
+ count(deny)==0
+}
+
+# allow by impersonate email
+allow {
+ some route
+ input.host = route_policies[route].source
+ token.payload.impersonate_email = route_policies[route].allowed_users[_]
+ token.valid
+ count(deny)==0
+}
+
+# allow by impersonate group
+allow {
+ some route
+ input.host = route_policies[route].source
+ token.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_]
+ token.valid
+ count(deny)==0
+}
+
+# allow by domain
+allow {
+ some route
+ input.host = route_policies[route].source
+ allowed_user_domain(token.payload.email)
+ token.valid
+ count(deny)==0
+}
+
+# allow by impersonate domain
+allow {
+ some route
+ input.host = route_policies[route].source
+ allowed_user_domain(token.payload.impersonate_email)
+ token.valid
+ count(deny)==0
+}
+
+allowed_user_domain(email){
+ x := split(email, "@")
+ count(x)=2
+ x[1] == route_policies[route].allowed_domains[_]
+}
+
+
+default expired = false
+
+expired {
+ now_seconds:=time.now_ns()/1e9
+ [header, payload, _] := io.jwt.decode(input.user)
+ payload.exp < now_seconds
+}
+
+deny["token is expired (exp)"]{
+ now_seconds:=time.now_ns()/1e9
+ [header, payload, _] := io.jwt.decode(input.user)
+ payload.exp < now_seconds
+}
+
+
+# allow user is admin
+allow {
+ element_in_list(data.admins, token.payload.email)
+ token.valid
+ count(deny)==0
+ contains(input.url,".pomerium/admin")
+}
+
+
+# deny non-admin users from accesing admin routes
+deny["user is not admin"]{
+ not element_in_list(data.admins, token.payload.email)
+ contains(input.url,".pomerium/admin")
+}
+
+token = {"payload": payload, "valid": valid} {
+ [valid, header, payload] := io.jwt.decode_verify(
+ input.user, {
+ "secret": shared_key,
+ "aud": input.host,
+ }
+ )
+}
+
+user:=token.payload.user
+email:=token.payload.email
+groups:=token.payload.groups
+signed_jwt:=io.jwt.encode_sign({"alg": "ES256"}, token.payload, data.signing_key)
+
+
+element_in_list(list, elem) {
+ list[_] = elem
+}
\ No newline at end of file
diff --git a/authorize/evaluator/opa/policy/statik.go b/authorize/evaluator/opa/policy/statik.go
new file mode 100644
index 000000000..19d1a2ffb
--- /dev/null
+++ b/authorize/evaluator/opa/policy/statik.go
@@ -0,0 +1,16 @@
+// Code generated by statik. DO NOT EDIT.
+
+package policy
+
+import (
+ "github.com/rakyll/statik/fs"
+)
+
+
+const Rego = "rego" // static asset namespace
+
+func init() {
+ data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x84\xa6dP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00authz.regoUT\x05\x00\x01\xf9\x14`^\xc4V\xddn\xda0\x14\xbe\xf6y\x8a#\xef&\x91\xb2t\xad\xb4I\x8b\x16i7{\x82]\"\x14\xb9\xf1\x01\xdc&v\x14;\x05\x86x\xf7\xc9v \x94vj\xbb\x8d\xed\x06\xc89\xf6\xe7\xef\xc7\xb1\xe9D}/\x96\x84\x9di\xa9WC\x9b\x8b\xc1\xad~\x00\xa8\xb63\xbdC)\x9c\xc8{38\xaa:\xd3\xa8Z\x91}\xd4\xb2+\xd1\x93\xac\xeei\x0b i!\x86\xc6\xa1h\x1a\xb3\xc6\x12\x17\xa2\xb1\x04\xf0n,\xdcn\x91Z\xa1\x1a\x88\x8f;`\xd6\xb4\x84\x01\x1c\x98\xd2\xdd\xe0\xf2\x95\xb1\x0eK|\xbc\xe0,<\xcesk\x86\xbe&`\xce\xdc\x93\xce;\xb1m\x8c\x90y\xc0\xfc\xe5\x9c\xb0\x16\xc9j\xb0\xd4\xdbY5?\xcc~\x10\x8d\x92\xc0j3h\x97H\xd2\xdb\xb4,?\xc0~b\xbb\xec\xcd\xd0\xfde\xaa\x01\xd3\xb3x\x91\xeeq\xe4\xab\xf9\xdenQ\xb5\x1d\xf5\xd6h\xe1\xe8\"N\x9f\xe0W\x17r\xfdL\xc5%B8Uq\xe1@\xa4i\x85\xd2\xf8\xc7\x02N\xed\xac\"h\xf2\xccK\x90\xbe\x85\xdb\xa9\xcd\x11\xf2\x1f\xd0|\xb2\x83^\xa6\xfc\x1ch\x9c\xba\x03\xb6\xc1\xa2D\xdb5\xca\xc5Z\x86\xfc+O\x0f0\x9b\xb4\xbc\x01\xb6\x99]\xcf\xb1|)\xdf\x88\x1c\x02\xde\xc3t\x94\xd1\xa6S=\xc9\xe90;\x14v\xc0\xb4YW\x96j\xa3\xa5-J\xa7Z\xca}E\xdb$\xbd\xba\xa6\xcf\xc0f+\x12\x92\xfa\x0cG\xf5\x19Vs\xcfW\x99\xfcn\xedrI\xb5\x91\x94D\x7f\xbd\xba\x14\xd81\xceM\x87_\xf0d\x01O\xca\xbb2\xe3\xc1.T\xf6H-\xa1M\x97\xf2\xf9\x7f t\xdcN~\xb0\xa7$d{\xba\x8d\xa8\xa1\x96\xb4\xab\x94\xae\x1ae]\x12.\x8c0\xc6f\xf8\xf6\xfd\xcbj\xa3\x9d\x0f\xe9@\xb1o2\x9e\x1f\xee\xad\xab\x00\xcc\xd3\x91\x98\x9f\x85\xda\xe8\xf7\xa1\x1c\x18Z\\\xf4\xa6EQ\xd7d\x95^F\xb6q[\xd8\xd1\xdc\x83\x10m\\l\x8f\xbe:\xfc\x0d-\xaf\xa6\x1b#-q\xc7G\x08^L\x11\xf1`\x06/0|\xef\xbd\xaf\xb3\xf03\xc3\xb38\x9ffY=P\xaf\x16\xdb\x04\x18\x9bR\xcd<\x04c\xdcR\xdd\x93\xe3\x05NWx\x16\x1ab\xf0\xcbM\xef\xbd\xaf\xee\x81\x05\xaa\x1e\xa0(\x1f\xeb\xf55\x08\xa2\xcf;\xf1\x12\x8aG\xe7y/V\xc1\xaa\xa5&Y\xdd\xad]Q\x8e\xdcI\x07\xee\xbe\x93\xec\xb8h\x96\xbc@\xfe\xed\xfb\xcd\xc7O|\x7f\xe6u6\xfe QK\xad\xf4\xd2KH\x01\xe0<+\xff\x91\x85\x04S\xdc\x01\xa2\x7f\x8eg\xbe\xaf\xc1\xfeg\x00\x00\x00\xff\xffPK\x07\x08\xd4XXE|\x02\x00\x00\x03 \x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x84\xa6dP\xd4XXE|\x02\x00\x00\x03 \x00\x00\n\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00authz.regoUT\x05\x00\x01\xf9\x14`^PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00A\x00\x00\x00\xbd\x02\x00\x00\x00\x00"
+ fs.RegisterWithNamespace("rego", data)
+ }
+
\ No newline at end of file
diff --git a/authorize/evaluator/opa/rego.go b/authorize/evaluator/opa/rego.go
deleted file mode 100644
index 72aff493d..000000000
--- a/authorize/evaluator/opa/rego.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package opa
-
-//todo(bdd): embed source files directly, and setup tests.
-
-const defaultAuthorization = `
-package pomerium.authz
-import data.route_policies
-import data.shared_key
-
-default allow = false
-
-# allow by email
-allow {
- some route
- input.host = route_policies[route].source
- jwt.payload.email = route_policies[route].allowed_users[_]
- jwt.valid
-}
-
-# allow group
-allow {
- some route
- input.host = route_policies[route].source
- jwt.payload.groups[_] = route_policies[route].allowed_groups[_]
- jwt.valid
-}
-
-# allow by impersonate email
-allow {
- some route
- input.host = route_policies[route].source
- jwt.payload.impersonate_email = route_policies[route].allowed_users[_]
- jwt.valid
-
-}
-
-# allow by impersonate group
-allow {
- some route
- input.host = route_policies[route].source
- jwt.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_]
- jwt.valid
-}
-
-# allow by domain
-allow {
- some route
- input.host = route_policies[route].source
- x := split(jwt.payload.email, "@")
- count(x)=2
- x[1] = route_policies[route].allowed_domains[_]
- jwt.valid
-}
-
-# allow by impersonate domain
-allow {
- some route
- input.host = route_policies[route].source
- x := split(jwt.payload.impersonate_email, "@")
- count(x)=2
- x[1] == route_policies[route].allowed_domains[_]
- jwt.valid
-}
-
-
-jwt = {"payload": payload, "valid": valid} {
- [valid, header, payload] := io.jwt.decode_verify(
- input.user, {
- "secret": shared_key,
- "aud": input.host,
- }
- )
-}
-`
-
-const defaultPAM = `
-package pomerium.pam
-import data.admins
-import data.shared_key
-
-default is_admin = false
-is_admin{
- io.jwt.verify_hs256(input.user,shared_key)
- jwt.payload.email = admins[_]
-}
-jwt = {"payload": payload} {[header, payload, signature] := io.jwt.decode(input.user)}
-`
diff --git a/authorize/grpc.go b/authorize/grpc.go
index c88f60301..247da2175 100644
--- a/authorize/grpc.go
+++ b/authorize/grpc.go
@@ -11,7 +11,7 @@ import (
// IsAuthorized checks to see if a given user is authorized to make a request.
func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorizedRequest) (*authorize.IsAuthorizedReply, error) {
- _, span := trace.StartSpan(ctx, "authorize.grpc.Authorize")
+ ctx, span := trace.StartSpan(ctx, "authorize.grpc.IsAuthorized")
defer span.End()
req := &evaluator.Request{
@@ -23,25 +23,7 @@ func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorized
RemoteAddr: in.GetRequestRemoteAddr(),
URL: in.GetRequestUrl(),
}
- ok, err := a.pe.IsAuthorized(ctx, req)
- if err != nil {
- return nil, err
- }
- return &authorize.IsAuthorizedReply{IsValid: ok}, nil
-}
-
-// IsAdmin checks to see if a given user has super user privleges.
-func (a *Authorize) IsAdmin(ctx context.Context, in *authorize.IsAdminRequest) (*authorize.IsAdminReply, error) {
- _, span := trace.StartSpan(ctx, "authorize.grpc.IsAdmin")
- defer span.End()
- req := &evaluator.Request{
- User: in.GetUserToken(),
- }
- ok, err := a.pe.IsAdmin(ctx, req)
- if err != nil {
- return nil, err
- }
- return &authorize.IsAdminReply{IsValid: ok}, nil
+ return a.pe.IsAuthorized(ctx, req)
}
type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers
diff --git a/authorize/grpc_test.go b/authorize/grpc_test.go
index 6ca70d34b..33245d15c 100644
--- a/authorize/grpc_test.go
+++ b/authorize/grpc_test.go
@@ -1,74 +1,49 @@
-//go:generate protoc -I ../internal/grpc/authorize/ --go_out=plugins=grpc:../internal/grpc/authorize/ ../internal/grpc/authorize/authorize.proto
-
package authorize
import (
"context"
"errors"
- "reflect"
"testing"
- "github.com/pomerium/pomerium/authorize/evaluator"
- "github.com/pomerium/pomerium/authorize/evaluator/mock"
+ "github.com/golang/mock/gomock"
+ "github.com/google/go-cmp/cmp"
+ "github.com/pomerium/pomerium/authorize/evaluator/mock_evaluator"
"github.com/pomerium/pomerium/internal/grpc/authorize"
)
func TestAuthorize_IsAuthorized(t *testing.T) {
- t.Parallel()
+
tests := []struct {
name string
- pe evaluator.Evaluator
+ retDec *authorize.IsAuthorizedReply
+ retErr error
+ ctx context.Context
in *authorize.IsAuthorizedRequest
want *authorize.IsAuthorizedReply
wantErr bool
}{
- {"want false", &mock.PolicyEvaluator{}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: false}, false},
- {"want true", &mock.PolicyEvaluator{IsAuthorizedResponse: true}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: true}, false},
- {"want err", &mock.PolicyEvaluator{IsAuthorizedErr: errors.New("err")}, &authorize.IsAuthorizedRequest{}, nil, true},
+ {"good", &authorize.IsAuthorizedReply{}, nil, context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good"}, &authorize.IsAuthorizedReply{}, false},
+ {"error", &authorize.IsAuthorizedReply{}, errors.New("error"), context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good"}, &authorize.IsAuthorizedReply{}, true},
+ {"headers", &authorize.IsAuthorizedReply{}, nil, context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good", RequestHeaders: nil}, &authorize.IsAuthorizedReply{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+ pe := mock_evaluator.NewMockEvaluator(mockCtrl)
+ pe.EXPECT().IsAuthorized(gomock.Any(), gomock.Any()).Return(tt.retDec, tt.retErr).AnyTimes()
+
a := &Authorize{
- pe: tt.pe,
+ pe: pe,
}
- got, err := a.IsAuthorized(context.TODO(), tt.in)
+ got, err := a.IsAuthorized(tt.ctx, tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.IsAuthorized() error = %v, wantErr %v", err, tt.wantErr)
return
}
- if !reflect.DeepEqual(got, tt.want) {
+ if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("Authorize.IsAuthorized() = %v, want %v", got, tt.want)
}
})
}
}
-
-func TestAuthorize_IsAdmin(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- pe evaluator.Evaluator
- in *authorize.IsAdminRequest
- want *authorize.IsAdminReply
- wantErr bool
- }{
- {"want false", &mock.PolicyEvaluator{}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: false}, false},
- {"want true", &mock.PolicyEvaluator{IsAdminResponse: true}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: true}, false},
- {"want err", &mock.PolicyEvaluator{IsAdminErr: errors.New("err")}, &authorize.IsAdminRequest{}, nil, true},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- a := &Authorize{
- pe: tt.pe,
- }
- got, err := a.IsAdmin(context.TODO(), tt.in)
- if (err != nil) != tt.wantErr {
- t.Errorf("Authorize.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("Authorize.IsAdmin() = %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/cmd/pomerium/main.go b/cmd/pomerium/main.go
index 748fa53bc..d00513bfa 100644
--- a/cmd/pomerium/main.go
+++ b/cmd/pomerium/main.go
@@ -189,8 +189,8 @@ func newGlobalRouter(o *config.Options) *mux.Router {
Dur("duration", duration).
Int("size", size).
Int("status", status).
- Str("email", r.Header.Get(proxy.HeaderEmail)).
- Str("group", r.Header.Get(proxy.HeaderGroups)).
+ // Str("email", r.Header.Get(httputil.HeaderPomeriumEmail)).
+ // Str("group", r.Header.Get(httputil.HeaderPomeriumGroups)).
Str("method", r.Method).
Str("service", o.Services).
Str("host", r.Host).
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 624b6b5c1..c565b5dbe 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -116,8 +116,6 @@ module.exports = {
"reference/impersonation",
"reference/programmatic-access",
"reference/getting-users-identity",
- "reference/signed-headers",
- // "reference/examples",
"reference/production-deployment"
]
}
diff --git a/docs/configuration/readme.md b/docs/configuration/readme.md
index 3ca8cfba9..80d20feb4 100644
--- a/docs/configuration/readme.md
+++ b/docs/configuration/readme.md
@@ -200,7 +200,7 @@ Enable grpc DNS based round robin load balancing. This method uses DNS to resolv
#### GRPC Server Max Connection Age
-Set max connection age for GRPC servers. After this interval, servers ask clients to reconnect and perform any rediscovery for new/updated endpoints from DNS.
+Set max connection age for GRPC servers. After this interval, servers ask clients to reconnect and perform any rediscovery for new/updated endpoints from DNS.
See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for details
@@ -209,7 +209,6 @@ See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for deta
- Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string`
- Default: `5m`
-
#### GRPC Server Max Connection Age Grace
Additive period with `grpc_server_max_connection_age`, after which servers will force connections to close.
@@ -221,7 +220,6 @@ See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for deta
- Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string`
- Default: `5m`
-
### Cookie options
These settings control the Pomerium session cookies sent to users's
@@ -359,8 +357,8 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
| Config Key | Description | Required |
| :--------------- | :---------------------------------------------------------------- | -------- |
-| tracing_provider | The name of the tracing provider. (e.g. jaeger) | ✅ |
-| tracing_debug | Will disable [sampling](https://opencensus.io/tracing/sampling/). | ❌ |
+| tracing_provider | The name of the tracing provider. (e.g. jaeger) | ✅ |
+| tracing_debug | Will disable [sampling](https://opencensus.io/tracing/sampling/). | ❌ |
#### Jaeger
@@ -374,8 +372,8 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
| Config Key | Description | Required |
| :-------------------------------- | :------------------------------------------ | -------- |
-| tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅ |
-| tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅ |
+| tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅ |
+| tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅ |
#### Example
@@ -547,15 +545,6 @@ See also:
## Proxy Service
-### Signing Key
-
-- Environmental Variable: `SIGNING_KEY`
-- Config File Key: `signing_key`
-- Type: [base64 encoded] `string`
-- Optional
-
-Signing key is the base64 encoded key used to sign outbound requests. For more information see the [signed headers](./signed-headers.md) docs.
-
### Authenticate Service URL
- Environmental Variable: `AUTHENTICATE_SERVICE_URL`
@@ -870,6 +859,19 @@ When enabled, this option will pass the host header from the incoming request to
See [ProxyPreserveHost](http://httpd.apache.org/docs/2.0/mod/mod_proxy.html#proxypreservehost).
+## Authorize Service
+
+### Signing Key
+
+- Environmental Variable: `SIGNING_KEY`
+- Config File Key: `signing_key`
+- Type: [base64 encoded] `string`
+- Optional
+
+Signing key is the base64 encoded key used to sign outbound requests. For more information see the [signed headers](./signed-headers.md) docs.
+
+If no certificate is specified, one will be generated for you and the base64'd public key will be added to the logs.
+
[base64 encoded]: https://en.wikipedia.org/wiki/Base64
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
[identity provider]: ./identity-providers.md
diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md
index ec355d2d4..f12a7917e 100644
--- a/docs/docs/CHANGELOG.md
+++ b/docs/docs/CHANGELOG.md
@@ -2,12 +2,12 @@
## v0.6.0
-## New
+### New
- authenticate: support backend refresh @desimone [GH-438]
- cache: add cache service @desimone [GH-457]
-## Changed
+### Changed
- authorize: consolidate gRPC packages @desimone [GH-443]
- config: added yaml tags to all options struct fields @travisgroth [GH-394],[gh-397]
@@ -16,19 +16,19 @@
- config: validate that `shared_key` does not contain whitespace @travisgroth [GH-427]
- httputil : wrap handlers for additional context @desimone [GH-413]
-## Fixed
+### Fixed
- proxy: fix unauthorized redirect loop for forward auth @desimone [GH-448]
- proxy: fixed regression preventing policy reload [GH-396](https://github.com/pomerium/pomerium/pull/396)
-## Documentation
+### Documentation
- add cookie settings @danderson [GH-429]
- fix typo in forward auth nginx example @travisgroth [GH-445]
- improved sentence flow and other stuff @Rio [GH-422]
- rename fwdauth to be forwardauth @desimone [GH-447]
-## Dependency
+### Dependency
- chore(deps): update golang.org/x/crypto commit hash to 61a8779 @renovate [GH-452]
- chore(deps): update golang.org/x/crypto commit hash to 530e935 @renovate [GH-458]
diff --git a/docs/docs/reference/getting-users-identity.md b/docs/docs/reference/getting-users-identity.md
index e92102e3b..5c16967a7 100644
--- a/docs/docs/reference/getting-users-identity.md
+++ b/docs/docs/reference/getting-users-identity.md
@@ -6,36 +6,86 @@ description: >-
# Getting the user's identity
-This article describes how to retrieve a user's identity from a pomerium managed application.
+This article describes how to retrieve a user's identity from a pomerium managed application. Pomerium uses JSON Web Tokens (JWT) to attest that a given request was handled by Pomerium's authorizer service.
-## Headers
+## Prerequisites
-By default, pomerium passes the following [response headers] to it's downstream applications to identify the requesting users.
+To secure your app with signed headers, you'll need the following:
-| Header | description |
-| :------------------------------------- | -------------------------------------------------------------- |
-| `x-pomerium-authenticated-user-id` | Subject is the user's id. |
-| `x-pomerium-authenticated-user-email` | Email is the user's email. |
-| `x-pomerium-authenticated-user-groups` | Groups is the user's groups. |
-| `x-pomerium-iap-jwt-assertion` | **Recommended** Contains the user's details as a signed [JWT]. |
+- An application you want users to connect to.
+- A [JWT] library that supports the `ES256` algorithm.
-In an ideal environment, the cryptographic authenticity of the user's identifying headers should be enforced at the protocol level using mTLS.
+## Verification
-### Recommended : Signed JWT header
-
-For whatever reason, (e.g. an attacker bypasses pomerium's protocol encryption, or it is accidentally turned off), it is possible that the `x-pomerium-authenticated-user-{email,id,groups}` headers could be forged. Therefore, it is highly recommended to use and validate the [JWT] assertion header which adds an additional layer of authenticity.
-
-Verify that the [JWT assertion header](./signed-headers.md) conforms to the following constraints:
+A JWT attesting to the authorization of a given request is added to the downstream HTTP request header `x-pomerium-jwt-assertion`. You should verify that the JWT contains at least the following claims:
| [JWT] | description |
| :------: | ------------------------------------------------------------------------------------------------------ |
| `exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew. |
| `iat` | Issued-at time in seconds since the UNIX epoch. Allow 1 minute for skew. |
| `aud` | The client's final domain e.g. `httpbin.corp.example.com`. |
-| `iss` | Issuer must be `pomerium-proxy`. |
+| `iss` | Issuer must be the URL of your authentication domain e.g. `authenticate.corp.example`. |
| `sub` | Subject is the user's id. Can be used instead of the `x-pomerium-authenticated-user-id` header. |
| `email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header. |
| `groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header. |
-[jwt]: https://jwt.io
-[response headers]: https://developer.mozilla.org/en-US/docs/Glossary/Response_header
+### Manual verification
+
+Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
+
+1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
+
+```bash
+# see ./scripts/generate_self_signed_signing_key.sh
+openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
+openssl req -x509 -new -key ec_private.pem -days 1000000 -out ec_public.pem -subj "/CN=unused"
+# careful! this will output your private key in terminal
+cat ec_private.pem | base64
+```
+
+Copy the base64 encoded value of your private key to `pomerium-proxy`'s environmental configuration variable `SIGNING_KEY`.
+
+```bash
+SIGNING_KEY=ZxqyyIPPX0oWrrOwsxXgl0hHnTx3mBVhQ2kvW1YB4MM=
+```
+
+2. Reload `pomerium-proxy`. Navigate to httpbin (by default, `https://httpbin.corp.${YOUR-DOMAIN}.com`), and login as usual. Click **request inspection**. Select `/headers'. Click **try it out** and then **execute**. You should see something like the following.
+
+
+
+3. `X-Pomerium-Jwt-Assertion` is the signature value. It's less scary than it looks and basically just a compressed, json blob as described above. Navigate to [jwt.io] which provides a helpful GUI to manually verify JWT values.
+
+4. Paste the value of `X-Pomerium-Jwt-Assertion` header token into the `Encoded` form. You should notice that the decoded values look much more familiar.
+
+
+
+5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and past the output of `cat ec_public.pem`.
+
+
+
+**Viola!** Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.
+
+::: warning
+
+In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing pomerium-proxy's public signing key to client apps (typically, a [key management service]).
+
+:::
+
+### Automatic verification
+
+In the future, we will be adding example client implementations for:
+
+- Python
+- Go
+- Java
+- C#
+- PHP
+
+[developer tools]: https://developers.google.com/web/tools/chrome-devtools/open
+[docker-compose.yml]: https://github.com/pomerium/pomerium/blob/master/docker-compose.yml
+[httpbin]: https://httpbin.org/
+[jwt]: https://jwt.io/introduction/
+[jwt.io]: https://jwt.io/
+[key management service]: https://en.wikipedia.org/wiki/Key_management
+[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
+[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations
diff --git a/docs/docs/reference/signed-headers.md b/docs/docs/reference/signed-headers.md
deleted file mode 100644
index 1843615db..000000000
--- a/docs/docs/reference/signed-headers.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-title: Signed Headers
-description: >-
- This article describes how to secure your app with signed headers. When
- configured, pomerium uses JSON Web Tokens (JWT) to make sure that a request to
- your app is authorized.
----
-
-# Securing your app with signed headers
-
-This page describes how to add an additional layer of security to your apps with signed headers. When configured, pomerium uses JSON Web Tokens (JWT) to make sure that a given request was handled by pomerium and the request to your app is authorized.
-
-## Prerequisites
-
-To secure your app with signed headers, you'll need the following:
-
-- An application you want users to connect to.
-- A [JWT] library that supports the `ES256` algorithm.
-
-## Rationale
-
-Signed headers provide **secondary** security in case someone bypasses mTLS and network segmentation. This protects your app from the following kind of risks:
-
-- Pomerium is accidentally disabled
-- Misconfigured firewalls
-- Mutually-authenticated TLS
-- Access from within the project.
-
-To properly secure your app, you must use signed headers for all app types.
-
-## Verification
-
-To secure your app with JWT, cryptographically verify the header, payload, and signature of the JWT. The JWT is in the HTTP request header `x-pomerium-iap-jwt-assertion`. If an attacker bypasses pomerium, they can forge the unsigned identity headers, `x-pomerium-authenticated-user-{email,id,groups}`. JWT provides a more secure alternative.
-
-Note that pomerium it strips the `x-pomerium-*` headers provided by the client when the request goes through the serving infrastructure.
-
-Verify that the JWT's header conforms to the following constraints:
-
- [JWT] | description
-:------: | ------------------------------------------------------------------------------------------------------
- `exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew.
- `iat` | Issued-at time in seconds since the UNIX epoch. Allow 1 minute for skew.
- `aud` | The client's final domain e.g. `httpbin.corp.example.com`.
- `iss` | Issuer must be `pomerium-proxy`.
- `sub` | Subject is the user's id. Can be used instead of the `x-pomerium-authenticated-user-id` header.
-`email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header.
-`groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header.
-
-### Manual verification
-
-Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
-
-1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
-
- ```bash
- # see ./scripts/generate_self_signed_signing_key.sh
- openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
- openssl req -x509 -new -key ec_private.pem -days 1000000 -out ec_public.pem -subj "/CN=unused"
- # careful! this will output your private key in terminal
- cat ec_private.pem | base64
- ```
-
- Copy the base64 encoded value of your private key to `pomerium-proxy`'s environmental configuration variable `SIGNING_KEY`.
-
- ```bash
- SIGNING_KEY=ZxqyyIPPX0oWrrOwsxXgl0hHnTx3mBVhQ2kvW1YB4MM=
- ```
-
-2. Reload `pomerium-proxy`. Navigate to httpbin (by default, `https://httpbin.corp.${YOUR-DOMAIN}.com`), and login as usual. Click **request inspection**. Select `/headers'. Click **try it out** and then **execute**. You should see something like the following.
-
- 
-
-3. `X-Pomerium-Jwt-Assertion` is the signature value. It's less scary than it looks and basically just a compressed, json blob as described above. Navigate to [jwt.io] which provides a helpful GUI to manually verify JWT values.
-
-4. Paste the value of `X-Pomerium-Jwt-Assertion` header token into the `Encoded` form. You should notice that the decoded values look much more familiar.
-
- 
-
-5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and past the output of `cat ec_public.pem`.
-
- 
-
-**Viola!** Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.
-
-::: warning
-
-In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing pomerium-proxy's public signing key to client apps (typically, a [key management service]).
-
-:::
-
-### Automatic verification
-
-In the future, we will be adding example client implementations for:
-
-- Python
-- Go
-- Java
-- C#
-- PHP
-
-[developer tools]: https://developers.google.com/web/tools/chrome-devtools/open
-[docker-compose.yml]: https://github.com/pomerium/pomerium/blob/master/docker-compose.yml
-[httpbin]: https://httpbin.org/
-[jwt]: https://jwt.io/introduction/
-[jwt.io]: https://jwt.io/
-[key management service]: https://en.wikipedia.org/wiki/Key_management
-[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
-[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations
diff --git a/docs/docs/upgrading.md b/docs/docs/upgrading.md
index 501ccd423..cb575274a 100644
--- a/docs/docs/upgrading.md
+++ b/docs/docs/upgrading.md
@@ -7,6 +7,15 @@ description: >-
# Upgrade Guide
+## Since 0.6.0
+
+### Breaking
+
+#### Getting user's identity
+
+User detail headers
+( `x-pomerium-authenticated-user-id` / `x-pomerium-authenticated-user-email` / `x-pomerium-authenticated-user-groups`) have been removed in favor of using the more secure, more data rich attestation jwt header (`x-pomerium-jwt-assertion`).
+
## Since 0.5.0
### Breaking
diff --git a/go.mod b/go.mod
index 2230d4ee5..671b458a6 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
github.com/fsnotify/fsnotify v1.4.7
github.com/go-redis/redis/v7 v7.2.0
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
+ github.com/golang/mock v1.3.1
github.com/golang/protobuf v1.3.4
github.com/google/go-cmp v0.4.0
github.com/gorilla/mux v1.7.4
@@ -40,8 +41,8 @@ require (
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.20.0
google.golang.org/appengine v1.6.5 // indirect
- google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect
- google.golang.org/grpc v1.27.1
+ google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178 // indirect
+ google.golang.org/grpc v1.27.0
gopkg.in/ini.v1 v1.51.1 // indirect
gopkg.in/square/go-jose.v2 v2.4.1
gopkg.in/yaml.v2 v2.2.8
diff --git a/go.sum b/go.sum
index 5d22c3710..b2d02d26f 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/OneOfOne/xxhash v1.2.3 h1:wS8NNaIgtzapuArKIAjsyXtEN/IUjQkbw90xszUdS40=
-github.com/OneOfOne/xxhash v1.2.3/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -45,8 +43,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
-github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
@@ -63,7 +59,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -107,6 +102,7 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -130,8 +126,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -165,16 +159,14 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/memberlist v0.1.5 h1:AYBsgJOW9gab/toO5tEB8lWetVgDKZycqkebJ8xxpqM=
-github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.1.7 h1:5WLszGdi8FjebYTCKuX9zaGNl7pD31uv6Bj6657HxqQ=
github.com/hashicorp/memberlist v0.1.7/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -184,6 +176,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -195,10 +188,10 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39 h1:0E3wlIAcvD6zt/8UJgTd4JMT6UQhsnYyjCIqllyVLbs=
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -208,6 +201,7 @@ github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae h1:yIn3M+2nBaa+i9jUVoO+YmFjdczHt/BgReCj4EJOYOo=
github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -215,6 +209,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -223,10 +218,6 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/open-policy-agent/opa v0.16.2 h1:Fdt1ysSA3p7z88HVHmUFiPM6hqqXbLDDZF9cQFYaIP0=
-github.com/open-policy-agent/opa v0.16.2/go.mod h1:P0xUE/GQAAgnvV537GzA0Ikw4+icPELRT327QJPkaKY=
-github.com/open-policy-agent/opa v0.17.1 h1:FchWeIevMohOKLM7oyFunUHEMp3gAOUCu+NNo/c6FjA=
-github.com/open-policy-agent/opa v0.17.1/go.mod h1:P0xUE/GQAAgnvV537GzA0Ikw4+icPELRT327QJPkaKY=
github.com/open-policy-agent/opa v0.17.3 h1:Irk+/pTpN8bipJ7/XpEbFTg82v6Cmx9+8S/uS6V8MoM=
github.com/open-policy-agent/opa v0.17.3/go.mod h1:6pC1cMYDI92i9EY/GoA2m+HcZlcCrh3jbfny5F7JVTA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -236,6 +227,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d h1:zapSxdmZYY6vJWXFKLQ+MkI+agc+HQyfrCGowDSHiKs=
github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -245,8 +237,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e h1:kD8uNYLnc/MGAGfFzCEDQPVKulBEBY3FlVHvMJkkmQ4=
-github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e/go.mod h1:4GKkzpe75Db5IosO0CrSuFTDr3X0E0rkteGJiY4L9aE=
github.com/pomerium/autocache v0.0.0-20200214161708-6c66ed582edc h1:8eatzx+SFs0jh6QQ4QSEcpW/TDMEbht0V2b+U6/MnAo=
github.com/pomerium/autocache v0.0.0-20200214161708-6c66ed582edc/go.mod h1:8YuqYfLW/ZIavspMvQvH0UrPusRuvdm/r338GoSu2/k=
github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 h1:FmzFXnCAepHZwl6QPhTFqBHcbcGevdiEQjutK+M5bj4=
@@ -262,8 +252,6 @@ github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
-github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8=
github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -279,8 +267,6 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7q
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -288,12 +274,9 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
-github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
@@ -305,8 +288,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
-github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
-github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -314,6 +295,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -329,6 +311,7 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0 h1:BgSbPgT2Zu8hDen1jJDGLWO8voaSRVrwsk18Q/uSh5M=
github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@@ -368,15 +351,12 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -400,16 +380,17 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -447,7 +428,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
@@ -461,11 +441,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs=
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50=
-golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
@@ -498,6 +475,7 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -528,18 +506,15 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 h1:51D++eCgOHufw5VfDE9Uzqyyc+OyQIjb9hkYy9LN5Fk=
-google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178 h1:4mrurAiSXsNNb6GoJatrIsnI+JqKHAVQQ1SbMS5OtDI=
+google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/cryptutil/sign.go b/internal/cryptutil/sign.go
new file mode 100644
index 000000000..e5919e289
--- /dev/null
+++ b/internal/cryptutil/sign.go
@@ -0,0 +1,49 @@
+package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/sha256"
+ "math/big"
+)
+
+// NewSigningKey generates a random P-256 ECDSA private key.
+// Go's P-256 is constant-time (which prevents certain types of attacks)
+// while its P-384 and P-521 are not.
+func NewSigningKey() (*ecdsa.PrivateKey, error) {
+ return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+}
+
+// Sign signs arbitrary data using ECDSA.
+func Sign(data []byte, privkey *ecdsa.PrivateKey) ([]byte, error) {
+ // hash message
+ digest := sha256.Sum256(data)
+
+ // sign the hash
+ r, s, err := ecdsa.Sign(rand.Reader, privkey, digest[:])
+ if err != nil {
+ return nil, err
+ }
+
+ // encode the signature {R, S}
+ // big.Int.Bytes() will need padding in the case of leading zero bytes
+ params := privkey.Curve.Params()
+ curveOrderByteSize := params.P.BitLen() / 8
+ rBytes, sBytes := r.Bytes(), s.Bytes()
+ signature := make([]byte, curveOrderByteSize*2)
+ copy(signature[curveOrderByteSize-len(rBytes):], rBytes)
+ copy(signature[curveOrderByteSize*2-len(sBytes):], sBytes)
+ return signature, nil
+}
+
+// Verify checks a raw ECDSA signature.
+// Returns true if it's valid and false if not.
+func Verify(data, signature []byte, pubkey *ecdsa.PublicKey) bool {
+ // hash message
+ digest := sha256.Sum256(data)
+ curveOrderByteSize := pubkey.Curve.Params().P.BitLen() / 8
+ r, s := new(big.Int), new(big.Int)
+ r.SetBytes(signature[:curveOrderByteSize])
+ s.SetBytes(signature[curveOrderByteSize:])
+ return ecdsa.Verify(pubkey, digest[:], r, s)
+}
diff --git a/internal/cryptutil/sign_test.go b/internal/cryptutil/sign_test.go
new file mode 100644
index 000000000..7d68c10ca
--- /dev/null
+++ b/internal/cryptutil/sign_test.go
@@ -0,0 +1,60 @@
+package cryptutil
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "testing"
+)
+
+func TestSign(t *testing.T) {
+ message := []byte("Hello, world!")
+
+ key, err := NewSigningKey()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ signature, err := Sign(message, key)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if !Verify(message, signature, &key.PublicKey) {
+ t.Error("signature was not correct")
+ return
+ }
+
+ message[0] ^= 0xff
+ if Verify(message, signature, &key.PublicKey) {
+ t.Error("signature was good for altered message")
+ }
+}
+
+func TestSignWithP384(t *testing.T) {
+ message := []byte("Hello, world!")
+
+ key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ signature, err := Sign(message, key)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if !Verify(message, signature, &key.PublicKey) {
+ t.Error("signature was not correct")
+ return
+ }
+
+ message[0] ^= 0xff
+ if Verify(message, signature, &key.PublicKey) {
+ t.Error("signature was good for altered message")
+ }
+}
diff --git a/internal/encoding/jws/jws.go b/internal/encoding/jws/jws.go
index 4f1bf702b..60093728c 100644
--- a/internal/encoding/jws/jws.go
+++ b/internal/encoding/jws/jws.go
@@ -3,9 +3,6 @@
package jws // import "github.com/pomerium/pomerium/internal/encoding/jws"
import (
- "encoding/base64"
-
- "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding"
jose "gopkg.in/square/go-jose.v2"
@@ -31,30 +28,6 @@ func NewHS256Signer(key []byte, issuer string) (encoding.MarshalUnmarshaler, err
return &JSONWebSigner{Signer: sig, key: key, Issuer: issuer}, nil
}
-// NewES256Signer creates a NIST P-256 (aka secp256r1 aka prime256v1) JWT signer
-// from a base64 encoded private key.
-//
-// RSA is not supported due to performance considerations of needing to sign each request.
-// Go's P-256 is constant-time and SHA-256 is faster on 64-bit machines and immune
-// to length extension attacks.
-// See : https://cloud.google.com/iot/docs/how-tos/credentials/keys
-func NewES256Signer(privKey, issuer string) (*JSONWebSigner, error) {
- decodedSigningKey, err := base64.StdEncoding.DecodeString(privKey)
- if err != nil {
- return nil, err
- }
- key, err := cryptutil.DecodePrivateKey(decodedSigningKey)
- if err != nil {
- return nil, err
- }
- sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key},
- (&jose.SignerOptions{}).WithType("JWT"))
- if err != nil {
- return nil, err
- }
- return &JSONWebSigner{Signer: sig, key: key, Issuer: issuer}, nil
-}
-
// Marshal signs, and serializes a JWT.
func (c *JSONWebSigner) Marshal(x interface{}) ([]byte, error) {
s, err := jwt.Signed(c.Signer).Claims(x).CompactSerialize()
diff --git a/internal/frontend/assets/html/dashboard.go.html b/internal/frontend/assets/html/dashboard.go.html
index 9d965f54f..32aa1baa3 100644
--- a/internal/frontend/assets/html/dashboard.go.html
+++ b/internal/frontend/assets/html/dashboard.go.html
@@ -191,7 +191,6 @@
- {{if .IsAdmin}}
- {{ end }}
diff --git a/internal/frontend/statik/statik.go b/internal/frontend/statik/statik.go
index f0bd875a5..6081af2e3 100644
--- a/internal/frontend/statik/statik.go
+++ b/internal/frontend/statik/statik.go
@@ -6,7 +6,11 @@ import (
"github.com/rakyll/statik/fs"
)
+
+const Web = "web" // static asset namespace
+
func init() {
- data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01\xee\x88+^\xecYKo\xeb6\x13\xdd\xe7W\xcc'd\xf9Y\x0c\xd2.\x8a\x0b\xd9hp\x1fE6M\x80\xe4.\xee*\xa0\xa5\xb1<\x05\x1f*I9\x0e\x0c\xff\xf7B\x0f;z9\x96\x15%7(\xeaM$r\x0e9s\x0e\xc9aF\x9bM\x84\x0bR\x08^\xc4\xedr\xae\xb9\x89\xfc\xa5\x93\xc2\xdbn\xcf\x82\xff}\xb9\xf9|\xff\xe3\xf6+d-\xb3\xb3 \xfb\x03\x82\xabx\xea\xa1\xf2 \\rc\xd1M\xbd\xd4-&\xbfy\xb33\x80`\x89<\xca\x1e\x00\x02GN\xe0\xecVK4\x94\xca\x80\x15\xefy\xdff\xe3P&\x82;\x04/C\xa0\xd9O\n\x10\xb0b\x90\xecq\xae\xa3\xa7r\xb8\x88V@\xd1\xd4\x93\x9c\x94W\xb4UZI-\xf4d\xae\xd7\xfb\x9e\xb2/\x14\xdc\xda\xa9\x17r\x13U\xba\xda\x9d\x93\xc2\x8d\x9aM\x16\xce\xe5\xecsj\x0c*\x07\xa9E\x13\xb0\xe5e\xddb\xb3\xa1\x05\xf8wh-i\xe5\xdfR\xe8R\x83\x90\xc7Q\x19\x86d\xbc\x9b\x8cB\xad<\xb0&\x9cz\x9bM\x13\xb8\xddz\xc0E\xc6\xa8E\x03$y\x8c\x1e\xb0\xe6\x8c(,v\xccPk\x80\xda|\x8d\xae|v\xe6'\xa54\x8c[\x8b\xce2\x921\xe3a\xa8S\xe5\x1eB2\xa1\xc0\xc9\xe5\xaf\xc9\xda\xb7\xab\xb89\xc2Z\ne\xa7\xde\xd2\xb9\xe4\x13c\x8f\x8f\x8f\xfe\xe3/\xbe61\xbb\xbc\xb8\xb8`-@;\x04\x15\xd5\"\x08XD\xab\\\xf2}\xcbB\x1b \x12\xddRGS\xef\xf6\xe6\xee\xde\x03\x1e:\xd2\xaa\xe6\xba\xa5X=\xe8\xd45\x85\xb3\x98\xdb\xce\x1a~\x07\xc9\x8e\x17\x89\xd6f\xf4\xce~\xe8\xd4@X\x8al\x0b= B\xc7IX?`Ik\x88\x05\xa1\x88,\xbafGs1\xfc\xc9eS\xa6\x1c/\xf8\x1cE\x1b\x9c9\x9dp5\xcb`\x01\xcb\x1f\xbblH%\xa9\xeb\xe8\x00pO N=\x87k\xd7T\xab\xf8\x95\x81\xe7\xeew[\xac\xb8H\xb1\xb6.\x8b\x18\xba\xad\xf3\xfd\xdc\xdb:\"\xcb\xe7\x02\xa3\x8eN\xd6\x0e4`\x07X\xda\xad\xfe\x06\xd7\x7f\xd0\n\xd5@\xc2s,|(\xda+\xe1\xf4\xe5\xfe(d<\x01\xb2\xbd\xdb\xe0\xff\x1b\x97$\x9e\x06\nP\x80?\x96\x02\xd5\x80\xfaJp\x1c3\xba\x06]Z\xdc\xa5\xf3\xbf0t\x03\x84\xf8n\xd1\\\x7f\xf90\x1a\xec\x03\xe9+\xc0\x11\xc0\xe8\xecWY\xff*9\x89\x01\x9c\xe7\xb8WP\x8e\x19~<\xce\xcb0\xfa2\xfe\xa2\xf9\x9b\xf2\x9d\xad\xd5\x01t\x7f\xcfoq\x83\xd9\x1ew\x81\x171\xf4\xe5\xfa%\xeb\xd1\xa96\\\xc5\x08\xe7\xf4\xff\xf3\x87O\xd3J\x9e5:M\xecI\xbc\xe7\xb2\xe1\xdfpNp\xd1\x01\xdcg\xe1l\xe4\xc3\xcat\xdez\xab\xf8\x17\xa1\x8d\xdb\xe6\x1e\xf9>r\x1f\x97\xf8\xddd\xad\x9dX\xeb\x84\xcc\xd3IZ\x96GV\x0e\xfc0\xbb\xa8p\xc7\xbf\xa7Sru\x0f\xd0\x9b\x92\x7fmm\x8a\xd1\xd5\x90,]@?\x0c\xfd\xbbHN\x14\xa0\x17\xec\xed%\x18\x92C\n\xe0\xcf\x16\xa0\x1d\xc8\x11\xea\xfb\x02F'}\x9fK\xa0\x9eL\xae\xd2\x88P\x85\xa7\xfd\xcb\xd03\x9d\xec\xc6\xfe7e\x94\x8a\x92pH\xbc\xca\xf28h3\xba\xc0\xb5]%\x134V+\xeep\xe8\x95\xf8y\x08R1\xbc\xf6\x82\xfc\x1a\xc6;N\xbbVx\xef\xc4\xf1\x81\x0bY\xc5\x9f\xb7\xbb\x9b\xd5\x05\xf9\xef\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\xb7l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1b\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x8eb\x057\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xed\xa58S\xae\xedU$Im\xb7\xbb\xc2\xf0;\x94\xfb\xb3\x90&\xa4&\xdc\xb6\x8b\xfd\xa3U\xd9m\x9a\xa0Y\x91\xc5\xe8!\xb5h\xde\xb2\xda>\xb8\xb2N\xcf'\xcb\xf0\xe2zk\x81\xe7\x82\x92u\x86;m,\x84\\\x81C\x99h\xc3\x0d\x89'\xa8L\n\\i\xb7D\x93\x7fx\xf1[;\xe2\x94\xba\xfc\xb1\x843<\xc5(.\xcbl\xdbL\x0c\x07\xb3\xefxe\x9b\xee\xceD\xf0\x10\x97ZDh\x8a\xefH\xbf\xe3\x9a\xcbD\xa0\x1fj\xd9\x059\xe5|:\xc6\xe4\x91\xdcp\x94IhPY\xe4\xb4#\\\x8ep\xf6\xf7\xa0\x12UL\n\xd1\x90j\xedO8\x89\xc5\x9fs\x9a\xb7\xbc\xeb\\\xbaW\xb9\x03\x9d|\x97LY\xec\xe0\xba#S\xb4lj\x99\xa3\xd1\xdb\xe6\xae\xe2S\x8b\xbeA\xb9&\xe7 U\x8d\xa0C\xf9g\xff\x18\xb0\xe2\x03t\xc0\x8a/\xe0\xbb\xf4\xfcO\x00\x00\x00\xff\xffPK\x07\x08\x18\x02.\xfcj\x04\x00\x006\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^\x9cUMo\xdb8\x10\xbd\xe7W\xcc\xf2\x1c\x8b\x81w\xb1h\x0b\xda\x97$\x87\x00\x05\x1a\xa4i\x81\x9e\x02\x9a\x1cI\x04D\xd2%G\xfe\x80\xa0\xff^P\xb6S\x8br\x02\xb4'K\xf3\xc5\xf7F\xef\xd1]\xa7\xb14\x0e\x81a\x08>\x145\xd9\x86\xf5\xfd\x95\xf8\xe7\xee\xcb\xed\xf3\x8f\xc7{H\x91\xe5\x95H?\xd0HW-\x18:\x06\xaa\x96!\"-XK\xe5\xec\x03[^\x01\x88\x1a\xa5N\x0f\x00\x82\x0c5\xb8\xec\xba\xe2+Ijc\xdf\xc3\x0c^\xdf\x9eqG}/\xf8\xa1hh\xe8:B\xbbn$!\xb04\x06\x7f#\x01\x10\xfc4Y\xac\xbc\xde\x1f\x8f\xd0f\x03F/\x98\x95\xc6\xb1C\xec,j\\\xe9g+\xbf{\xcd\x1cs\xaa\x911.\x98\x92A\x9f\xa5\xa6\xc9\xd9\x01\xc5\xa8\x06@\x18[\x8d\x02p\xea1\xca;\x96\xa5bP\x0b\xc6\x8b\xb5\xb7\x18Lk\xb9\x8c\x11)rc+>,{6\xffo\xbd+\xe2\xa6\xca\x1bw\xb6qq\xc1j\xa2\xf5'\xce\xb7\xdbm\xb1\xfd\xb7\xf0\xa1\xe2\xf3\x9b\x9b\x1b>i\xe0\x19\xc8z\xbe\x9c\xec\xba\x9e\xbfS4-\x10\\\x9b\xcd(\x10Q\x91\xf1.\x9br\xb65\x8b1\xca\n\xb3\x8d\x8dk\x08w4\xb3\xde\xf9\xb8\x96\nYBp\x9f6\x91\x00d\x07^\xc0\x90dbJ(n\xa5\xbb\xc3U[\x0d\xe2\xb8|\xd0[`\x1eJ\xd8\xfb\x16b\xed\xdbFC-7\x08R)\x8c\xf1\x1a\x94w$\x15\xa5|\x00\xa9\xadq&R\x90\xe4\x03H\xa7a\x1d\xfc\xc6h\xcc\xe6Q\x8d\x16\xb6\x86\xea\xa1-g.\xa1\x0eX\x8e4\xc0\x96\x01\x7f\xb6\x18 4\x924M\x14\\.\x8bK\xbc3\xe2\xe8t\xdf\x0f\x0b(\x9e\x90\xc2\xfe\xdb\xd3\xe7\xbf\xe7\xbf\xc2\xc6\xe0\x06\x13|\x18\xa4\x08&Br\xa0\x0f2\xec\xaf\x87\x1a%\xdd[|\xba\xee\x0cCbDa\x9fx\x0c\xf3\x8e\xfc.r:\x0f\x1d)\x8dDwAd\x13c\x96\xde\xd3\xd4\x98'h\xc93\xf1h\x9a\xd3\xd2\x0b\xe3\xa7\xaa\x9c:\xf9]\xc3\x9eb/\xca\x04\xd5\xe0\xcb\xc7\xff/\x19\xf7\x8f\xad\x0b\xef\xdf \xb9\xb3\xb9\xcct11W0UMp\xf0YK\xa8!Z\xd94\x13\xfa\xc3\x17\x1c\xbe\xd3\xc3]\xdf\x83X\x85\xfc,\x80\xc7#\xe5T\xfc\x1dC4\xde\xe5\x8a\x9b\xdc\x12\xe3\xc0\xe8\xf5\xec\xe5\xf5Q\xf0\xc3\x85.\xf8\xe1_\xe6\xa4\x89_\x01\x00\x00\xff\xffPK\x07\x08\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^D\x8dKn\xc30\x0cD\xf7:\x85\xa0ub\xa1\xfb(wa\xed DT\xa4\x0d\x91\xce\x07\x86\xef^T\x9b.\xdf\xc3\x0c\xdeq,x\xb0\"\xa6\nZ\xd0\xa7\xea\xd2\xd2y\x86\x9b\xc0)\xc4\xa8$(\xe9\xc9xmk\xf7\x14b\x9cWu\xa8\x97\xf4\xe2\xc5kY\xf0\xe4\x19\xd7\x01\x97\xc8\xca\xce\xd4\xae6SC\xf9\xbaD\xa17\xcb.\xffb7\xf4A\xf4\xddPtM!\xdf\xc3\xad\xb1\xfe\x84\x18;ZI\xe6\x9f\x06\xab\xc0\xc8\xf9gCI\x8e\xb7\xe7\xd9\xec\xcf\xd4\x8eGIy\xdaVA\xe7]2\x99\xc1-\x8f_\x16b\x9d\xc62\xdf\xc3q@\x97\xf3\x0c\xbf\x01\x00\x00\xff\xffPK\x07\x08\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^<\x90Ao\x830\x0c\x85\xff\xcaSv\x8eq\x9c\x90\xc2Tz\xd8.\xbb\xec\xb4_0\xb5\x8c \xb5\xa5\x1a\x88T\xfc\xfa\xc9\x80&\x81\xfc\xf9\xe5=K\xf6q\x9c;\xcd\xcb\xeb\xa5\xd1\xf5/\x00\x00\xff\xffPK\x07\x08\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^\xc4UMo#7\x0c\xfd+\x84{i\x0fz\x16I}\x16\xeb\x1c\xfaO\x06\x89\xe3Y\xc0N\x16q:Y\xf8\xd7\x17\xa4\xc6N\x8a\xdd\xf4\xd4\xa2\x08\xc2\x91\x1f)\x0d\xf9\x9e\xc8\xf9r^\x0e\xb4|\xdd\xbf\xfd\xf1\xfc}\xb7\x89\x14\x89\xb5\x93\xc6\x0d}?\x1d\x9f\xce\xbb\xcd\xfc\xfa\xfa\xed\xf7\xed\xf6\xed\xed\x0do\x8a\xe7\x97\xc3Vb\x8c\xdb\xf3r\xd8\xdc}9\xd0\xeb\xcb\xf4t~|~9\xed6\xbe\xe9\xbe\x8d\x9f\xe1\xe5\xcf\xe3~\xb7\xd9/\xfb\xa7\xe7\x87\x87\xcd\xd6\xaaZ\xc3\x1e\x1f\x1f?\x96P3\x12IA\x9190R92X\x83\xa0\xf6 \x90l\x89\xf4\x99\x91\xd3\x91-\x01E+\xc4H\x1a\x14\xad\xba\xe3r\xb2 \xb6\x04\xd3\xb4f\x99\x84\x9cv\n\x8c\x1c\x90\xf3\x02\xad\xf6\x82\xccKhHi\xf6\xa5Z|#7#~\x0d\xbfgd\x83\x04\xc5^'m\xac\x8c\xa2s`\xc4f\xcb\x16\xae`\xbb\x9c\x02TBBk\x13\xa34r\x13\xfd/0\xb8!\xeb\"\xc8b\xdeLn\x86w8\xd3=Z\xf6_)\x05T\x1d\x0bF;\x07\xe4\x1e\xd8\xe8X\xa1\xda/\xa7\xd0\xd0L<\xcb_-7m\x1f\xea\xb5\xfc\xedY)z\x86\x81!u\xac\xbc\x00F\xecC\xed\x1bf<$rs\xe3\xc1X\x0bjD\xc9\xd2\x90\xb2\xd3'\x97S4\xea\xad\x0e\xcb\xb2\xe8\x87*k@\xd6\xfb`\x12EK7\xa3\xaa?\xc9\xd2>#wb\x187\x8e\xf8\x19v\xc4;\x13v\x80\xe9\xd9\x9d?\xf3\xfb\xaa\xdf\"|s'E\xce\xc3\\\x1d\xb5\x04\xe4\xb20\xa4L\xc9\x08t\xb3r\"\x84TH\xe9\x06\xd8\xeda%A\xab\xc3\x0c\\\xd0\x9a\xd1\xa1Cm7WO\xee\xa4\xd6j\xab\x19p$\xe4r91\xd4\xf8\xd54\xb1)\xc1W9\x9c\x15\xad\x06\x94Y\x90\xeb\xe4W\xc9\xcd;k\xa6\xac\x16\xbf\xc5\x9a\xd0uQt\xe7z \x19}\\T\xe4> \xc9M\x9c\x94\xc3\xca\xcd\x87\xcb\xc4\x84\xd4\x11\x8d\x05\xcd\x13\xa3\x99@\xed&\x10\xb2\x06D\xcb\xa6\xad\xe6\x96C2\x02/\xa7P`\x9e\xc4?a\xbe\x96\xff\x8d\xfa\xf2\x1f1/ze\xbe\xa0TJh\x06H\xb0.\x13J\xa6\x81\x1e}\x16\x0e\x15l4uJ\xe4\x9a\xd9\x04\xd1\xfa\x03\x94\xf9\x18l\xf6Z\x93v\x88\x82m&u\xeb\xad\x9e>r\x10\xfb\xca\n\x13S1]\"\xfd\x18h\xb8\xf1e]\x97\xd0\xc5\x9a\xab\x8d\xe6\xea\xa8\xd9\x9f.\xf2\x19\x85i\x95|\x85\xac\x875\xb8\xa2Y\xc7\x08A\xb1-^\xa6CMG\xf1\xe2\xf7\xc1\x0b.\x8b\xa0\xb3\xcf\xe2n\xa32\xd6\xd9d\x9a\xc4^\xe0f\xa4l\x9f\x19\xb1\xb1'\x12\x9c*N\x8b\xa0\xf1l\xab\xbf\xa9a\xb16\x88j\xbe|\xf6qx\xff\xa0\xa9\xa02qB\x9f\x14]\xc9\xcd\xaa\x98\xad\x837\x87&\xf0'\x01\x06\xc8\xc2\x05\x9cf;k \x99\xa7l\xac\xb8\x19\xe93S\\\xcc3\xcb\xbf\x11\x81T/\xa7\xa0\x1d\x9a(\xe1\xe7\xa1\x97\x13\x0bz\xa6\xf8\x0f\xde\xf4\x99\xd7X\xda\x1e\xd6\xff\xf3r\xb8\xfb+\x00\x00\xff\xffPK\x07\x08K\xfe\x8b#M\x03\x00\x00d\x08\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^l\x95\xc7\x0e\xe4HvE\xf7\xf3\x15\x85\xdaR\x9a\xa47\xad\xa9\x06\x82Lz\x9f\xf4\xb9\x19\xd0{\xef\xf9\xf5B\xb5\xd0\x82\x16\n \x16\x07\xe7\xe2\xdd\xb7{\xffZ\x8f\xf2\xc7\xd5w\xc3\xfa\xebg\xb5m\xd3\x1f\xaf\xd7y\x9e\xff<\xb1\x7f\x8eK\xf9Ba\x18~\xadG\xf9\xf3\x7f\"\x7f\\]=\xb4\xff_\x10a\x18\xe6\xf5\x97\xfd\xf9\xe3\xac\xb3\xad\xfa\xf5\x93!\xff c?\x7fTy]V\xdb\xff\xe2Q\xe7';^\xbf~\xc2?\xe0\x1f\x0c\xfc\xd7\xff\xf9\xe7?~\xfc\xf8W\xdd\xc7e\xfe\xa3\xce~\xfd\xf4\xf3t\x1b\x97\x7f;}\xbcl\xff6\x93&O\xb7\x9f?\xb2x\x8b\xffs\x88\xfb\xfco\xff\xe3/\xff\xe3o\xffw+\x0c\xff\x9f\xce\xdf\xf0\xd7V\x7fTK^\xfc\xfa\xf9{\xc8\x1fu_\xbe\xa6\xa1\xfc\xaf$^s\x12\xff\x8f\xdag\xcd\xcf \xabb9\x02\x00\x80\xe1x\x15\xef\x95\x00\x00\xf17\xb2%\x07\"\x00\xc0\xbb\xee\xa5\x14\xff\xedI\xbe\xe3m\xff\x83\x0f&\x9aq!\xeb\xf9\x9fX<0\x9bs8n\xb5GPQ\xb2\x8d\x8f\\S\x86#I5{\xa9\x93K\x95\xd8j\xdb\xe8\x8f/\xec\xd1\x93\x98\x1c\xd9!{]v\xf2x\xd6S\xa2\xee:?\xce$\x18\xe5\xc6\xe4\xd6\xb1/9 \n\xab\xde\xf6v+s\x9c-\xdeL\xe6N\xf9\xb3\x11d\xe8b\x01u\xb8N1\x1fd;\xf9\x13\x031\x04A=\xd4\x82\x0d/3\xa7!D\x13\xdfV\xe0\x8eb)\x0d@\x83\xd9dd\x8b\x08\xac2k\x8c\x00\xb5\xc1\x93\x84\xa3\xe0\x81+eK\x1c\x98\xa7\xb8\x95\xc0\xe9\xc1a5X\xd2\x1e\xcf\x81*p\x95\xbe_g\xb8;\xe0\xddq\xc4\xc8\x1a\xa7^\x00\x86\x06\x11\xa4}\xf3\xe4\xd4S\x16;\x87\x84\xd2\x9e\x91q\x07\x18?\xdfRi\xa0\xc0M\x85\x916#\x14\x05:\xce\x1f%;\x80\xd9\x95\xf6\x1b\xdeZ\x80Xp\xa3\x95xWa\xc2]\xbe\xa7S>\x80\xb3c\xbe!\xb8r#\xd4m\xf3\xe5\xbf\xf5#\xf1g\xc9\xbd\x10\xdf\xc2\x87\xaa\x86C\x1f\xc2\xdd\xe85}\xe7\x17\xc50#xN\xb1\x00\x1b\xcc$M\xfa\x81\xd98b\x97g\x9a\x9c+\x17c.\x0b\xf7\xb5\xa0\xd5\x9a\xf9\xe8\xc9v\ns\xd9]\xe9\x9d\xf7<\xa8\xfd\xe9\x11_U\x9c\\\xf9\xbb\xb4\x91\xbc1\xfa\x0d8\x94M\xed\xc1\x81\x81\xc5\x01\x8a\xb8\x7f_(\x92\xb4\x1f,\xc2\xc1\xb2\xd9\xef\xeb\xbdG\xce;\xb1\xd2\x0b\\f\x17\xe6DS\xdd\xae\xc7\x02,\x0b\x1d\xfam\x81$\xe2\x05\x87\x0e\xc3S\x188\x06\x8b\x8eU\xac\x81\x14\x98\x8a~\xd41K\x86\xf1$4\xab\x18\xab(\xc1pN\x8dc\x0ch\x902\x8a8\xc9\xd6\xe3\x8e\xf6^5\x9c\xa2\x1aq!\xd1\x19\xd6\x99\xde.@t\xbe\x10D\x11\x1e\xfb\xe5Q\xca\";\x1c\xa5\x9a\x0f7d\xed\xb7|+b\nm\xd2\xabM\xd8\x8c\nr4\xe3{v(?\x1e\xc2)9\xaf\xb7=B=d\xcb^\xa6\x97\xf6\x88C\xc7\xf8c\x9b\x05w\xd6b\x0c^\xb7\x0b\xa1\xec\xa8Om\xe4\x18\x8a\xcc.\xfdU\xaa\xe9\x87\xff\\E\xa9AB\xdb\x88\x0ep<$\xf0\x13\xe9\xbdK`\xab\x1e\x0bP\xb8\x94\xbfC\xc7\xae\xa3[O\xa5m\xdeuu\xdd\xdc\xa5\xc2A\x03\xcdk\xbc]\x9f\x0fFX+4\xaa\xc7U3\xef\x18\x16\x05\xb4\xfbB\xcex\xe09\x06\xf2\xae\xbaE\xa4\x0f\xd6\x8a\xbf2h\xe6jI\xd8\xbb3jR\xc4\x9dV,ds\xe6\\O\xad\xa8\x84\xb9\xd4\x8emS\xe2\x98\xad}\xa1\xd5\xe8\xb0\xeaN\xb9\xa4!\xe9\x11H\xbbk\xcb\xee\xb8\xc8z.\x17\x94\xc2\xb5\xf4\x9b\xb03Hl\x86\xcf&\xc4k\xe3\xab,v\x83m\xc0n\xc7\x08\\\x87\xb9\xda%_z\x0e\xc1(\xe1xVf\xa2u\xe2\xf5\x99cOs\xe5\x99\x1b\xa5s\x83[\xd9\xbb\xba ;\x08\x84e\x16z\xb2\xd2\xa8@\x19\xee\xcd\xcf=\xed\x1e\x19\xd6qx\xdaT\xc3I\xf5\x99\x93\x8c\xb1\x93|\xdbx0\xcb\x10\xd2x\xa1-y*\x8c\x96\xe0\xbe[$(\x0e4\x19\xcd\x85\x1a\x9d\xa7\xda\x8ey\x17\x84\n7\xba\x13\xc6\xad\x93\xc6\x92\x13u\x0d`xm\x18\xcajL\x92\x99\x16t5\xb1/r\xfa\xaeB\xe0\xc0{\x19 \xaf\x07\xbb\n\x084\xaf\xa3T\xd47\xac\x9d]f\x9ej`[i\x9f\xab\x83\xc4\xb8b\x88\xe1):8 90/$s\xfb\x08.k\xa8\xa60\x18\x8cq8{\x86%F,\xdb\xed\xb3y\xc3\xd2\\/\x15+\xed\x086\n\x9d\xaaUY\xd8\x0b>\x14\xa9u\xc1V\xf4\xb0\x0c7\x03\xa1@t^ C\x18\x90:\xa2\xd7\xc1\xc2\xac.\x9f\x91K\x10\xab\xdaE\xec\xa4\xda&\x01\x83\x81\xef\xfdI\x0bXt\x94\xa2\xd9\x91\xb3\xb1\xfa\xdd\xb39\xbc\xb9\x9f\xf7\xbe\xc3\x19\xa1\n\xdf\xcf\xce2\xfdQ\xd6\x8b5\x9a5@\xd8+\xd4\xb1\x93\xa83\xf0\x10V\xb9v\xc2\x99~>\xce\xee{\x89(\xc9\x1fM\x19\xc50?\xbf6\xb7\x88\xcai\x1b\xa9\xdcg*\x0e\xf9\xf6\x96Bp\x08=\x0coF9\x9bz\xd4\xa8\xef\xc77\xc0d\x8e\\B\x1a*158\xa1\xc6\xc4\xd3\xf2I\x8dL\xa4]fh\xaf\x8dVj\xf2\xcb\x91\xd8w\xc6 \xf3\xa5\x1f\xe6\xf2\x92\xb8\x95\x15\xbff\xc9y\x0f\xfe%C<=\x97\xc78\xe8\xa2l\xe9\xec,h\xba2Z\xa6\xfb\x88\x8c\xd8~\xe7{,)d\x8b\xd6\xb3\n\npZn$\xf4|@}x\xeaH\x98\xd1\xb5\xbb\xdc\xf2s\x1aL\xd9\x8c{rl\x11\xd0\xac\xd1\nI\x84\xf7\xde\xbeXC~:#m\x91WE\xc75\xb8sJ\xb9##j!3\x1d\xdb\x93bh\xab,\"SJ`1\xb53\xb3\xef\xa8a\x9a\x1cf\xf6\x82,\x98\xb5\xc0\xb6\xcah\xbcs\xf3\x8c \xe7+\x9e\xaf\xb9\xa3\xad\xa2P\x1dz\xcf\xc9\xf2\xfe\xce\x13\xfb\xe9z\xd1\xad\xa4!\xfbb6\xb7\xd30\x9c.\xf2\xb6\xc1\xd0WOC3`\xc2o\x05\x90\x89]\x07\x9c\x80\x82g\x9a\xe5\xf2\xc9s\xde\xde\xf4DU|\xff+\xbeH\x0c\x87%m`\xf4\x99\x8ds\xd3\xa3=\xf5\xcamL\x1f\xca\x8aq\x91\xf8uA\xce\xc7\xe8\x84\xfeB ;-\x9b<\x7f\x16:a\x08F<4\xcc6\xd3\\8.\xec@\x94\x13\xa3\xf93y\x945\x10 \x9f'\xbeg_\xd5\xe5\x80\xe4[\xe2\xd7\x155\xb9;\x9aEo\x1e\x11\xe7Dtk7|\xe8|\xc3^\x0f\x86\x08\xa2\xd7\xd3\n\xf5\x12\x88\xb3\xef(\xf2Z\x837_\x14\xc7|1\xc7\x8b\xff\x16\xd7-\x0c\xee\x91\xdc\x06N\xcfk\xe7\x12\x89i\xc6Lt\xe8Q\x9c\x1f\x0dl3\\\x1eY\xe1\x10w\xdc\x8dm;\xfb(\xae\x19\x04Sf\x0dsK\xb4\xda\xe5\x0cJh\xab\xe9J\xe4\xc9\xb7\x0c\xcb\xeb\xb2\xb3*\xf7*1g\xba\x14\xc9q\x85,\x92\xb8E\xda\xf2fvk\xbe\\\x04\xaf$\x12\xa2\xeevh\xf2\xa3\xda\x92\xadK\xb46Y\xc2\xa7\x8d\x90\xb8\\\xf6-F\x8f\x18\xa5x\xf6]e\x80\xbe\x12\x14%\x0f\xaf8\xfa|I\xfc\x0eS\xd0\x01\x13\xc1\xa0\xaf\x8b\xa8\x99\x14\xcc\x1c\xc1\xef;v\xec*\xcf\x1cA\xa5\x80\xa97Br5\x96\xfd\x1e_\xf9\xd6\x95\xda\xaeu\xba,\x0e\xf1\x0e\xa1Tdn;\x11j\x13\xc1(\xa7\xaer\x9b\x97\xc5\xb0\x9b\x8bMe\xb3\xdc\x90H\xae=\xea\x95\xcfaCvB{h.%\xc3^\x8f\xcbl\xda\xb4\xdc\x06|e\xa3\xe2tC\x89m\xbb\x10B\xf4\x1b\xac\xe8\xb8[\x96\xc7\xe5\xe6\xd4\xdd\xd2\x1b\x7fI\xcfW\x0f\x9al\xeb\xe3G`\x9e[\x17.\x7f\xb4\"O!\xd8.\xc9\x01\x11\xf6\xe5Z\xca'\xbc\xd0\x93\xf9\xe5QChx\xb2,\x9a\xb2\x08.A5\xe3S\xc4\x06I4[h\xa5\xd5J\x11UUh\xa8w\xeb\xd0W\x15VR\xdb\xd0\xa3V\xc4\xbb\x80\xf4\xf7\xc4c\x16<\xb3\x10\xf2\xa5x^\x128J2B\x15*\xde\xe4\x18\x9cI\x828\xbd\xd5\xb8g\xad\x1bf\x99\xeeR\xdc\xd3\xd2\xb3\xdb\x86_\xcb\xda\xd9\xcb\x97\x9a\x87\x96\x97\xd2\x0bF\xbe\x85\x82~.]x\xa8`\x0dO&/l\x83\x88\xbb\\\xb7\x0dd\xe6\x0cUL\xaa\";\x10\xa6[\x96\xec\x03\xde\x02\x08j<\x11\x8a\xf0\x9a\x16U\xb6+n\x9b`D\"\x886\x9d;g\x1d_\xfaL#\xe8[7\xc5\x83\x17\xbf\xa3R\xe4\xab\x82j\x88\x90\xba\xf9}F\xf3}\xa9\xafb\xe7e!\xdd\xb3\x0c`\x99\xb1\xf6\xdfhL\xe4\xa1\x91\xbdN'\xec\xaa\x8aNmW+\xff^\x1a.\x0e1X\x1c/\xf2\xcd\x91\x04\x92\xf8\xea9\xf3\xb5\xfb\x852\x1cW\x94\x05_\xdf9\xc4\x1e>\xfd6\xdd\x8c\xa5ZAu\xd2o+\xafJ \xa1\x1a\xedJ\x0b,JC\x99i\xd5\xfa=R\x98h!\xb6`\xee R\x8b\x15\x9e\xd5{\x1e\x9d\x80\x87\xa1\xde\xe6\xf2\xb0\xabk\xd4\x87L\xe9\xeb\x12>'\xc3\xd5\xe4\xe9\x18\xee#\x98\x8c.p\xc5FQ?\xd6\x1e\x9b\xc2E\xd0\xd9\x98PG\xfb\x14\xd5\xcdt3\xf6p\xae\xfem\xa6vkO\xc8%\xfd\xaa\xd0\xed\xcb%\x98U\x81\x87V\x91\x0d\xd1\xbf-\x90\x7f\xf5Y\x12\xc1\xd8\xddJ^\xe5\x90\xf7*^4\xc3\xd8\x9b&\xaf\x96\x9f\x10\xb2\xd2]X\xb1\x80r\xc9\xe0s\xbb\xc1\xba['\xeel3\xd3\xf5\xc2\x03\x88*C\xbdo\x7f\xec\xf3tF\xa3\x00t\xfc\xad/\x8b\xdd\x9c\xca\x18\x0e.J,\xee\xd5\xc6\xf2Y\x7f\x9a\xad\xd1\xda;0\x95\xafRX\x1e\xecdC\"[A\xae\xb40Q\x18\xafy%\xb6\x82\x1e\xa8\x1c\x8b\xa7\xf7\x99\xcc\xd09\xe7\xfdW\xa1Z\xb6\xb5\xa4\x84?\xcb0X\x0d8\\\xf8\xd7\x05ZE\xba\xa1o\xd7Q\xad\xf7\x89*n\x9bIi\xb0\x1327S+\xeeb$\xab\xd4\x05\"\x02\xf7(\x86\x04\xed^/HF\x1bJ\xf0\x00\x19\xa0e\xa42\xf7\xecb\x9c2\xd1P\xc8\xe8\xc8\xc4\x13#u};\xc0\xe5v\xe3s\x01Gv:\xc7k\xa9[\x08\x81\xd7?\x0e\x8a\xd6\xcd\xb6\xde\xe0\xa5\xbc\x13\xe7\xb2\xc7\x98\x9a\xb3\xfd\xa1\xe54\xce\x84aC\x08d \x0e\xe0n\x0c\xd4\\\x06t\x0f\x1d\xaf8\x81\xad\xf4{>\x00~\xc8Q\xc7pG\x99\x8b\xa6\x96lv&\xc2\xbd\xb2\x93&\x7f\xbc\xd2\xbe\x0d\xeb\xb8f\xaa\x02\xad\xb4\x1e\xb9\xcd\x95?u/vL\xbc\x9c\xa0\xb8.5\x89\x13\x02X\xb1\x98l\x98\x97\xe9b\x86L*\xdb\\\xa8(-\x88\x03\xe2\xccc(\xbb\x91\xa8\xde\xb1\xfb\xd0\x9b-7\xaa\x9e4\xaf\xcb\x9cv:i\xcdbB\xb2\xac\x9bd\xfauKP\xcb\xea\xdf\xf9\x84O\xf7\x82\xc0\xe0\xb9\xb4\xaeo#\xb0\x84\xd5\xbf\x03_z2\x99\xd2\x02\xa15\x97q\xbb\x0e\xacgu\x15\xc93Y\x04\xd8Y\xfa\x9eR%\xa3\xb1\xd8\x9f\x1dE\x84\xad\x07\xe4r\xa9K\x92\x04:\xde\xc0~\x04\x9fc\xf8\xd5Z\xbe\xd25^\\\xb7V1\xc8\xd3\x99L\xbeK\xc45\x99\xcfFR|\xc3\x8cw\xebh\xec\xfe+\xdb\xfe\xeb\xc3\xe1\xe8~\xcdj\xec8\x9c\xe1Gi\xc8\xf2\x10s\x18\x03\xe7\xb6^\x1a\xbb\xa5Wv\xa1\xc6\xf6\x9f{.\x03\xca\x7fP\x85\xdc\xdf\x1f\x99\x8c C\x15<\xa7\x8a\xf32\xd0\xf6\x11|>\xa4\x82\x8cH\x83Q\x96\xd0M\xa7\xd3#ix\xe36\xa4\x84=dm0\xad\x8e\xcag\xbeK\x86c\xfb\x926i\x16\xda\xfa\n\xbb\x116\x94\x84\xcf\xdbY\xe7\x15\xbeFN\xd9\xad\x17\xe38s,s\xef\xbd\x08/.\xbd=\xd2\x9fO\xa8\xd3\xbbD\xd2v\x19n\x07\xe6\xc4\xe9\x85\x1f \x12#\x81Q\x82\xb3A\xfb\xbe\xcf\xe2e.\x89W\xf76\x15\xc6\xdf\xca\xc6\x10\x04\xf8\x9a\x88\xa8\xe4\x00\xdd\xa6\x1d.z\x9e+\xb2\x8e\x81\n\x12\x01\xd5_\xd7\x8ea9N?\xc9\xa8\xcf\xc2\\\xa7\x92\x94\xb5db\x90\xef1\xf1\x92\xe6\xc2\xaf 2\xbeU\xd9\xe3\x92d\xbfQ9\xea\x0f\x85|\xab2\xb7\xd9\x11\x14\xccX\xecY&\x01[\xd9\xcb\xab\x0e\xd8*\x85Z\xe4\xe7\xbd\xedk{J\x81\xe4\xa6\xa6U\xa9\x0f\x9d\xe5\xfd2\xc3\x11\xaew6\x11X\xf7\xbb\xea'd\x7f\x15'\x88\xbf3W\xeb\xbeU\xdaM\xb6S\x93\xd7O\xaa\x16\xe32\xa2-\xb0}\x9f\xe6\xd0\x8d\xed\x9d\xdb+\x92\x0c\x1fq\x91\x1b\x03\x83\x1b\xd0\xd9U\xf6\x1e\xce\xa9\xc0\xd0r\xa2\xe5*$\xb7)[%\xc6$=r\xc1:\"*3\x1ar\x90\xbd\xcd\\\x08k\xf7\xe8\xcfr\x7f\x9b\xc5S\xe1z\x06\xa0\xdb\xc8\x0e\xa2\xe3\xb7\x00?=\xd2\xc4\x02\xd2,\xc8\xd6\xdf\x97Q\xc1\xe9\x17\xb9Db\x80\xc7\xae\x1c\xcd\x94\x8b`\x1e\x9c6w\x8b\xc1\xc6\xc4\xc1\xe4\x12\x1a\xd4\x87b\x0c\xd5\xde\x85\xea\xf5\x91)\x14c\xdf\xf2\xc1$\x16sU\xe4\xf7\xa1?\xe5I\xac\"z\xc3\x90H,\x85\xe9X\x96\xda\xa5\xe3\xc7\xf6\x18\x01\xe5D\xe2\x1e\xb1\x93\xce\xcb<:D\x0c\xfe\x8a\\\xc3\xa20\"\x98\x8f\xcf\xde\xc4W\xeb\x1d\x99U\xe9YJ\xb2Q\x86mF\xa5\xa0\x0f\xceZ%\x82P\xbe\x17v\xc45#\xad\xe3\x15\xb3|\xf5\xcc|1\xa6\x04\xca)\xff\xd2P\x90\x91\x0f\x01Mu\x9a\xd6\xe7a\xb0k\xc9\x16\xae\x8e\xf3\xe5\x02\x95\x0c\x176lL\x08u\x96.O\xb1 l\x18#Ee\\\xf6\xea\xedak\x9fVW)d\xe3\x97\xc5\xfa \xe1q.]\xd2\xc1\x16\x86\xb0'/U\xfa\x81lPP_]\xb3\xa5\x04qY\x88\x1c\x86\x98\xf2\xaauT\x0d\\\xe2\xd6\x16\x0b\xd2\xaap-\x9e\xd1j\xef\xf0\x93\xbb#\xfe\xda\xdd\xd8\x0e\xcb\x81\xech\xea\xeb\xb4V\xcc#'t|`\x13\x9e\xc7\n\x97\xf9|\xa0D\xe7\x10\xd9e8\x06\x0bI/\xa9\xd6\xd4{\x19 )D\xc5N\x87\x91\xe6gU\x10\xacx\xfa* 3+\xa1\xe4\xe3\x9c\xc4\x18\xdbo\xf3z\xa6\xaf\xdb\xf5>\x0ffZ\x96\xcf\x1f]\xb7m\x1b6\x87\xc7\xbfs\xa7\"\xd2\xcd\xeb\xd9\xd0v\xf9X\xa6\xc1\xa874\x9d.\xe7ii\xbc^N\xdb\xcf\xc7\xd7`\x84\x84\xd4\x93z\xf3\xfe\xf6\xf9g\x99\xe8c0\xbf\xadE\xce\xa4#\x07\x04%a+\xe4\xe1\xfb\xb2[\x99\x0bR\xc5\xf6\xf0!\xf0\x81\xdc\xd0\xca~s\x88\x96\"\x9c\x1f-$\x91\x90Ev\xe8c\xdd\xdbR4I\xfc\x14\xf9\xe5p\xbbR\x85b\xf3\xcbf\x88\xad\xeeS\xfd\xbe\xb9\xdf\xb8H\xa1\x1f-Jl\x85+q%6\xaaK\xf9b\x8d\xde\xde\xf9\xe5\xcc\x15\xab\xc3/g,\x198[\x82\x84\xef\xa3\x95\xf6\x9bP\x86u\xabC\n#+<#\x05\xf6p\xac\x88\x1c`={\xe4\xd8\xeeZX%\x87\x98\xd8\"f\xaan!\x84V\x04\x85\xf4\xe5G\xa0Zt\x8f\xbe\x9c\xcc\xa4\x10= \xf6\xfb\xb3722\xb4\x94\x07\xa1\xd5\x03)3\xc4\xaf\xec!\xa9&\xed\x95\x14\xd9\xb3\xc2\xba\xd2\xbf\x06G#\x14Y\xe129\xf4\x9e,l\xcd/\x159\x95\x08\x91\x02$s\x82\x0f\x95v\xd3\x1d\xf3\xf1\xf7r\xbd\x0e\xe6\xfe\xb8\x9fL\x9d\x15!\x99\xd4\xaf\xea\x7fI=T\xc6\xee\xfd\x7f\x00\x00\x00\xff\xffPK\x07\x08uq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01\xee\x88+^\xbcX[o\xdc\xbc\x11}\xf7\xaf`\xd7\xf8\x00;\x10\x15]V\xbb\x8e\xf2\xd6\x14A\x0b\xd4yH\xd0\x87>R\xd2h\xc5\x9a\"\x05\x92\xf2\xaeS\xe4\xbf\x17\xbc\xe8\xba\xbb\xb6\x9b\x87/\x88\x01\x8b\xd7\x993g\xce\x0c\xfd\x01\xfd\xf7\x06\xa1\x96\xc8\x03\xe59\x8a>\xdf \xd4\x91\xaa\xa2\xfc\xe0\xbf\xf0\x11\x8a'\xaaq-\xb8\xc6\xaa\x15B7v\x92pM \xa3DAe\x97\xb5\xe2'\x16\xeat\xb6\xee \xc9\x8b* \x83\xf9a\x1aN\x1a+\xfa\x130\xa9\xfe\xd3+\x9d#.\xb8]Q\x88\x93\x99\xb0[\x0b!+\x90\xb8\x10'3c\x0f\xaeIK\xd9K\x8e0\xe9:\x06X\xbd(\x0dm\x80\xfe\xca(\x7fz$\xe5\x0f\xfb\xfdUp\x1d\xa0\xcd\x0f8\x08@\xff\xfa\xc7&@\xdfE!\xb4\x08n\x10Bh\xf3w`\xcf\xa0iI\xd07\xe8a\x13 E\xb8\xc2\n$\xad\xc7{\x8cm9\x8a%\xb4f\x88Q\x0e\xb8\x01zht\x8e\xe2pkF\x7f\xdd\xdc\x84\x9d\xa4-\x91/\x16\xc2R0!st\xbb\x83m\n\x0ff>df\x83\x9d\xfc\xf8\x01\x91l\x1f\xd75\xfa\xf0qZ+\x0f\xc5]\xbc\xcb\x02\x14\xc7i\x80\x92,\xbb\xb7\xdb*\"\x9f\x86]\xb7\xdb$\xf9\xdbnw\xb6m\xb7\x0b\xd0\xd6\xec\x8c\x92{g\x8b\x85\xb4\x15\\\xa8\x8e\x94`\xf7\xcf<\x89\xc2\x87\xcc;\xb3\xc0\xf1\xc7\xd7G\xc1\x05\xfe\x0e\x87\x9e\x11\x19\xa0G\xe0L\x04\xe8QpR\x8a\x00}\x11\\ FT\x806\xff\xa4\x05H\xa2\xa9\xe0fVl<\x98_D/)H\xf4\x0d\x8e\x9b\x00M\xf7\xff\x85\xb6\x9d\x90\x9apm\xcd+D\xe5`\xaa\xa8\xea\x18y\xc9Q\xcd\xc0\x85\x95\xc1 WTBi\xce\xce\x91\x14G3L\x18=pL5\xb4*G%p\x0d\xd2\xd2\x83\x94O\x07)z^Y\x1c\xc8\x12\xbf\x00EadP\\`\x95\xa6\x01J\xf7\x01\xda\xc6v\xc6\xf0\x8d\xd6/\xb8\x14\\\x03\xd79\xb2\x06\xe3\x02\xf4\x11\x80[ko[B\xf9\xfb\xcc-\x05\xeb[~\xf1\xdc\xc9\xea#\xadt\x93\xa38\x8a\xfe0\x9f-\xe5\x13\x9b\xa2\xe8\xb9q\x97R^\x0bCv{\xf1+\xa7]\x81\xe6\xccT\x9f\xc7\xb8\x10Z\x8b6GI\x98H\xcf\\\xe5\xcc\xff\x7f]\xec\x84\xa2>J\xc0\x88\xa6\xcf6e-\xf3\xacQ9bP\xeb3\x17\x13\x7fk\x13\xafi\x99\x84\x0bV\x1e\xfd\x8em\x14\xad\x0f\x9e\xfcd\xa05Hl\xa2\xe6T*L\xbb\xd3\xb8\\K\xc2U-d\x9b\xa3\xbe\xeb@\x96D\xc1\x8a\x0fq\x1c\x05h\xb7\x0fP\x92\xfa\xe4i\xe2PS\xcd\\\xd6\\\xbeu\x12\xc5polFq\x98d\x03\x9cMr\xb6s\x00b\x90\x85\xd4\xfe{\xd3\xcc\xab\xce\xcdUi\xb8y\x85\xda.\x8b\x9c\x14\x94DV\xd6 /\xa0\x92T\xb4W\xe6\xb0a\xdfjb\xeb\xeep\xa39\x8a\xbb\x13R\x82\xd1\xca\xa5X\x14 \xff?\x8c\x13\x97]\x96\"\x07)\x8e9\x8a\xc7o\xd5H\xca\x9f\xfc\xc8XQ\x10N#w|KN\xd8g\xc2vJ\x84a\xe4\xc1\xaf\x1a\x81\xf6n\xce\xdd\xb5\xc5\xa1!\x95\xb97r\xe6\x98\x15\x91_\xb9\xb66\xda;E\xbd\xa9)\xb0J\x81\x9e\xd5\xba)+\xfc\xbdse\xb9\xad\xcb\xba\xac\xeb\xac\xfa\xedK\xdf\x82x(ws\x89^\x9a\xcaH\x01\xcc\x1a|9\xeb\xdeL[/\xa4C\x16n\x935\xbeQwr%\xfe\x8a\xa0\xbc\"A\xe7\xd9zfz\xce\x85\xbe\xcb\x19Q\x1a\x97\x0de\xd5\xfd\x9c\x91\x03\xf8\xaf0\xed\xfe\x12\x1c\xaa#N\xb4f\xcc\x89\x93l\xe5Y\x84\xe2l& >#\xa5\xb1\xd6\x1eJ\xdbCHK/\x7f\xfe\x14\xd2k1\x87+\xdd\xcd\x036\x8613\xc459\xd6\x82R\xe4\xe0\x14c\xbc8 3\xcf\x8d\xfd\xa8\x0d\xa1u\xc1 \x1f\x83\x93O\x8f\x0b\xb6^R\xc0\xab\x15\xff\x15\xbe\x8a^\x9b~e\xe2X\xd9KeN1`\x98\xefcC5X\x89\xb1\x8b\x8e\x92tv\xdf3\xc8\x9a\x19\x967\xb4\xaa\x80\x8f\xf8M\x13\xc0\x18\xed\x14U\xcb\xd0\x84\n\x18\x94:\xcfI\xadA\xfaf\xc8Sf\xb3\xf9\xbc`0)\x94`\xbd\xb6vy\xe4?9\xf7\x07\xe0=\x1a\xd2}\x0d\xd9\xa9E\xe7\xc1\x1f\x13\xd8\x0e\xe1\x81\xd5\x82\x1anbx\x06\xae\xd5\xe0\xbb\x895\xefz=\xa7\x9e\xd2/l\x06\xce\n-[\x1b\x8d7\xeb\x80\xbdk\xf7\xd4\xe0\x92\xae\x03\" /\xe7s\xa6E\xbe4qi\xec<\x8aC!\x89\xd3\"\xad\xe7q\xf5\xbe\xafIaKLG$\xf8\x16,4\xde\xbc\xaf\xe2{\xe9\xb0\xc3\x86\x1e9\x1aH\xf2\x8e\xe6),z\xad}r\x0dF;~\x9e\xd1v\xe8\x95\xd7\"\xbb\xedNh\xd7\x9d\x9c(dQ\x80\xcc\xcf\xa7\xd4\xeaB|\x1f\x98\xa4\xe9N(\x1dV\xcc\xd5\xf7\xe1\x1d\xe2\x1b\x9d\xa5\xdb\xde\xa5\xdb\xa8\x95\x9euWSi\x06\xf9BO\xc7\x9c\xa9\xa0\x14\xae[\x9e1\xcb#\x136\x84\xd5#\xbf|\x19\x8d>\xaf\xcb\xe84R\x10E\x8d2\x13V\xdee\xd1\x1f\x08\xdb\xbb\xee\x17g\xd6=c\xeb3\xe3\xf9\x8a\xbc1y\xec\xfa\x94\xa9\xf9\xb0\xbf2\xa2\xe1\xdfw8\xb6g\xaec\xb1\xefN(6\x01\x89.\x86\xe3\xde\xbd\x03\"\x1b\x8d\xdd\xb4l\x1d\x93_7\xa1\xa8kl \xe1\xd2qN\x84l\x9ffE\xe6i*\x84\xc9\xe4Q\xa0G\xb6Rn\xdfa\x9a\x14\xee1\xb9\xd0\x818Y\xcaH\xe2c\xbe\x92\xf7g\x90\xe6\xe5\xc7\x86\xa2\xa0E7\x7f>\xf5\x1a\xaa\xe5k\xae\xdcg\xfbj\xf1\x9aY\xbd\xabfJo\xfa.\xec\xcc\xbf\x9cgoe\xcf\xaa\x14\x8f\xcd\xf4\xcc\xd1\xb1\x97\xb2C^%\xf1\xacGZ\xb57\x17\xa6\xcc\xb1\xcb\x89\xeb\xcd\xed\xd5\xaf2\x86\x9f-\x1a\x81o\x80T\xd7\x80\x7fo\x87s\x16\x8f\xdf\x81\xde\x13\xf2O\xc6\xfd\x1d-\xd5\x12z-\xba\xb7p7K\xae\x82n3F\x8e\x7f\xe2\xb8\xd8j\xfd/\x00\x00\xff\xffPK\x07\x08L\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x18\x02.\xfcj\x04\x00\x006\x1f\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00html/dashboard.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xb7\x04\x00\x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81U\x07\x00\x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x83\xba\x83\xe4\xf5\x00\x00\x00|\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81I\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x90 \x00\x00img/error-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029PK\xfe\x8b#M\x03\x00\x00d\x08\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x90\n\x00\x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81$\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029Puq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x88\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029PL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x00style/main.cssUT\x05\x00\x01\xee\x88+^PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00^%\x00\x00\x00\x00"
- fs.Register(data)
-}
+ data := "PK\x03\x04\x14\x00\x08\x00\x08\x004\x17bP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01%v\\^\xecYKo\xe36\x10\xbe\xe7WL\x85\x1ck1H{(\x16\xb2\xd1`\x1f\xc5^\x9a\x00\xc9\x1e\xf6\x14\xd0\xd2X\x9e\x82\x0f\x95\xa4\x1c\x07F\xfe{\xa1\x87\x1d\xbd\x1c\xcb\x8a\x925\x8a\xf5%\x129\x1f9\xf3}$\x87\x19m6\x11.H!x\x11\xb7\xcb\xb9\xe6&\xf2\x97N\n\xef\xe9\xe9,\xf8\xe5\xd3\xf5\xc7\xbb\xef7\x9f!k\x99\x9d\x05\xd9\x1f\x10\\\xc5S\x0f\x95\x07\xe1\x92\x1b\x8bn\xea\xa5n1\xf9\xc3\x9b\x9d\x01\x04K\xe4Q\xf6\x00\x108r\x02g7Z\xa2\xa1T\x06\xacx\xcf\xfb6\x1b\x872\x11\xdc!x\x19\x02\xcdnR\x80\x80\x15\x83d\x8fs\x1d=\x96\xc3E\xb4\x02\x8a\xa6\x9e\xe4\xa4\xbc\xa2\xad\xd2Jj\xa1's\xbd\xde\xf5\x94}\xa1\xe0\xd6N\xbd\x90\x9b\xa8\xd2\xd5\xee\x9c\x14n\xd4l\xb2p.g\x1fScP9H-\x9a\x80-/\xeb\x16\x9b\x0d-\xc0\xbfEkI+\xff\x86B\x97\x1a\x84<\x8e\xca0$\xe3\xedd\x14j\xe5\x815\xe1\xd4\xdbl\x9a\xc0\xa7'\x0f\xb8\xc8\x18\xb5h\x80$\x8f\xd1\x03\xd6\x9c\x11\x85\xc5\x8e\x19j\x0dP\x9b\xaf\xd1\x95\xcf\xce\xfc\xa4\x94\x86qk\xd1YF2f<\x0cu\xaa\xdc}H&\x148\xb9\xfc=Y\xfbv\x157GXK\xa1\xec\xd4[:\x97|`\xec\xe1\xe1\xc1\x7f\xf8\xcd\xd7&f\x97\x17\x17\x17\xac\x05h\x87\xa0\xa2Z\x04\x01\x8bh\x95K\xbekYh#A\xa2[\xeah\xea\xdd\\\xdf\xdey\xc0CGZ\xd5\\\xb7\x14\xab{\x9d\xba\xa6p\x16s\xdbY\xc3\xef \xd9\xf2\"\xd1\xda\x8c\xde\xd9w\x9d\x1a\x08K\x91m\xa1\x07D\xe88 \xeb\x07,i\x0d\xb1 \x14\x91E\xd7\xech.\x86\xbf\xb9l\xca\x94\xe3\x05\x9f\xa3h\x833\xa7\x13\xaef\x19,`\xf9c\x97\x0d\xa9$u\x1d\x1d\x00\xee1\xc1\xa9\xe7p\xed\x9aj\x15\xbf2\xf0\xdc\xfdn\x8b\x15\x17)\xd6\xd6e\x11C\xb7u\xbe\x9f{[Gd\xf9\\`\xd4\xd1\xc9\xda\x81\x06l\x0fK\xdb\xd5\xdf\xe0\xfa/Z\xa1\x1aHx\x8e\x85\x93\xa2\xbd\x12N_\xee\x0fB\xc6\x13 \xdb\xbb\x0d\xfe\xbfpI\xe2q\xa0\x00\x05\xf8\xb4\x14\xa8\x06\xd4W\x82\xc3\x98\xd15\xe8\xd2\xe26\x9d\xff\x83\xa1\x1b \xc47\x8b\xe6\xeb\xa7\x93\xd1`\x17H_\x01\x0e\x00Fg\xbf\xca\xfag\xc9I\x0c\xe0<\xc7\xbd\x82r\xcc\xf0\xe3q^\x86\xd1\x97\xf1\x17\xcd\xdf\x94\xefl\xad\x0e\xa0\xfb[~\x8b\x1b\xcc\xf6\xb8\x0b\xbc\x88\xa1/\xd7/Y\x8fN\xb5\xe1*F8\xa7_\xcf\xef?L+y\xd6\xe84\xb1G\xf1\x9e\xcb\x86\xff\xc29\xc1E\x07p\x97\x85\xb3\x91\xf7+\xd3y\xeb\xad\xe2_\x846n\x9b;\xe4\xfb\xc8}X\xe2w\x93\xb5vb\xad\x132\x8fGiY\x1eY9\xf0dvQ\xe1\x8e\x7fG\xc7\xe4\xea\x1e\xa07%\xff\xab\xb5)FWC\xb2t\x01=\x19\xfa\xb7\x91\x1c)@/\xd8\xdbK0$\x87\x14\xc0\x1f-@;\x90\x03\xd4\xf7\x05\x8cN\xfa.\x97@=\x99\\\xa5\x11\xa1\n\x8f\xfb\x97\xa1g:\xd9\x8e\xfd\x7f\xca(\x15%a\x9fx\x95\xe5\xb1\xd7ft\x81k\xbbJ&h\xacV\xdc\xe1\xd0+\xf1\xf3\x10\xa4bx\xed\x05\xf95\x8cw\x9cv\xad\xf0\xde\x89\xe3=\x17\xb2\x8a?ow7\xab\x0b\xf2\xf3\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\x97l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1d\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x96b\x05\xd7\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xbd\x04\xfc\x0e\x85\xfd\xcc\xf9 \xa9 \xb7\xed\xb2\xfeh\xf5t\x9b&hVd1\xbaO-\x9a\xb7\xac\xab\x0f\xae\xa1\xf3H\x92b\xf4|\x92\x0c/\xa6\xb7\x16\xf4U66Yg\xb8\xd3\xc6B\xc8\x158\x94\x896\xdc\x90x\x84\xca\xa4\xc0\x95vK4\xf9\x87\x16\xbf\xb5\x03\x8e\xa9\xc3\x1fJ0\xc3S\x8a\xe2\xb2\xcc\xae\xcdD\xb07\xdb\x8eW\xa6\xe9\xeeL\x04\x0fq\xa9E\x84\xa6\xf8n\xf4'\xae\xb9L\x04\xfa\xa1\x96]\x90c\xce\xa3CL\x1e\xc8\x05\x07\x99\x84\x06\x95E\x0e;\xc0\xe5\x08g}\x0f*Q\xc5\xa4\x10\x0d\xa9\xd6.\x85\xa3X\xfc1\xa7w\xcb\xbb\xce\xa5{\x95;\xd0\xc9w\xc9\x94\xc5\x0e\xae;2C\xcb\xa6\x96)\x1a\xbdm\xee*>\xb5\xe8\x1b-\xb7\xd4\x1e\x03V|\\\x0eX\xf1u{\x9bz\xff\x0b\x00\x00\xff\xffPK\x07\x08!yu\xf8Z\x04\x00\x00\x12\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^\x9cUMo\xdb8\x10\xbd\xe7W\xcc\xf2\x1c\x8b\x81w\xb1h\x0b\xda\x97$\x87\x00\x05\x1a\xa4i\x81\x9e\x02\x9a\x1cI\x04D\xd2%G\xfe\x80\xa0\xff^P\xb6S\x8br\x02\xb4'K\xf3\xc5\xf7F\xef\xd1]\xa7\xb14\x0e\x81a\x08>\x145\xd9\x86\xf5\xfd\x95\xf8\xe7\xee\xcb\xed\xf3\x8f\xc7{H\x91\xe5\x95H?\xd0HW-\x18:\x06\xaa\x96!\"-XK\xe5\xec\x03[^\x01\x88\x1a\xa5N\x0f\x00\x82\x0c5\xb8\xec\xba\xe2+Ijc\xdf\xc3\x0c^\xdf\x9eqG}/\xf8\xa1hh\xe8:B\xbbn$!\xb04\x06\x7f#\x01\x10\xfc4Y\xac\xbc\xde\x1f\x8f\xd0f\x03F/\x98\x95\xc6\xb1C\xec,j\\\xe9g+\xbf{\xcd\x1cs\xaa\x911.\x98\x92A\x9f\xa5\xa6\xc9\xd9\x01\xc5\xa8\x06@\x18[\x8d\x02p\xea1\xca;\x96\xa5bP\x0b\xc6\x8b\xb5\xb7\x18Lk\xb9\x8c\x11)rc+>,{6\xffo\xbd+\xe2\xa6\xca\x1bw\xb6qq\xc1j\xa2\xf5'\xce\xb7\xdbm\xb1\xfd\xb7\xf0\xa1\xe2\xf3\x9b\x9b\x1b>i\xe0\x19\xc8z\xbe\x9c\xec\xba\x9e\xbfS4-\x10\\\x9b\xcd(\x10Q\x91\xf1.\x9br\xb65\x8b1\xca\n\xb3\x8d\x8dk\x08w4\xb3\xde\xf9\xb8\x96\nYBp\x9f6\x91\x00d\x07^\xc0\x90dbJ(n\xa5\xbb\xc3U[\x0d\xe2\xb8|\xd0[`\x1eJ\xd8\xfb\x16b\xed\xdbFC-7\x08R)\x8c\xf1\x1a\x94w$\x15\xa5|\x00\xa9\xadq&R\x90\xe4\x03H\xa7a\x1d\xfc\xc6h\xcc\xe6Q\x8d\x16\xb6\x86\xea\xa1-g.\xa1\x0eX\x8e4\xc0\x96\x01\x7f\xb6\x18 4\x924M\x14\\.\x8bK\xbc3\xe2\xe8t\xdf\x0f\x0b(\x9e\x90\xc2\xfe\xdb\xd3\xe7\xbf\xe7\xbf\xc2\xc6\xe0\x06\x13|\x18\xa4\x08&Br\xa0\x0f2\xec\xaf\x87\x1a%\xdd[|\xba\xee\x0cCbDa\x9fx\x0c\xf3\x8e\xfc.r:\x0f\x1d)\x8dDwAd\x13c\x96\xde\xd3\xd4\x98'h\xc93\xf1h\x9a\xd3\xd2\x0b\xe3\xa7\xaa\x9c:\xf9]\xc3\x9eb/\xca\x04\xd5\xe0\xcb\xc7\xff/\x19\xf7\x8f\xad\x0b\xef\xdf \xb9\xb3\xb9\xcct11W0UMp\xf0YK\xa8!Z\xd94\x13\xfa\xc3\x17\x1c\xbe\xd3\xc3]\xdf\x83X\x85\xfc,\x80\xc7#\xe5T\xfc\x1dC4\xde\xe5\x8a\x9b\xdc\x12\xe3\xc0\xe8\xf5\xec\xe5\xf5Q\xf0\xc3\x85.\xf8\xe1_\xe6\xa4\x89_\x01\x00\x00\xff\xffPK\x07\x08\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^D\x8dKn\xc30\x0cD\xf7:\x85\xa0ub\xa1\xfb(wa\xed DT\xa4\x0d\x91\xce\x07\x86\xef^T\x9b.\xdf\xc3\x0c\xdeq,x\xb0\"\xa6\nZ\xd0\xa7\xea\xd2\xd2y\x86\x9b\xc0)\xc4\xa8$(\xe9\xc9xmk\xf7\x14b\x9cWu\xa8\x97\xf4\xe2\xc5kY\xf0\xe4\x19\xd7\x01\x97\xc8\xca\xce\xd4\xae6SC\xf9\xbaD\xa17\xcb.\xffb7\xf4A\xf4\xddPtM!\xdf\xc3\xad\xb1\xfe\x84\x18;ZI\xe6\x9f\x06\xab\xc0\xc8\xf9gCI\x8e\xb7\xe7\xd9\xec\xcf\xd4\x8eGIy\xdaVA\xe7]2\x99\xc1-\x8f_\x16b\x9d\xc62\xdf\xc3q@\x97\xf3\x0c\xbf\x01\x00\x00\xff\xffPK\x07\x08\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^<\x90Ao\x830\x0c\x85\xff\xcaSv\x8eq\x9c\x90\xc2Tz\xd8.\xbb\xec\xb4_0\xb5\x8c \xb5\xa5\x1a\x88T\xfc\xfa\xc9\x80&\x81\xfc\xf9\xe5=K\xf6q\x9c;\xcd\xcb\xeb\xa5\xd1\xf5/\x00\x00\xff\xffPK\x07\x08\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^\xc4UMo#7\x0c\xfd+\x84{i\x0fz\x16I}\x16\xeb\x1c\xfaO\x06\x89\xe3Y\xc0N\x16q:Y\xf8\xd7\x17\xa4\xc6N\x8a\xdd\xf4\xd4\xa2\x08\xc2\x91\x1f)\x0d\xf9\x9e\xc8\xf9r^\x0e\xb4|\xdd\xbf\xfd\xf1\xfc}\xb7\x89\x14\x89\xb5\x93\xc6\x0d}?\x1d\x9f\xce\xbb\xcd\xfc\xfa\xfa\xed\xf7\xed\xf6\xed\xed\x0do\x8a\xe7\x97\xc3Vb\x8c\xdb\xf3r\xd8\xdc}9\xd0\xeb\xcb\xf4t~|~9\xed6\xbe\xe9\xbe\x8d\x9f\xe1\xe5\xcf\xe3~\xb7\xd9/\xfb\xa7\xe7\x87\x87\xcd\xd6\xaaZ\xc3\x1e\x1f\x1f?\x96P3\x12IA\x9190R92X\x83\xa0\xf6 \x90l\x89\xf4\x99\x91\xd3\x91-\x01E+\xc4H\x1a\x14\xad\xba\xe3r\xb2 \xb6\x04\xd3\xb4f\x99\x84\x9cv\n\x8c\x1c\x90\xf3\x02\xad\xf6\x82\xccKhHi\xf6\xa5Z|#7#~\x0d\xbfgd\x83\x04\xc5^'m\xac\x8c\xa2s`\xc4f\xcb\x16\xae`\xbb\x9c\x02TBBk\x13\xa34r\x13\xfd/0\xb8!\xeb\"\xc8b\xdeLn\x86w8\xd3=Z\xf6_)\x05T\x1d\x0bF;\x07\xe4\x1e\xd8\xe8X\xa1\xda/\xa7\xd0\xd0L<\xcb_-7m\x1f\xea\xb5\xfc\xedY)z\x86\x81!u\xac\xbc\x00F\xecC\xed\x1bf<$rs\xe3\xc1X\x0bjD\xc9\xd2\x90\xb2\xd3'\x97S4\xea\xad\x0e\xcb\xb2\xe8\x87*k@\xd6\xfb`\x12EK7\xa3\xaa?\xc9\xd2>#wb\x187\x8e\xf8\x19v\xc4;\x13v\x80\xe9\xd9\x9d?\xf3\xfb\xaa\xdf\"|s'E\xce\xc3\\\x1d\xb5\x04\xe4\xb20\xa4L\xc9\x08t\xb3r\"\x84TH\xe9\x06\xd8\xeda%A\xab\xc3\x0c\\\xd0\x9a\xd1\xa1Cm7WO\xee\xa4\xd6j\xab\x19p$\xe4r91\xd4\xf8\xd54\xb1)\xc1W9\x9c\x15\xad\x06\x94Y\x90\xeb\xe4W\xc9\xcd;k\xa6\xac\x16\xbf\xc5\x9a\xd0uQt\xe7z \x19}\\T\xe4> \xc9M\x9c\x94\xc3\xca\xcd\x87\xcb\xc4\x84\xd4\x11\x8d\x05\xcd\x13\xa3\x99@\xed&\x10\xb2\x06D\xcb\xa6\xad\xe6\x96C2\x02/\xa7P`\x9e\xc4?a\xbe\x96\xff\x8d\xfa\xf2\x1f1/ze\xbe\xa0TJh\x06H\xb0.\x13J\xa6\x81\x1e}\x16\x0e\x15l4uJ\xe4\x9a\xd9\x04\xd1\xfa\x03\x94\xf9\x18l\xf6Z\x93v\x88\x82m&u\xeb\xad\x9e>r\x10\xfb\xca\n\x13S1]\"\xfd\x18h\xb8\xf1e]\x97\xd0\xc5\x9a\xab\x8d\xe6\xea\xa8\xd9\x9f.\xf2\x19\x85i\x95|\x85\xac\x875\xb8\xa2Y\xc7\x08A\xb1-^\xa6CMG\xf1\xe2\xf7\xc1\x0b.\x8b\xa0\xb3\xcf\xe2n\xa32\xd6\xd9d\x9a\xc4^\xe0f\xa4l\x9f\x19\xb1\xb1'\x12\x9c*N\x8b\xa0\xf1l\xab\xbf\xa9a\xb16\x88j\xbe|\xf6qx\xff\xa0\xa9\xa02qB\x9f\x14]\xc9\xcd\xaa\x98\xad\x837\x87&\xf0'\x01\x06\xc8\xc2\x05\x9cf;k \x99\xa7l\xac\xb8\x19\xe93S\\\xcc3\xcb\xbf\x11\x81T/\xa7\xa0\x1d\x9a(\xe1\xe7\xa1\x97\x13\x0bz\xa6\xf8\x0f\xde\xf4\x99\xd7X\xda\x1e\xd6\xff\xf3r\xb8\xfb+\x00\x00\xff\xffPK\x07\x08K\xfe\x8b#M\x03\x00\x00d\x08\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^l\x95\xc7\x0e\xe4HvE\xf7\xf3\x15\x85\xdaR\x9a\xa47\xad\xa9\x06\x82Lz\x9f\xf4\xb9\x19\xd0{\xef\xf9\xf5B\xb5\xd0\x82\x16\n \x16\x07\xe7\xe2\xdd\xb7{\xffZ\x8f\xf2\xc7\xd5w\xc3\xfa\xebg\xb5m\xd3\x1f\xaf\xd7y\x9e\xff<\xb1\x7f\x8eK\xf9Ba\x18~\xadG\xf9\xf3\x7f\"\x7f\\]=\xb4\xff_\x10a\x18\xe6\xf5\x97\xfd\xf9\xe3\xac\xb3\xad\xfa\xf5\x93!\xff c?\x7fTy]V\xdb\xff\xe2Q\xe7';^\xbf~\xc2?\xe0\x1f\x0c\xfc\xd7\xff\xf9\xe7?~\xfc\xf8W\xdd\xc7e\xfe\xa3\xce~\xfd\xf4\xf3t\x1b\x97\x7f;}\xbcl\xff6\x93&O\xb7\x9f?\xb2x\x8b\xffs\x88\xfb\xfco\xff\xe3/\xff\xe3o\xffw+\x0c\xff\x9f\xce\xdf\xf0\xd7V\x7fTK^\xfc\xfa\xf9{\xc8\x1fu_\xbe\xa6\xa1\xfc\xaf$^s\x12\xff\x8f\xdag\xcd\xcf \xabb9\x02\x00\x80\xe1x\x15\xef\x95\x00\x00\xf17\xb2%\x07\"\x00\xc0\xbb\xee\xa5\x14\xff\xedI\xbe\xe3m\xff\x83\x0f&\x9aq!\xeb\xf9\x9fX<0\x9bs8n\xb5GPQ\xb2\x8d\x8f\\S\x86#I5{\xa9\x93K\x95\xd8j\xdb\xe8\x8f/\xec\xd1\x93\x98\x1c\xd9!{]v\xf2x\xd6S\xa2\xee:?\xce$\x18\xe5\xc6\xe4\xd6\xb1/9 \n\xab\xde\xf6v+s\x9c-\xdeL\xe6N\xf9\xb3\x11d\xe8b\x01u\xb8N1\x1fd;\xf9\x13\x031\x04A=\xd4\x82\x0d/3\xa7!D\x13\xdfV\xe0\x8eb)\x0d@\x83\xd9dd\x8b\x08\xac2k\x8c\x00\xb5\xc1\x93\x84\xa3\xe0\x81+eK\x1c\x98\xa7\xb8\x95\xc0\xe9\xc1a5X\xd2\x1e\xcf\x81*p\x95\xbe_g\xb8;\xe0\xddq\xc4\xc8\x1a\xa7^\x00\x86\x06\x11\xa4}\xf3\xe4\xd4S\x16;\x87\x84\xd2\x9e\x91q\x07\x18?\xdfRi\xa0\xc0M\x85\x916#\x14\x05:\xce\x1f%;\x80\xd9\x95\xf6\x1b\xdeZ\x80Xp\xa3\x95xWa\xc2]\xbe\xa7S>\x80\xb3c\xbe!\xb8r#\xd4m\xf3\xe5\xbf\xf5#\xf1g\xc9\xbd\x10\xdf\xc2\x87\xaa\x86C\x1f\xc2\xdd\xe85}\xe7\x17\xc50#xN\xb1\x00\x1b\xcc$M\xfa\x81\xd98b\x97g\x9a\x9c+\x17c.\x0b\xf7\xb5\xa0\xd5\x9a\xf9\xe8\xc9v\ns\xd9]\xe9\x9d\xf7<\xa8\xfd\xe9\x11_U\x9c\\\xf9\xbb\xb4\x91\xbc1\xfa\x0d8\x94M\xed\xc1\x81\x81\xc5\x01\x8a\xb8\x7f_(\x92\xb4\x1f,\xc2\xc1\xb2\xd9\xef\xeb\xbdG\xce;\xb1\xd2\x0b\\f\x17\xe6DS\xdd\xae\xc7\x02,\x0b\x1d\xfam\x81$\xe2\x05\x87\x0e\xc3S\x188\x06\x8b\x8eU\xac\x81\x14\x98\x8a~\xd41K\x86\xf1$4\xab\x18\xab(\xc1pN\x8dc\x0ch\x902\x8a8\xc9\xd6\xe3\x8e\xf6^5\x9c\xa2\x1aq!\xd1\x19\xd6\x99\xde.@t\xbe\x10D\x11\x1e\xfb\xe5Q\xca\";\x1c\xa5\x9a\x0f7d\xed\xb7|+b\nm\xd2\xabM\xd8\x8c\nr4\xe3{v(?\x1e\xc2)9\xaf\xb7=B=d\xcb^\xa6\x97\xf6\x88C\xc7\xf8c\x9b\x05w\xd6b\x0c^\xb7\x0b\xa1\xec\xa8Om\xe4\x18\x8a\xcc.\xfdU\xaa\xe9\x87\xff\\E\xa9AB\xdb\x88\x0ep<$\xf0\x13\xe9\xbdK`\xab\x1e\x0bP\xb8\x94\xbfC\xc7\xae\xa3[O\xa5m\xdeuu\xdd\xdc\xa5\xc2A\x03\xcdk\xbc]\x9f\x0fFX+4\xaa\xc7U3\xef\x18\x16\x05\xb4\xfbB\xcex\xe09\x06\xf2\xae\xbaE\xa4\x0f\xd6\x8a\xbf2h\xe6jI\xd8\xbb3jR\xc4\x9dV,ds\xe6\\O\xad\xa8\x84\xb9\xd4\x8emS\xe2\x98\xad}\xa1\xd5\xe8\xb0\xeaN\xb9\xa4!\xe9\x11H\xbbk\xcb\xee\xb8\xc8z.\x17\x94\xc2\xb5\xf4\x9b\xb03Hl\x86\xcf&\xc4k\xe3\xab,v\x83m\xc0n\xc7\x08\\\x87\xb9\xda%_z\x0e\xc1(\xe1xVf\xa2u\xe2\xf5\x99cOs\xe5\x99\x1b\xa5s\x83[\xd9\xbb\xba ;\x08\x84e\x16z\xb2\xd2\xa8@\x19\xee\xcd\xcf=\xed\x1e\x19\xd6qx\xdaT\xc3I\xf5\x99\x93\x8c\xb1\x93|\xdbx0\xcb\x10\xd2x\xa1-y*\x8c\x96\xe0\xbe[$(\x0e4\x19\xcd\x85\x1a\x9d\xa7\xda\x8ey\x17\x84\n7\xba\x13\xc6\xad\x93\xc6\x92\x13u\x0d`xm\x18\xcajL\x92\x99\x16t5\xb1/r\xfa\xaeB\xe0\xc0{\x19 \xaf\x07\xbb\n\x084\xaf\xa3T\xd47\xac\x9d]f\x9ej`[i\x9f\xab\x83\xc4\xb8b\x88\xe1):8 90/$s\xfb\x08.k\xa8\xa60\x18\x8cq8{\x86%F,\xdb\xed\xb3y\xc3\xd2\\/\x15+\xed\x086\n\x9d\xaaUY\xd8\x0b>\x14\xa9u\xc1V\xf4\xb0\x0c7\x03\xa1@t^ C\x18\x90:\xa2\xd7\xc1\xc2\xac.\x9f\x91K\x10\xab\xdaE\xec\xa4\xda&\x01\x83\x81\xef\xfdI\x0bXt\x94\xa2\xd9\x91\xb3\xb1\xfa\xdd\xb39\xbc\xb9\x9f\xf7\xbe\xc3\x19\xa1\n\xdf\xcf\xce2\xfdQ\xd6\x8b5\x9a5@\xd8+\xd4\xb1\x93\xa83\xf0\x10V\xb9v\xc2\x99~>\xce\xee{\x89(\xc9\x1fM\x19\xc50?\xbf6\xb7\x88\xcai\x1b\xa9\xdcg*\x0e\xf9\xf6\x96Bp\x08=\x0coF9\x9bz\xd4\xa8\xef\xc77\xc0d\x8e\\B\x1a*158\xa1\xc6\xc4\xd3\xf2I\x8dL\xa4]fh\xaf\x8dVj\xf2\xcb\x91\xd8w\xc6 \xf3\xa5\x1f\xe6\xf2\x92\xb8\x95\x15\xbff\xc9y\x0f\xfe%C<=\x97\xc78\xe8\xa2l\xe9\xec,h\xba2Z\xa6\xfb\x88\x8c\xd8~\xe7{,)d\x8b\xd6\xb3\n\npZn$\xf4|@}x\xeaH\x98\xd1\xb5\xbb\xdc\xf2s\x1aL\xd9\x8c{rl\x11\xd0\xac\xd1\nI\x84\xf7\xde\xbeXC~:#m\x91WE\xc75\xb8sJ\xb9##j!3\x1d\xdb\x93bh\xab,\"SJ`1\xb53\xb3\xef\xa8a\x9a\x1cf\xf6\x82,\x98\xb5\xc0\xb6\xcah\xbcs\xf3\x8c \xe7+\x9e\xaf\xb9\xa3\xad\xa2P\x1dz\xcf\xc9\xf2\xfe\xce\x13\xfb\xe9z\xd1\xad\xa4!\xfbb6\xb7\xd30\x9c.\xf2\xb6\xc1\xd0WOC3`\xc2o\x05\x90\x89]\x07\x9c\x80\x82g\x9a\xe5\xf2\xc9s\xde\xde\xf4DU|\xff+\xbeH\x0c\x87%m`\xf4\x99\x8ds\xd3\xa3=\xf5\xcamL\x1f\xca\x8aq\x91\xf8uA\xce\xc7\xe8\x84\xfeB ;-\x9b<\x7f\x16:a\x08F<4\xcc6\xd3\\8.\xec@\x94\x13\xa3\xf93y\x945\x10 \x9f'\xbeg_\xd5\xe5\x80\xe4[\xe2\xd7\x155\xb9;\x9aEo\x1e\x11\xe7Dtk7|\xe8|\xc3^\x0f\x86\x08\xa2\xd7\xd3\n\xf5\x12\x88\xb3\xef(\xf2Z\x837_\x14\xc7|1\xc7\x8b\xff\x16\xd7-\x0c\xee\x91\xdc\x06N\xcfk\xe7\x12\x89i\xc6Lt\xe8Q\x9c\x1f\x0dl3\\\x1eY\xe1\x10w\xdc\x8dm;\xfb(\xae\x19\x04Sf\x0dsK\xb4\xda\xe5\x0cJh\xab\xe9J\xe4\xc9\xb7\x0c\xcb\xeb\xb2\xb3*\xf7*1g\xba\x14\xc9q\x85,\x92\xb8E\xda\xf2fvk\xbe\\\x04\xaf$\x12\xa2\xeevh\xf2\xa3\xda\x92\xadK\xb46Y\xc2\xa7\x8d\x90\xb8\\\xf6-F\x8f\x18\xa5x\xf6]e\x80\xbe\x12\x14%\x0f\xaf8\xfa|I\xfc\x0eS\xd0\x01\x13\xc1\xa0\xaf\x8b\xa8\x99\x14\xcc\x1c\xc1\xef;v\xec*\xcf\x1cA\xa5\x80\xa97Br5\x96\xfd\x1e_\xf9\xd6\x95\xda\xaeu\xba,\x0e\xf1\x0e\xa1Tdn;\x11j\x13\xc1(\xa7\xaer\x9b\x97\xc5\xb0\x9b\x8bMe\xb3\xdc\x90H\xae=\xea\x95\xcfaCvB{h.%\xc3^\x8f\xcbl\xda\xb4\xdc\x06|e\xa3\xe2tC\x89m\xbb\x10B\xf4\x1b\xac\xe8\xb8[\x96\xc7\xe5\xe6\xd4\xdd\xd2\x1b\x7fI\xcfW\x0f\x9al\xeb\xe3G`\x9e[\x17.\x7f\xb4\"O!\xd8.\xc9\x01\x11\xf6\xe5Z\xca'\xbc\xd0\x93\xf9\xe5QChx\xb2,\x9a\xb2\x08.A5\xe3S\xc4\x06I4[h\xa5\xd5J\x11UUh\xa8w\xeb\xd0W\x15VR\xdb\xd0\xa3V\xc4\xbb\x80\xf4\xf7\xc4c\x16<\xb3\x10\xf2\xa5x^\x128J2B\x15*\xde\xe4\x18\x9cI\x828\xbd\xd5\xb8g\xad\x1bf\x99\xeeR\xdc\xd3\xd2\xb3\xdb\x86_\xcb\xda\xd9\xcb\x97\x9a\x87\x96\x97\xd2\x0bF\xbe\x85\x82~.]x\xa8`\x0dO&/l\x83\x88\xbb\\\xb7\x0dd\xe6\x0cUL\xaa\";\x10\xa6[\x96\xec\x03\xde\x02\x08j<\x11\x8a\xf0\x9a\x16U\xb6+n\x9b`D\"\x886\x9d;g\x1d_\xfaL#\xe8[7\xc5\x83\x17\xbf\xa3R\xe4\xab\x82j\x88\x90\xba\xf9}F\xf3}\xa9\xafb\xe7e!\xdd\xb3\x0c`\x99\xb1\xf6\xdfhL\xe4\xa1\x91\xbdN'\xec\xaa\x8aNmW+\xff^\x1a.\x0e1X\x1c/\xf2\xcd\x91\x04\x92\xf8\xea9\xf3\xb5\xfb\x852\x1cW\x94\x05_\xdf9\xc4\x1e>\xfd6\xdd\x8c\xa5ZAu\xd2o+\xafJ \xa1\x1a\xedJ\x0b,JC\x99i\xd5\xfa=R\x98h!\xb6`\xee R\x8b\x15\x9e\xd5{\x1e\x9d\x80\x87\xa1\xde\xe6\xf2\xb0\xabk\xd4\x87L\xe9\xeb\x12>'\xc3\xd5\xe4\xe9\x18\xee#\x98\x8c.p\xc5FQ?\xd6\x1e\x9b\xc2E\xd0\xd9\x98PG\xfb\x14\xd5\xcdt3\xf6p\xae\xfem\xa6vkO\xc8%\xfd\xaa\xd0\xed\xcb%\x98U\x81\x87V\x91\x0d\xd1\xbf-\x90\x7f\xf5Y\x12\xc1\xd8\xddJ^\xe5\x90\xf7*^4\xc3\xd8\x9b&\xaf\x96\x9f\x10\xb2\xd2]X\xb1\x80r\xc9\xe0s\xbb\xc1\xba['\xeel3\xd3\xf5\xc2\x03\x88*C\xbdo\x7f\xec\xf3tF\xa3\x00t\xfc\xad/\x8b\xdd\x9c\xca\x18\x0e.J,\xee\xd5\xc6\xf2Y\x7f\x9a\xad\xd1\xda;0\x95\xafRX\x1e\xecdC\"[A\xae\xb40Q\x18\xafy%\xb6\x82\x1e\xa8\x1c\x8b\xa7\xf7\x99\xcc\xd09\xe7\xfdW\xa1Z\xb6\xb5\xa4\x84?\xcb0X\x0d8\\\xf8\xd7\x05ZE\xba\xa1o\xd7Q\xad\xf7\x89*n\x9bIi\xb0\x1327S+\xeeb$\xab\xd4\x05\"\x02\xf7(\x86\x04\xed^/HF\x1bJ\xf0\x00\x19\xa0e\xa42\xf7\xecb\x9c2\xd1P\xc8\xe8\xc8\xc4\x13#u};\xc0\xe5v\xe3s\x01Gv:\xc7k\xa9[\x08\x81\xd7?\x0e\x8a\xd6\xcd\xb6\xde\xe0\xa5\xbc\x13\xe7\xb2\xc7\x98\x9a\xb3\xfd\xa1\xe54\xce\x84aC\x08d \x0e\xe0n\x0c\xd4\\\x06t\x0f\x1d\xaf8\x81\xad\xf4{>\x00~\xc8Q\xc7pG\x99\x8b\xa6\x96lv&\xc2\xbd\xb2\x93&\x7f\xbc\xd2\xbe\x0d\xeb\xb8f\xaa\x02\xad\xb4\x1e\xb9\xcd\x95?u/vL\xbc\x9c\xa0\xb8.5\x89\x13\x02X\xb1\x98l\x98\x97\xe9b\x86L*\xdb\\\xa8(-\x88\x03\xe2\xccc(\xbb\x91\xa8\xde\xb1\xfb\xd0\x9b-7\xaa\x9e4\xaf\xcb\x9cv:i\xcdbB\xb2\xac\x9bd\xfauKP\xcb\xea\xdf\xf9\x84O\xf7\x82\xc0\xe0\xb9\xb4\xaeo#\xb0\x84\xd5\xbf\x03_z2\x99\xd2\x02\xa15\x97q\xbb\x0e\xacgu\x15\xc93Y\x04\xd8Y\xfa\x9eR%\xa3\xb1\xd8\x9f\x1dE\x84\xad\x07\xe4r\xa9K\x92\x04:\xde\xc0~\x04\x9fc\xf8\xd5Z\xbe\xd25^\\\xb7V1\xc8\xd3\x99L\xbeK\xc45\x99\xcfFR|\xc3\x8cw\xebh\xec\xfe+\xdb\xfe\xeb\xc3\xe1\xe8~\xcdj\xec8\x9c\xe1Gi\xc8\xf2\x10s\x18\x03\xe7\xb6^\x1a\xbb\xa5Wv\xa1\xc6\xf6\x9f{.\x03\xca\x7fP\x85\xdc\xdf\x1f\x99\x8c C\x15<\xa7\x8a\xf32\xd0\xf6\x11|>\xa4\x82\x8cH\x83Q\x96\xd0M\xa7\xd3#ix\xe36\xa4\x84=dm0\xad\x8e\xcag\xbeK\x86c\xfb\x926i\x16\xda\xfa\n\xbb\x116\x94\x84\xcf\xdbY\xe7\x15\xbeFN\xd9\xad\x17\xe38s,s\xef\xbd\x08/.\xbd=\xd2\x9fO\xa8\xd3\xbbD\xd2v\x19n\x07\xe6\xc4\xe9\x85\x1f \x12#\x81Q\x82\xb3A\xfb\xbe\xcf\xe2e.\x89W\xf76\x15\xc6\xdf\xca\xc6\x10\x04\xf8\x9a\x88\xa8\xe4\x00\xdd\xa6\x1d.z\x9e+\xb2\x8e\x81\n\x12\x01\xd5_\xd7\x8ea9N?\xc9\xa8\xcf\xc2\\\xa7\x92\x94\xb5db\x90\xef1\xf1\x92\xe6\xc2\xaf 2\xbeU\xd9\xe3\x92d\xbfQ9\xea\x0f\x85|\xab2\xb7\xd9\x11\x14\xccX\xecY&\x01[\xd9\xcb\xab\x0e\xd8*\x85Z\xe4\xe7\xbd\xedk{J\x81\xe4\xa6\xa6U\xa9\x0f\x9d\xe5\xfd2\xc3\x11\xaew6\x11X\xf7\xbb\xea'd\x7f\x15'\x88\xbf3W\xeb\xbeU\xdaM\xb6S\x93\xd7O\xaa\x16\xe32\xa2-\xb0}\x9f\xe6\xd0\x8d\xed\x9d\xdb+\x92\x0c\x1fq\x91\x1b\x03\x83\x1b\xd0\xd9U\xf6\x1e\xce\xa9\xc0\xd0r\xa2\xe5*$\xb7)[%\xc6$=r\xc1:\"*3\x1ar\x90\xbd\xcd\\\x08k\xf7\xe8\xcfr\x7f\x9b\xc5S\xe1z\x06\xa0\xdb\xc8\x0e\xa2\xe3\xb7\x00?=\xd2\xc4\x02\xd2,\xc8\xd6\xdf\x97Q\xc1\xe9\x17\xb9Db\x80\xc7\xae\x1c\xcd\x94\x8b`\x1e\x9c6w\x8b\xc1\xc6\xc4\xc1\xe4\x12\x1a\xd4\x87b\x0c\xd5\xde\x85\xea\xf5\x91)\x14c\xdf\xf2\xc1$\x16sU\xe4\xf7\xa1?\xe5I\xac\"z\xc3\x90H,\x85\xe9X\x96\xda\xa5\xe3\xc7\xf6\x18\x01\xe5D\xe2\x1e\xb1\x93\xce\xcb<:D\x0c\xfe\x8a\\\xc3\xa20\"\x98\x8f\xcf\xde\xc4W\xeb\x1d\x99U\xe9YJ\xb2Q\x86mF\xa5\xa0\x0f\xceZ%\x82P\xbe\x17v\xc45#\xad\xe3\x15\xb3|\xf5\xcc|1\xa6\x04\xca)\xff\xd2P\x90\x91\x0f\x01Mu\x9a\xd6\xe7a\xb0k\xc9\x16\xae\x8e\xf3\xe5\x02\x95\x0c\x176lL\x08u\x96.O\xb1 l\x18#Ee\\\xf6\xea\xedak\x9fVW)d\xe3\x97\xc5\xfa \xe1q.]\xd2\xc1\x16\x86\xb0'/U\xfa\x81lPP_]\xb3\xa5\x04qY\x88\x1c\x86\x98\xf2\xaauT\x0d\\\xe2\xd6\x16\x0b\xd2\xaap-\x9e\xd1j\xef\xf0\x93\xbb#\xfe\xda\xdd\xd8\x0e\xcb\x81\xech\xea\xeb\xb4V\xcc#'t|`\x13\x9e\xc7\n\x97\xf9|\xa0D\xe7\x10\xd9e8\x06\x0bI/\xa9\xd6\xd4{\x19 )D\xc5N\x87\x91\xe6gU\x10\xacx\xfa* 3+\xa1\xe4\xe3\x9c\xc4\x18\xdbo\xf3z\xa6\xaf\xdb\xf5>\x0ffZ\x96\xcf\x1f]\xb7m\x1b6\x87\xc7\xbfs\xa7\"\xd2\xcd\xeb\xd9\xd0v\xf9X\xa6\xc1\xa874\x9d.\xe7ii\xbc^N\xdb\xcf\xc7\xd7`\x84\x84\xd4\x93z\xf3\xfe\xf6\xf9g\x99\xe8c0\xbf\xadE\xce\xa4#\x07\x04%a+\xe4\xe1\xfb\xb2[\x99\x0bR\xc5\xf6\xf0!\xf0\x81\xdc\xd0\xca~s\x88\x96\"\x9c\x1f-$\x91\x90Ev\xe8c\xdd\xdbR4I\xfc\x14\xf9\xe5p\xbbR\x85b\xf3\xcbf\x88\xad\xeeS\xfd\xbe\xb9\xdf\xb8H\xa1\x1f-Jl\x85+q%6\xaaK\xf9b\x8d\xde\xde\xf9\xe5\xcc\x15\xab\xc3/g,\x198[\x82\x84\xef\xa3\x95\xf6\x9bP\x86u\xabC\n#+<#\x05\xf6p\xac\x88\x1c`={\xe4\xd8\xeeZX%\x87\x98\xd8\"f\xaan!\x84V\x04\x85\xf4\xe5G\xa0Zt\x8f\xbe\x9c\xcc\xa4\x10= \xf6\xfb\xb3722\xb4\x94\x07\xa1\xd5\x03)3\xc4\xaf\xec!\xa9&\xed\x95\x14\xd9\xb3\xc2\xba\xd2\xbf\x06G#\x14Y\xe129\xf4\x9e,l\xcd/\x159\x95\x08\x91\x02$s\x82\x0f\x95v\xd3\x1d\xf3\xf1\xf7r\xbd\x0e\xe6\xfe\xb8\x9fL\x9d\x15!\x99\xd4\xaf\xea\x7fI=T\xc6\xee\xfd\x7f\x00\x00\x00\xff\xffPK\x07\x08uq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00-\xb7EP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01VH;^\xbcX[o\xdc\xbc\x11}\xf7\xaf`\xd7\xf8\x00;\x10\x15]V\xbb\x8e\xf2\xd6\x14A\x0b\xd4yH\xd0\x87>R\xd2h\xc5\x9a\"\x05\x92\xf2\xaeS\xe4\xbf\x17\xbc\xe8\xba\xbb\xb6\x9b\x87/\x88\x01\x8b\xd7\x993g\xce\x0c\xfd\x01\xfd\xf7\x06\xa1\x96\xc8\x03\xe59\x8a>\xdf \xd4\x91\xaa\xa2\xfc\xe0\xbf\xf0\x11\x8a'\xaaq-\xb8\xc6\xaa\x15B7v\x92pM \xa3DAe\x97\xb5\xe2'\x16\xeat\xb6\xee \xc9\x8b* \x83\xf9a\x1aN\x1a+\xfa\x130\xa9\xfe\xd3+\x9d#.\xb8]Q\x88\x93\x99\xb0[\x0b!+\x90\xb8\x10'3c\x0f\xaeIK\xd9K\x8e0\xe9:\x06X\xbd(\x0dm\x80\xfe\xca(\x7fz$\xe5\x0f\xfb\xfdUp\x1d\xa0\xcd\x0f8\x08@\xff\xfa\xc7&@\xdfE!\xb4\x08n\x10Bh\xf3w`\xcf\xa0iI\xd07\xe8a\x13 E\xb8\xc2\n$\xad\xc7{\x8cm9\x8a%\xb4f\x88Q\x0e\xb8\x01zht\x8e\xe2pkF\x7f\xdd\xdc\x84\x9d\xa4-\x91/\x16\xc2R0!st\xbb\x83m\n\x0ff>df\x83\x9d\xfc\xf8\x01\x91l\x1f\xd75\xfa\xf0qZ+\x0f\xc5]\xbc\xcb\x02\x14\xc7i\x80\x92,\xbb\xb7\xdb*\"\x9f\x86]\xb7\xdb$\xf9\xdbnw\xb6m\xb7\x0b\xd0\xd6\xec\x8c\x92{g\x8b\x85\xb4\x15\\\xa8\x8e\x94`\xf7\xcf<\x89\xc2\x87\xcc;\xb3\xc0\xf1\xc7\xd7G\xc1\x05\xfe\x0e\x87\x9e\x11\x19\xa0G\xe0L\x04\xe8QpR\x8a\x00}\x11\\ FT\x806\xff\xa4\x05H\xa2\xa9\xe0fVl<\x98_D/)H\xf4\x0d\x8e\x9b\x00M\xf7\xff\x85\xb6\x9d\x90\x9apm\xcd+D\xe5`\xaa\xa8\xea\x18y\xc9Q\xcd\xc0\x85\x95\xc1 WTBi\xce\xce\x91\x14G3L\x18=pL5\xb4*G%p\x0d\xd2\xd2\x83\x94O\x07)z^Y\x1c\xc8\x12\xbf\x00EadP\\`\x95\xa6\x01J\xf7\x01\xda\xc6v\xc6\xf0\x8d\xd6/\xb8\x14\\\x03\xd79\xb2\x06\xe3\x02\xf4\x11\x80[ko[B\xf9\xfb\xcc-\x05\xeb[~\xf1\xdc\xc9\xea#\xadt\x93\xa38\x8a\xfe0\x9f-\xe5\x13\x9b\xa2\xe8\xb9q\x97R^\x0bCv{\xf1+\xa7]\x81\xe6\xccT\x9f\xc7\xb8\x10Z\x8b6GI\x98H\xcf\\\xe5\xcc\xff\x7f]\xec\x84\xa2>J\xc0\x88\xa6\xcf6e-\xf3\xacQ9bP\xeb3\x17\x13\x7fk\x13\xafi\x99\x84\x0bV\x1e\xfd\x8em\x14\xad\x0f\x9e\xfcd\xa05Hl\xa2\xe6T*L\xbb\xd3\xb8\\K\xc2U-d\x9b\xa3\xbe\xeb@\x96D\xc1\x8a\x0fq\x1c\x05h\xb7\x0fP\x92\xfa\xe4i\xe2PS\xcd\\\xd6\\\xbeu\x12\xc5polFq\x98d\x03\x9cMr\xb6s\x00b\x90\x85\xd4\xfe{\xd3\xcc\xab\xce\xcdUi\xb8y\x85\xda.\x8b\x9c\x14\x94DV\xd6 /\xa0\x92T\xb4W\xe6\xb0a\xdfjb\xeb\xeep\xa39\x8a\xbb\x13R\x82\xd1\xca\xa5X\x14 \xff?\x8c\x13\x97]\x96\"\x07)\x8e9\x8a\xc7o\xd5H\xca\x9f\xfc\xc8XQ\x10N#w|KN\xd8g\xc2vJ\x84a\xe4\xc1\xaf\x1a\x81\xf6n\xce\xdd\xb5\xc5\xa1!\x95\xb97r\xe6\x98\x15\x91_\xb9\xb66\xda;E\xbd\xa9)\xb0J\x81\x9e\xd5\xba)+\xfc\xbdse\xb9\xad\xcb\xba\xac\xeb\xac\xfa\xedK\xdf\x82x(ws\x89^\x9a\xcaH\x01\xcc\x1a|9\xeb\xdeL[/\xa4C\x16n\x935\xbeQwr%\xfe\x8a\xa0\xbc\"A\xe7\xd9zfz\xce\x85\xbe\xcb\x19Q\x1a\x97\x0de\xd5\xfd\x9c\x91\x03\xf8\xaf0\xed\xfe\x12\x1c\xaa#N\xb4f\xcc\x89\x93l\xe5Y\x84\xe2l& >#\xa5\xb1\xd6\x1eJ\xdbCHK/\x7f\xfe\x14\xd2k1\x87+\xdd\xcd\x036\x8613\xc459\xd6\x82R\xe4\xe0\x14c\xbc8 3\xcf\x8d\xfd\xa8\x0d\xa1u\xc1 \x1f\x83\x93O\x8f\x0b\xb6^R\xc0\xab\x15\xff\x15\xbe\x8a^\x9b~e\xe2X\xd9KeN1`\x98\xefcC5X\x89\xb1\x8b\x8e\x92tv\xdf3\xc8\x9a\x19\x967\xb4\xaa\x80\x8f\xf8M\x13\xc0\x18\xed\x14U\xcb\xd0\x84\n\x18\x94:\xcfI\xadA\xfaf\xc8Sf\xb3\xf9\xbc`0)\x94`\xbd\xb6vy\xe4?9\xf7\x07\xe0=\x1a\xd2}\x0d\xd9\xa9E\xe7\xc1\x1f\x13\xd8\x0e\xe1\x81\xd5\x82\x1anbx\x06\xae\xd5\xe0\xbb\x895\xefz=\xa7\x9e\xd2/l\x06\xce\n-[\x1b\x8d7\xeb\x80\xbdk\xf7\xd4\xe0\x92\xae\x03\" /\xe7s\xa6E\xbe4qi\xec<\x8aC!\x89\xd3\"\xad\xe7q\xf5\xbe\xafIaKLG$\xf8\x16,4\xde\xbc\xaf\xe2{\xe9\xb0\xc3\x86\x1e9\x1aH\xf2\x8e\xe6),z\xad}r\x0dF;~\x9e\xd1v\xe8\x95\xd7\"\xbb\xedNh\xd7\x9d\x9c(dQ\x80\xcc\xcf\xa7\xd4\xeaB|\x1f\x98\xa4\xe9N(\x1dV\xcc\xd5\xf7\xe1\x1d\xe2\x1b\x9d\xa5\xdb\xde\xa5\xdb\xa8\x95\x9euWSi\x06\xf9BO\xc7\x9c\xa9\xa0\x14\xae[\x9e1\xcb#\x136\x84\xd5#\xbf|\x19\x8d>\xaf\xcb\xe84R\x10E\x8d2\x13V\xdee\xd1\x1f\x08\xdb\xbb\xee\x17g\xd6=c\xeb3\xe3\xf9\x8a\xbc1y\xec\xfa\x94\xa9\xf9\xb0\xbf2\xa2\xe1\xdfw8\xb6g\xaec\xb1\xefN(6\x01\x89.\x86\xe3\xde\xbd\x03\"\x1b\x8d\xdd\xb4l\x1d\x93_7\xa1\xa8kl \xe1\xd2qN\x84l\x9ffE\xe6i*\x84\xc9\xe4Q\xa0G\xb6Rn\xdfa\x9a\x14\xee1\xb9\xd0\x818Y\xcaH\xe2c\xbe\x92\xf7g\x90\xe6\xe5\xc7\x86\xa2\xa0E7\x7f>\xf5\x1a\xaa\xe5k\xae\xdcg\xfbj\xf1\x9aY\xbd\xabfJo\xfa.\xec\xcc\xbf\x9cgoe\xcf\xaa\x14\x8f\xcd\xf4\xcc\xd1\xb1\x97\xb2C^%\xf1\xacGZ\xb57\x17\xa6\xcc\xb1\xcb\x89\xeb\xcd\xed\xd5\xaf2\x86\x9f-\x1a\x81o\x80T\xd7\x80\x7fo\x87s\x16\x8f\xdf\x81\xde\x13\xf2O\xc6\xfd\x1d-\xd5\x12z-\xba\xb7p7K\xae\x82n3F\x8e\x7f\xe2\xb8\xd8j\xfd/\x00\x00\xff\xffPK\x07\x08L\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x004\x17bP!yu\xf8Z\x04\x00\x00\x12\x1f\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00html/dashboard.go.htmlUT\x05\x00\x01%v\\^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x04\x00\x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81E\x07\x00\x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x83\xba\x83\xe4\xf5\x00\x00\x00|\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x80 \x00\x00img/error-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029PK\xfe\x8b#M\x03\x00\x00d\x08\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x80\n\x00\x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x14\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029Puq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81x\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00-\xb7EPL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x816\x1f\x00\x00style/main.cssUT\x05\x00\x01VH;^PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00N%\x00\x00\x00\x00"
+ fs.RegisterWithNamespace("web", data)
+ }
+
\ No newline at end of file
diff --git a/internal/frontend/templates.go b/internal/frontend/templates.go
index 6612b5dc3..90851417e 100644
--- a/internal/frontend/templates.go
+++ b/internal/frontend/templates.go
@@ -1,4 +1,4 @@
-//go:generate statik -src=./assets -include=*.svg,*.html,*.css,*.js
+//go:generate statik -src=./assets -include=*.svg,*.html,*.css,*.js -ns web
// Package frontend handles the generation, and instantiation of Pomerium's
// html templates.
@@ -16,10 +16,12 @@ import (
_ "github.com/pomerium/pomerium/internal/frontend/statik" // load static assets
)
+const statikNamespace = "web"
+
// NewTemplates loads pomerium's templates. Panics on failure.
func NewTemplates() (*template.Template, error) {
t := template.New("pomerium-templates")
- statikFS, err := fs.New()
+ statikFS, err := fs.NewWithNamespace(statikNamespace)
if err != nil {
return nil, fmt.Errorf("internal/frontend: error creating new file system: %w", err)
}
@@ -49,7 +51,7 @@ func NewTemplates() (*template.Template, error) {
// MustAssetHandler wraps a call to the embedded static file system and panics
// if the error is non-nil. It is intended for use in variable initializations
func MustAssetHandler() http.Handler {
- statikFS, err := fs.New()
+ statikFS, err := fs.NewWithNamespace(statikNamespace)
if err != nil {
panic(err)
}
diff --git a/internal/frontend/templates_test.go b/internal/frontend/templates_test.go
deleted file mode 100644
index e8a965dac..000000000
--- a/internal/frontend/templates_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package frontend
-
-import (
- "html/template"
- "reflect"
- "testing"
-
- _ "github.com/pomerium/pomerium/internal/frontend/statik"
- "github.com/rakyll/statik/fs"
-)
-
-func TestTemplatesCompile(t *testing.T) {
- templates := template.Must(NewTemplates())
- if templates == nil {
- t.Errorf("unexpected nil value %#v", templates)
- }
-}
-
-func TestNewTemplates(t *testing.T) {
- tests := []struct {
- name string
- testData string
- want *template.Template
- wantErr bool
- }{
- {"empty statik fs", "", nil, true},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- fs.Register(tt.testData)
- got, err := NewTemplates()
- if (err != nil) != tt.wantErr {
- t.Errorf("NewTemplates() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("NewTemplates() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestMustAssetHandler(t *testing.T) {
- tests := []struct {
- name string
- testData string
- wantPanic bool
- }{
- {"empty statik fs", "", true},
- {"empty statik fs", "", false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- defer func() {
- if r := recover(); r == nil {
- t.Errorf("The code did not panic")
- }
- }()
- MustAssetHandler()
-
- })
- }
-}
diff --git a/internal/grpc/authorize/authorize.pb.go b/internal/grpc/authorize/authorize.pb.go
index d6444b04f..16885a3fd 100644
--- a/internal/grpc/authorize/authorize.pb.go
+++ b/internal/grpc/authorize/authorize.pb.go
@@ -163,7 +163,13 @@ func (m *IsAuthorizedRequest_Headers) GetValue() []string {
}
type IsAuthorizedReply struct {
- IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
+ Allow bool `protobuf:"varint,1,opt,name=allow,proto3" json:"allow,omitempty"`
+ SessionExpired bool `protobuf:"varint,2,opt,name=session_expired,json=sessionExpired,proto3" json:"session_expired,omitempty"`
+ DenyReasons []string `protobuf:"bytes,3,rep,name=deny_reasons,json=denyReasons,proto3" json:"deny_reasons,omitempty"`
+ SignedJwt string `protobuf:"bytes,4,opt,name=signed_jwt,json=signedJwt,proto3" json:"signed_jwt,omitempty"`
+ User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
+ Email string `protobuf:"bytes,6,opt,name=email,proto3" json:"email,omitempty"`
+ Groups []string `protobuf:"bytes,7,rep,name=groups,proto3" json:"groups,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -194,89 +200,53 @@ func (m *IsAuthorizedReply) XXX_DiscardUnknown() {
var xxx_messageInfo_IsAuthorizedReply proto.InternalMessageInfo
-func (m *IsAuthorizedReply) GetIsValid() bool {
+func (m *IsAuthorizedReply) GetAllow() bool {
if m != nil {
- return m.IsValid
+ return m.Allow
}
return false
}
-type IsAdminRequest struct {
- UserToken string `protobuf:"bytes,1,opt,name=user_token,json=userToken,proto3" json:"user_token,omitempty"`
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
-}
-
-func (m *IsAdminRequest) Reset() { *m = IsAdminRequest{} }
-func (m *IsAdminRequest) String() string { return proto.CompactTextString(m) }
-func (*IsAdminRequest) ProtoMessage() {}
-func (*IsAdminRequest) Descriptor() ([]byte, []int) {
- return fileDescriptor_ffbc3c71370bee9a, []int{2}
-}
-
-func (m *IsAdminRequest) XXX_Unmarshal(b []byte) error {
- return xxx_messageInfo_IsAdminRequest.Unmarshal(m, b)
-}
-func (m *IsAdminRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
- return xxx_messageInfo_IsAdminRequest.Marshal(b, m, deterministic)
-}
-func (m *IsAdminRequest) XXX_Merge(src proto.Message) {
- xxx_messageInfo_IsAdminRequest.Merge(m, src)
-}
-func (m *IsAdminRequest) XXX_Size() int {
- return xxx_messageInfo_IsAdminRequest.Size(m)
-}
-func (m *IsAdminRequest) XXX_DiscardUnknown() {
- xxx_messageInfo_IsAdminRequest.DiscardUnknown(m)
-}
-
-var xxx_messageInfo_IsAdminRequest proto.InternalMessageInfo
-
-func (m *IsAdminRequest) GetUserToken() string {
+func (m *IsAuthorizedReply) GetSessionExpired() bool {
if m != nil {
- return m.UserToken
+ return m.SessionExpired
+ }
+ return false
+}
+
+func (m *IsAuthorizedReply) GetDenyReasons() []string {
+ if m != nil {
+ return m.DenyReasons
+ }
+ return nil
+}
+
+func (m *IsAuthorizedReply) GetSignedJwt() string {
+ if m != nil {
+ return m.SignedJwt
}
return ""
}
-type IsAdminReply struct {
- IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
-}
-
-func (m *IsAdminReply) Reset() { *m = IsAdminReply{} }
-func (m *IsAdminReply) String() string { return proto.CompactTextString(m) }
-func (*IsAdminReply) ProtoMessage() {}
-func (*IsAdminReply) Descriptor() ([]byte, []int) {
- return fileDescriptor_ffbc3c71370bee9a, []int{3}
-}
-
-func (m *IsAdminReply) XXX_Unmarshal(b []byte) error {
- return xxx_messageInfo_IsAdminReply.Unmarshal(m, b)
-}
-func (m *IsAdminReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
- return xxx_messageInfo_IsAdminReply.Marshal(b, m, deterministic)
-}
-func (m *IsAdminReply) XXX_Merge(src proto.Message) {
- xxx_messageInfo_IsAdminReply.Merge(m, src)
-}
-func (m *IsAdminReply) XXX_Size() int {
- return xxx_messageInfo_IsAdminReply.Size(m)
-}
-func (m *IsAdminReply) XXX_DiscardUnknown() {
- xxx_messageInfo_IsAdminReply.DiscardUnknown(m)
-}
-
-var xxx_messageInfo_IsAdminReply proto.InternalMessageInfo
-
-func (m *IsAdminReply) GetIsValid() bool {
+func (m *IsAuthorizedReply) GetUser() string {
if m != nil {
- return m.IsValid
+ return m.User
}
- return false
+ return ""
+}
+
+func (m *IsAuthorizedReply) GetEmail() string {
+ if m != nil {
+ return m.Email
+ }
+ return ""
+}
+
+func (m *IsAuthorizedReply) GetGroups() []string {
+ if m != nil {
+ return m.Groups
+ }
+ return nil
}
func init() {
@@ -284,62 +254,63 @@ func init() {
proto.RegisterMapType((map[string]*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.RequestHeadersEntry")
proto.RegisterType((*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.Headers")
proto.RegisterType((*IsAuthorizedReply)(nil), "authorize.IsAuthorizedReply")
- proto.RegisterType((*IsAdminRequest)(nil), "authorize.IsAdminRequest")
- proto.RegisterType((*IsAdminReply)(nil), "authorize.IsAdminReply")
}
-func init() { proto.RegisterFile("authorize.proto", fileDescriptor_ffbc3c71370bee9a) }
+func init() {
+ proto.RegisterFile("authorize.proto", fileDescriptor_ffbc3c71370bee9a)
+}
var fileDescriptor_ffbc3c71370bee9a = []byte{
- // 390 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0xc1, 0x8e, 0xda, 0x30,
- 0x10, 0x86, 0x1b, 0x52, 0x08, 0x19, 0x28, 0x14, 0x53, 0xa9, 0x26, 0x6a, 0x0b, 0x8d, 0xd4, 0x8a,
- 0x5e, 0x52, 0x29, 0xbd, 0x54, 0x55, 0xa5, 0x8a, 0x43, 0x25, 0x38, 0xb4, 0x87, 0xa8, 0xed, 0xa5,
- 0x87, 0x28, 0x2b, 0x5b, 0xc2, 0x22, 0x60, 0xd6, 0x76, 0x90, 0xb2, 0xef, 0xb2, 0xef, 0xb7, 0x8f,
- 0xb1, 0x8a, 0xb1, 0xc3, 0xb2, 0x62, 0xd9, 0x3d, 0xe1, 0xf9, 0xe7, 0x9b, 0xdf, 0xe3, 0x5f, 0x04,
- 0xfa, 0x59, 0xa1, 0x96, 0x5c, 0xb0, 0x2b, 0x1a, 0x6d, 0x05, 0x57, 0x1c, 0xf9, 0xb5, 0x10, 0xde,
- 0xb8, 0x30, 0x5c, 0xc8, 0x99, 0xad, 0x49, 0x42, 0x2f, 0x0b, 0x2a, 0x15, 0x7a, 0x0b, 0x50, 0x48,
- 0x2a, 0x52, 0xc5, 0x57, 0x74, 0x83, 0x9d, 0x89, 0x33, 0xf5, 0x13, 0xbf, 0x52, 0xfe, 0x54, 0x02,
- 0xfa, 0x00, 0x3d, 0xb1, 0x27, 0xd3, 0x35, 0x55, 0x4b, 0x4e, 0x70, 0x43, 0x23, 0x2f, 0x8c, 0xfa,
- 0x4b, 0x8b, 0x68, 0x0c, 0x1d, 0x8b, 0x15, 0x22, 0xc7, 0xae, 0x66, 0xc0, 0x48, 0x7f, 0x45, 0x8e,
- 0xde, 0x43, 0xd7, 0x02, 0x4b, 0x2e, 0x15, 0x7e, 0xae, 0x09, 0x3b, 0x34, 0xe7, 0x52, 0xa1, 0x08,
- 0x86, 0x16, 0x39, 0x78, 0x31, 0xdc, 0xd4, 0xe4, 0xc0, 0x48, 0x89, 0xb5, 0x64, 0xc7, 0xfc, 0x9a,
- 0x2b, 0x9a, 0x66, 0x84, 0x08, 0xdc, 0xba, 0xc7, 0x57, 0x9d, 0x19, 0x21, 0x02, 0xfd, 0x87, 0x7e,
- 0xbd, 0x02, 0xcd, 0x08, 0x15, 0x12, 0x7b, 0x13, 0x77, 0xda, 0x89, 0xe3, 0xe8, 0x90, 0xdb, 0x89,
- 0x88, 0x22, 0xf3, 0x3b, 0xdf, 0x0f, 0xfd, 0xdc, 0x28, 0x51, 0x26, 0x36, 0x15, 0x23, 0x06, 0x63,
- 0xf0, 0xcc, 0x11, 0xbd, 0x82, 0xe6, 0x2e, 0xcb, 0x0b, 0x8a, 0x9d, 0x89, 0x3b, 0xf5, 0x93, 0x7d,
- 0x11, 0x30, 0x18, 0x9e, 0xf0, 0x41, 0x2f, 0xc1, 0x5d, 0xd1, 0xd2, 0xe4, 0x5e, 0x1d, 0xd1, 0x77,
- 0x3b, 0x5e, 0x05, 0xdd, 0x89, 0x3f, 0x3e, 0xb2, 0x9c, 0x71, 0x33, 0xd7, 0x7c, 0x6b, 0x7c, 0x75,
- 0xc2, 0x08, 0x06, 0xc7, 0xe4, 0x36, 0x2f, 0xd1, 0x08, 0xda, 0x4c, 0xa6, 0xbb, 0x2c, 0x67, 0x44,
- 0xdf, 0xd6, 0x4e, 0x3c, 0x26, 0xff, 0x55, 0x65, 0xf8, 0x19, 0x7a, 0x0b, 0x39, 0x23, 0x6b, 0xb6,
- 0x79, 0xda, 0x9f, 0x22, 0xfc, 0x04, 0xdd, 0x7a, 0xe0, 0xbc, 0x77, 0x7c, 0xed, 0x00, 0xd4, 0xab,
- 0x08, 0xf4, 0x5b, 0x4f, 0xd6, 0xab, 0xa1, 0x77, 0xe7, 0x5f, 0x17, 0xbc, 0x79, 0xb0, 0xbf, 0xcd,
- 0xcb, 0xf0, 0x19, 0xfa, 0x01, 0x9e, 0xd9, 0x04, 0x8d, 0x8e, 0xd1, 0x3b, 0xcf, 0x09, 0x5e, 0x9f,
- 0x6a, 0x69, 0x83, 0x8b, 0x96, 0xfe, 0x50, 0xbe, 0xdc, 0x06, 0x00, 0x00, 0xff, 0xff, 0x72, 0x3a,
- 0xa3, 0xe0, 0x3b, 0x03, 0x00, 0x00,
+ // 431 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x93, 0xcf, 0x6e, 0x13, 0x31,
+ 0x10, 0xc6, 0xd9, 0x6e, 0x9b, 0x76, 0x27, 0xa5, 0xa1, 0x13, 0x84, 0xac, 0x08, 0x68, 0x88, 0x04,
+ 0xe4, 0x94, 0x43, 0xb8, 0x20, 0xc4, 0xa5, 0x87, 0x4a, 0x05, 0x09, 0x0e, 0x16, 0x9c, 0x40, 0x5a,
+ 0x2d, 0xf2, 0xa8, 0x59, 0xea, 0xac, 0x17, 0xdb, 0x4b, 0x58, 0x1e, 0x94, 0x67, 0xe0, 0x31, 0x90,
+ 0xff, 0xa5, 0x14, 0x15, 0x38, 0xad, 0xe7, 0xe7, 0xcf, 0xe3, 0x99, 0xf9, 0xbc, 0x30, 0xaa, 0x3a,
+ 0xbb, 0x52, 0xba, 0xfe, 0x4e, 0x8b, 0x56, 0x2b, 0xab, 0xb0, 0xd8, 0x82, 0xd9, 0xcf, 0x1c, 0xc6,
+ 0xaf, 0xcc, 0x69, 0x8a, 0x05, 0xa7, 0x2f, 0x1d, 0x19, 0x8b, 0x0f, 0x00, 0x3a, 0x43, 0xba, 0xb4,
+ 0xea, 0x92, 0x1a, 0x96, 0x4d, 0xb3, 0x79, 0xc1, 0x0b, 0x47, 0xde, 0x39, 0x80, 0x8f, 0xe1, 0x48,
+ 0x07, 0x65, 0xb9, 0x26, 0xbb, 0x52, 0x82, 0xed, 0x78, 0xc9, 0xed, 0x48, 0xdf, 0x78, 0x88, 0x27,
+ 0x30, 0x4c, 0xb2, 0x4e, 0x4b, 0x96, 0x7b, 0x0d, 0x44, 0xf4, 0x5e, 0x4b, 0x7c, 0x04, 0x87, 0x49,
+ 0xb0, 0x52, 0xc6, 0xb2, 0x5d, 0xaf, 0x48, 0x87, 0xce, 0x95, 0xb1, 0xb8, 0x80, 0x71, 0x92, 0x5c,
+ 0xe5, 0xaa, 0xd9, 0x9e, 0x57, 0x1e, 0x47, 0xc4, 0x53, 0xca, 0xfa, 0xba, 0x7e, 0xad, 0x2c, 0x95,
+ 0x95, 0x10, 0x9a, 0x0d, 0xfe, 0xd0, 0xbb, 0x9d, 0x53, 0x21, 0x34, 0x7e, 0x80, 0xd1, 0xb6, 0x04,
+ 0xaa, 0x04, 0x69, 0xc3, 0xf6, 0xa7, 0xf9, 0x7c, 0xb8, 0x5c, 0x2e, 0xae, 0xe6, 0x76, 0xc3, 0x88,
+ 0x16, 0xf1, 0x7b, 0x1e, 0x0e, 0x9d, 0x35, 0x56, 0xf7, 0x3c, 0x4d, 0x25, 0xc2, 0xc9, 0x09, 0xec,
+ 0xc7, 0x25, 0xde, 0x85, 0xbd, 0xaf, 0x95, 0xec, 0x88, 0x65, 0xd3, 0x7c, 0x5e, 0xf0, 0x10, 0x4c,
+ 0x6a, 0x18, 0xdf, 0x90, 0x07, 0xef, 0x40, 0x7e, 0x49, 0x7d, 0x9c, 0xbb, 0x5b, 0xe2, 0xcb, 0x74,
+ 0xdc, 0x0d, 0x7a, 0xb8, 0x7c, 0xf2, 0x9f, 0xe2, 0x62, 0xb6, 0x78, 0xcd, 0x8b, 0x9d, 0xe7, 0xd9,
+ 0xec, 0x47, 0x06, 0xc7, 0xd7, 0xa5, 0xad, 0xec, 0x5d, 0x59, 0x95, 0x94, 0x6a, 0xe3, 0xef, 0x3a,
+ 0xe0, 0x21, 0xc0, 0xa7, 0x30, 0x32, 0x64, 0x4c, 0xad, 0x9a, 0x92, 0xbe, 0xb5, 0xb5, 0xa6, 0x60,
+ 0xf0, 0x01, 0x3f, 0x8a, 0xf8, 0x2c, 0x50, 0x67, 0xa0, 0xa0, 0xa6, 0x2f, 0x35, 0x55, 0x46, 0x35,
+ 0x86, 0xe5, 0xbe, 0xb9, 0xa1, 0x63, 0x3c, 0x20, 0xf7, 0x94, 0x4c, 0x7d, 0xd1, 0x90, 0x28, 0x3f,
+ 0x6f, 0x92, 0xc3, 0x45, 0x20, 0xaf, 0x37, 0x16, 0x11, 0x76, 0xdd, 0xbb, 0x8a, 0x86, 0xfa, 0xb5,
+ 0x2b, 0x8a, 0xd6, 0x55, 0x2d, 0xa3, 0x6b, 0x21, 0xc0, 0x7b, 0x30, 0xb8, 0xd0, 0xaa, 0x6b, 0x83,
+ 0x41, 0x05, 0x8f, 0xd1, 0xf2, 0x23, 0xc0, 0xb6, 0x2b, 0x8d, 0x6f, 0xe1, 0xf0, 0xf7, 0x2e, 0xf1,
+ 0xe1, 0xbf, 0x27, 0x35, 0xb9, 0xff, 0xd7, 0xfd, 0x56, 0xf6, 0xb3, 0x5b, 0x9f, 0x06, 0xfe, 0x9f,
+ 0x79, 0xf6, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x8b, 0x10, 0x59, 0xee, 0x46, 0x03, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
-var _ grpc.ClientConn
+var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
-const _ = grpc.SupportPackageIsVersion4
+const _ = grpc.SupportPackageIsVersion6
// AuthorizerClient is the client API for Authorizer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type AuthorizerClient interface {
IsAuthorized(ctx context.Context, in *IsAuthorizedRequest, opts ...grpc.CallOption) (*IsAuthorizedReply, error)
- IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminReply, error)
}
type authorizerClient struct {
- cc *grpc.ClientConn
+ cc grpc.ClientConnInterface
}
-func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient {
+func NewAuthorizerClient(cc grpc.ClientConnInterface) AuthorizerClient {
return &authorizerClient{cc}
}
@@ -352,19 +323,9 @@ func (c *authorizerClient) IsAuthorized(ctx context.Context, in *IsAuthorizedReq
return out, nil
}
-func (c *authorizerClient) IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminReply, error) {
- out := new(IsAdminReply)
- err := c.cc.Invoke(ctx, "/authorize.Authorizer/IsAdmin", in, out, opts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
// AuthorizerServer is the server API for Authorizer service.
type AuthorizerServer interface {
IsAuthorized(context.Context, *IsAuthorizedRequest) (*IsAuthorizedReply, error)
- IsAdmin(context.Context, *IsAdminRequest) (*IsAdminReply, error)
}
// UnimplementedAuthorizerServer can be embedded to have forward compatible implementations.
@@ -374,9 +335,6 @@ type UnimplementedAuthorizerServer struct {
func (*UnimplementedAuthorizerServer) IsAuthorized(ctx context.Context, req *IsAuthorizedRequest) (*IsAuthorizedReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented")
}
-func (*UnimplementedAuthorizerServer) IsAdmin(ctx context.Context, req *IsAdminRequest) (*IsAdminReply, error) {
- return nil, status.Errorf(codes.Unimplemented, "method IsAdmin not implemented")
-}
func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
s.RegisterService(&_Authorizer_serviceDesc, srv)
@@ -400,24 +358,6 @@ func _Authorizer_IsAuthorized_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
-func _Authorizer_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(IsAdminRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(AuthorizerServer).IsAdmin(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: "/authorize.Authorizer/IsAdmin",
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(AuthorizerServer).IsAdmin(ctx, req.(*IsAdminRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
var _Authorizer_serviceDesc = grpc.ServiceDesc{
ServiceName: "authorize.Authorizer",
HandlerType: (*AuthorizerServer)(nil),
@@ -426,10 +366,6 @@ var _Authorizer_serviceDesc = grpc.ServiceDesc{
MethodName: "IsAuthorized",
Handler: _Authorizer_IsAuthorized_Handler,
},
- {
- MethodName: "IsAdmin",
- Handler: _Authorizer_IsAdmin_Handler,
- },
},
Streams: []grpc.StreamDesc{},
Metadata: "authorize.proto",
diff --git a/internal/grpc/authorize/authorize.proto b/internal/grpc/authorize/authorize.proto
index 41eaab2fd..a34e90105 100644
--- a/internal/grpc/authorize/authorize.proto
+++ b/internal/grpc/authorize/authorize.proto
@@ -4,7 +4,6 @@ package authorize;
service Authorizer {
rpc IsAuthorized(IsAuthorizedRequest) returns (IsAuthorizedReply) {}
- rpc IsAdmin(IsAdminRequest) returns (IsAdminReply) {}
}
message IsAuthorizedRequest {
@@ -30,8 +29,13 @@ message IsAuthorizedRequest {
map request_headers = 7;
}
-message IsAuthorizedReply { bool is_valid = 1; }
+message IsAuthorizedReply {
+ bool allow = 1;
+ bool session_expired = 2; // special case
+ repeated string deny_reasons = 3;
+ string signed_jwt = 4;
+ string user = 5;
+ string email = 6;
+ repeated string groups = 7;
+}
-message IsAdminRequest { string user_token = 1; }
-
-message IsAdminReply { bool is_valid = 1; }
diff --git a/internal/grpc/authorize/client/authorize_client.go b/internal/grpc/authorize/client/authorize_client.go
index 05c4fded5..33f6966c5 100644
--- a/internal/grpc/authorize/client/authorize_client.go
+++ b/internal/grpc/authorize/client/authorize_client.go
@@ -16,31 +16,28 @@ import (
type Authorizer interface {
// Authorize takes a route and user session and returns whether the
// request is valid per access policy
- Authorize(ctx context.Context, user string, r *http.Request) (bool, error)
- // IsAdmin takes a session and returns whether the user is an administrator
- IsAdmin(ctx context.Context, user string) (bool, error)
+ Authorize(ctx context.Context, user string, r *http.Request) (*pb.IsAuthorizedReply, error)
// Close closes the auth connection if any.
Close() error
}
// Client is a gRPC implementation of an authenticator (authorize client)
type Client struct {
- Conn *grpc.ClientConn
+ conn *grpc.ClientConn
client pb.AuthorizerClient
}
// New returns a new authorize service client.
func New(conn *grpc.ClientConn) (p *Client, err error) {
- return &Client{Conn: conn, client: pb.NewAuthorizerClient(conn)}, nil
+ return &Client{conn: conn, client: pb.NewAuthorizerClient(conn)}, nil
}
// Authorize takes a route and user session and returns whether the
// request is valid per access policy
-func (c *Client) Authorize(ctx context.Context, user string, r *http.Request) (bool, error) {
+func (c *Client) Authorize(ctx context.Context, user string, r *http.Request) (*pb.IsAuthorizedReply, error) {
ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.Authorize")
defer span.End()
- // var h map[string]&structpb.ListValue{}
- response, err := c.client.IsAuthorized(ctx, &pb.IsAuthorizedRequest{
+ in := &pb.IsAuthorizedRequest{
UserToken: user,
RequestHost: r.Host,
RequestMethod: r.Method,
@@ -48,25 +45,13 @@ func (c *Client) Authorize(ctx context.Context, user string, r *http.Request) (b
RequestRemoteAddr: r.RemoteAddr,
RequestRequestUri: r.RequestURI,
RequestUrl: r.URL.String(),
- })
- return response.GetIsValid(), err
-}
-
-// IsAdmin takes a route and user session and returns whether the
-// request is valid per access policy
-func (c *Client) IsAdmin(ctx context.Context, user string) (bool, error) {
- ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.IsAdmin")
- defer span.End()
-
- response, err := c.client.IsAdmin(ctx, &pb.IsAdminRequest{
- UserToken: user,
- })
- return response.GetIsValid(), err
+ }
+ return c.client.IsAuthorized(ctx, in)
}
// Close tears down the ClientConn and all underlying connections.
func (c *Client) Close() error {
- return c.Conn.Close()
+ return c.conn.Close()
}
type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers
diff --git a/internal/grpc/authorize/client/mock.go b/internal/grpc/authorize/client/mock.go
index 41e4285b2..814cc4720 100644
--- a/internal/grpc/authorize/client/mock.go
+++ b/internal/grpc/authorize/client/mock.go
@@ -3,13 +3,15 @@ package client
import (
"context"
"net/http"
+
+ pb "github.com/pomerium/pomerium/internal/grpc/authorize"
)
var _ Authorizer = &MockAuthorize{}
// MockAuthorize provides a mocked implementation of the authorizer interface.
type MockAuthorize struct {
- AuthorizeResponse bool
+ AuthorizeResponse *pb.IsAuthorizedReply
AuthorizeError error
IsAdminResponse bool
IsAdminError error
@@ -20,11 +22,6 @@ type MockAuthorize struct {
func (a MockAuthorize) Close() error { return a.CloseError }
// Authorize is a mocked authorizer client function.
-func (a MockAuthorize) Authorize(ctx context.Context, user string, s *http.Request) (bool, error) {
+func (a MockAuthorize) Authorize(ctx context.Context, user string, r *http.Request) (*pb.IsAuthorizedReply, error) {
return a.AuthorizeResponse, a.AuthorizeError
}
-
-// IsAdmin is a mocked IsAdmin function.
-func (a MockAuthorize) IsAdmin(ctx context.Context, user string) (bool, error) {
- return a.IsAdminResponse, a.IsAdminError
-}
diff --git a/internal/grpc/cache/cache.pb.go b/internal/grpc/cache/cache.pb.go
index 47a0230e2..ebb73477f 100644
--- a/internal/grpc/cache/cache.pb.go
+++ b/internal/grpc/cache/cache.pb.go
@@ -195,7 +195,9 @@ func init() {
proto.RegisterType((*SetReply)(nil), "cache.SetReply")
}
-func init() { proto.RegisterFile("cache.proto", fileDescriptor_5fca3b110c9bbf3a) }
+func init() {
+ proto.RegisterFile("cache.proto", fileDescriptor_5fca3b110c9bbf3a)
+}
var fileDescriptor_5fca3b110c9bbf3a = []byte{
// 176 bytes of a gzipped FileDescriptorProto
@@ -214,11 +216,11 @@ var fileDescriptor_5fca3b110c9bbf3a = []byte{
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
-var _ grpc.ClientConn
+var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
-const _ = grpc.SupportPackageIsVersion4
+const _ = grpc.SupportPackageIsVersion6
// CacheClient is the client API for Cache service.
//
@@ -229,10 +231,10 @@ type CacheClient interface {
}
type cacheClient struct {
- cc *grpc.ClientConn
+ cc grpc.ClientConnInterface
}
-func NewCacheClient(cc *grpc.ClientConn) CacheClient {
+func NewCacheClient(cc grpc.ClientConnInterface) CacheClient {
return &cacheClient{cc}
}
diff --git a/internal/httputil/headers.go b/internal/httputil/headers.go
index 5b4de870b..bdcc89bc3 100644
--- a/internal/httputil/headers.go
+++ b/internal/httputil/headers.go
@@ -1,11 +1,14 @@
package httputil // import "github.com/pomerium/pomerium/internal/httputil"
+// Pomerium headers contain information added to a request.
const (
// HeaderPomeriumResponse is set when pomerium itself creates a response,
// as opposed to the downstream application and can be used to distinguish
// between an application error, and a pomerium related error when debugging.
// Especially useful when working with single page apps (SPA).
HeaderPomeriumResponse = "x-pomerium-intercepted-response"
+ // HeaderPomeriumJWTAssertion is the header key containing JWT signed user details.
+ HeaderPomeriumJWTAssertion = "x-pomerium-jwt-assertion"
)
// HeadersContentSecurityPolicy are the content security headers added to the service's handlers
diff --git a/internal/identity/okta.go b/internal/identity/okta.go
index 57ad16539..4e0feb068 100644
--- a/internal/identity/okta.go
+++ b/internal/identity/okta.go
@@ -63,9 +63,8 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
}
userAPI.Path = "/api/v1/users/"
oktaProvider.userAPI = userAPI
-
} else {
- log.Warn().Msg("identity/okta: api token provided, cannot retrieve groups")
+ log.Warn().Msg("identity/okta: api token not set, cannot retrieve groups")
}
return &oktaProvider, nil
diff --git a/internal/sessions/cache/cache_store.go b/internal/sessions/cache/cache_store.go
index db117fb15..0e8408b95 100644
--- a/internal/sessions/cache/cache_store.go
+++ b/internal/sessions/cache/cache_store.go
@@ -51,26 +51,22 @@ func NewStore(o *Options) *Store {
// LoadSession looks for a preset query parameter in the request body
// representing the key to lookup from the cache.
-func (s *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
+func (s *Store) LoadSession(r *http.Request) (string, error) {
// look for our cache's key in the default query param
sessionID := r.URL.Query().Get(s.queryParam)
if sessionID == "" {
- return nil, "", sessions.ErrNoSessionFound
+ return "", sessions.ErrNoSessionFound
}
exists, val, err := s.cache.Get(r.Context(), sessionID)
if err != nil {
log.FromRequest(r).Debug().Msg("sessions/cache: miss, trying wrapped loader")
- return nil, "", err
+ return "", err
}
if !exists {
- return nil, "", sessions.ErrNoSessionFound
+ return "", sessions.ErrNoSessionFound
}
- var session sessions.State
- if err := s.encoder.Unmarshal(val, &session); err != nil {
- log.FromRequest(r).Error().Err(err).Msg("sessions/cache: unmarshal")
- return nil, "", sessions.ErrMalformed
- }
- return &session, string(val), nil
+
+ return string(val), nil
}
// ClearSession clears the session from the wrapped store.
diff --git a/internal/sessions/cache/cache_store_test.go b/internal/sessions/cache/cache_store_test.go
index 6321e26d2..eae675a99 100644
--- a/internal/sessions/cache/cache_store_test.go
+++ b/internal/sessions/cache/cache_store_test.go
@@ -163,13 +163,6 @@ func TestStore_LoadSession(t *testing.T) {
defaultOptions.QueryParam,
&mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}},
true},
- {"unmarshal failure",
- &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"},
- &mockCache{KeyExists: true},
- mock_encoder.Encoder{UnmarshalError: errors.New("err")},
- defaultOptions.QueryParam,
- &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}},
- true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -187,7 +180,7 @@ func TestStore_LoadSession(t *testing.T) {
r.URL.RawQuery = q.Encode()
r.Header.Set("Accept", "application/json")
- _, _, err := s.LoadSession(r)
+ _, err := s.LoadSession(r)
if (err != nil) != tt.wantErr {
t.Errorf("Store.LoadSession() error = %v, wantErr %v", err, tt.wantErr)
return
diff --git a/internal/sessions/cookie/cookie_store.go b/internal/sessions/cookie/cookie_store.go
index 38143dc3b..da44cb1bb 100644
--- a/internal/sessions/cookie/cookie_store.go
+++ b/internal/sessions/cookie/cookie_store.go
@@ -125,10 +125,10 @@ func getCookies(r *http.Request, name string) []*http.Cookie {
}
// LoadSession returns a State from the cookie in the request.
-func (cs *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
+func (cs *Store) LoadSession(r *http.Request) (string, error) {
cookies := getCookies(r, cs.Name)
if len(cookies) == 0 {
- return nil, "", sessions.ErrNoSessionFound
+ return "", sessions.ErrNoSessionFound
}
for _, cookie := range cookies {
jwt := loadChunkedCookie(r, cookie)
@@ -136,10 +136,10 @@ func (cs *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
session := &sessions.State{}
err := cs.decoder.Unmarshal([]byte(jwt), session)
if err == nil {
- return session, jwt, nil
+ return jwt, nil
}
}
- return nil, "", sessions.ErrMalformed
+ return "", sessions.ErrMalformed
}
// SaveSession saves a session state to a request's cookie store.
diff --git a/internal/sessions/cookie/cookie_store_test.go b/internal/sessions/cookie/cookie_store_test.go
index 52cdeaf40..b19f61d60 100644
--- a/internal/sessions/cookie/cookie_store_test.go
+++ b/internal/sessions/cookie/cookie_store_test.go
@@ -99,8 +99,7 @@ func TestStore_SaveSession(t *testing.T) {
t.Fatal(err)
}
tests := []struct {
- name string
- // State *State
+ name string
State interface{}
encoder encoding.Marshaler
decoder encoding.Unmarshaler
@@ -138,16 +137,20 @@ func TestStore_SaveSession(t *testing.T) {
r.AddCookie(cookie)
}
- state, _, err := s.LoadSession(r)
+ enc := ecjson.New(c)
+ jwt, err := s.LoadSession(r)
if (err != nil) != tt.wantLoadErr {
t.Errorf("LoadSession() error = %v, wantErr %v", err, tt.wantLoadErr)
return
}
+ var state sessions.State
+ enc.Unmarshal([]byte(jwt), &state)
+
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(sessions.State{}),
}
if err == nil {
- if diff := cmp.Diff(state, tt.State, cmpOpts...); diff != "" {
+ if diff := cmp.Diff(&state, tt.State, cmpOpts...); diff != "" {
t.Errorf("Store.LoadSession() got = %s", diff)
}
}
diff --git a/internal/sessions/cookie/middleware_test.go b/internal/sessions/cookie/middleware_test.go
index f1fa3cf64..6fa23c279 100644
--- a/internal/sessions/cookie/middleware_test.go
+++ b/internal/sessions/cookie/middleware_test.go
@@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _, err := sessions.FromContext(r.Context())
+ _, err := sessions.FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int
}{
{"good cookie session", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, http.StatusText(http.StatusOK), http.StatusOK},
- {"expired cookie", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: validation failed, token is expired (exp)\n", http.StatusUnauthorized},
- {"malformed cookie", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is malformed\n", http.StatusUnauthorized},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/internal/sessions/header/header_store.go b/internal/sessions/header/header_store.go
index 2e3a09f1e..a572fef09 100644
--- a/internal/sessions/header/header_store.go
+++ b/internal/sessions/header/header_store.go
@@ -42,16 +42,12 @@ func NewStore(enc encoding.Unmarshaler, headerType string) *Store {
}
// LoadSession tries to retrieve the token string from the Authorization header.
-func (as *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
+func (as *Store) LoadSession(r *http.Request) (string, error) {
jwt := TokenFromHeader(r, as.authHeader, as.authType)
if jwt == "" {
- return nil, "", sessions.ErrNoSessionFound
+ return "", sessions.ErrNoSessionFound
}
- var session sessions.State
- if err := as.encoder.Unmarshal([]byte(jwt), &session); err != nil {
- return nil, "", sessions.ErrMalformed
- }
- return &session, jwt, nil
+ return jwt, nil
}
// TokenFromHeader retrieves the value of the authorization header from a given
diff --git a/internal/sessions/header/middleware_test.go b/internal/sessions/header/middleware_test.go
index 3859c8d7a..b9d36a509 100644
--- a/internal/sessions/header/middleware_test.go
+++ b/internal/sessions/header/middleware_test.go
@@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _, err := sessions.FromContext(r.Context())
+ _, err := sessions.FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int
}{
{"good auth header session", "Bearer ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, http.StatusText(http.StatusOK), http.StatusOK},
- {"expired auth header", "Bearer ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: validation failed, token is expired (exp)\n", http.StatusUnauthorized},
- {"malformed auth header", "Bearer ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is malformed\n", http.StatusUnauthorized},
{"empty auth header", "Bearer ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized},
{"bad auth type", "bees ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized},
}
diff --git a/internal/sessions/middleware.go b/internal/sessions/middleware.go
index 9895134b4..d5244d395 100644
--- a/internal/sessions/middleware.go
+++ b/internal/sessions/middleware.go
@@ -4,15 +4,12 @@ import (
"context"
"errors"
"net/http"
-
- "github.com/pomerium/pomerium/internal/urlutil"
)
// Context keys
var (
- SessionCtxKey = &contextKey{"Session"}
- SessionJWTCtxKey = &contextKey{"SessionJWT"}
- ErrorCtxKey = &contextKey{"Error"}
+ SessionCtxKey = &contextKey{"Session"}
+ ErrorCtxKey = &contextKey{"Error"}
)
// RetrieveSession takes a slice of session loaders and tries to find a valid
@@ -27,8 +24,8 @@ func retrieve(s ...SessionLoader) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
hfn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- state, jwt, err := retrieveFromRequest(r, s...)
- ctx = NewContext(ctx, state, jwt, err)
+ jwt, err := retrieveFromRequest(r, s...)
+ ctx = NewContext(ctx, jwt, err)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(hfn)
@@ -37,36 +34,31 @@ func retrieve(s ...SessionLoader) func(http.Handler) http.Handler {
// retrieveFromRequest extracts sessions state from the request by calling
// token find functions in the order they where provided.
-func retrieveFromRequest(r *http.Request, sessions ...SessionLoader) (*State, string, error) {
+func retrieveFromRequest(r *http.Request, sessions ...SessionLoader) (string, error) {
for _, s := range sessions {
- state, jwt, err := s.LoadSession(r)
+ jwt, err := s.LoadSession(r)
if err != nil && !errors.Is(err, ErrNoSessionFound) {
- return state, jwt, err
- }
- if state != nil {
- //todo(bdd): have authz verify
- err := state.Verify(urlutil.StripPort(r.Host))
- return state, jwt, err // N.B.: state is _not_ nil
+ return "", err
+ } else if err == nil {
+ return jwt, nil
}
}
- return nil, "", ErrNoSessionFound
+ return "", ErrNoSessionFound
}
// NewContext sets context values for the user session state and error.
-func NewContext(ctx context.Context, t *State, jwt string, err error) context.Context {
- ctx = context.WithValue(ctx, SessionCtxKey, t)
- ctx = context.WithValue(ctx, SessionJWTCtxKey, jwt)
+func NewContext(ctx context.Context, jwt string, err error) context.Context {
+ ctx = context.WithValue(ctx, SessionCtxKey, jwt)
ctx = context.WithValue(ctx, ErrorCtxKey, err)
return ctx
}
// FromContext retrieves context values for the user session state and error.
-func FromContext(ctx context.Context) (*State, string, error) {
- state, _ := ctx.Value(SessionCtxKey).(*State)
- jwt, _ := ctx.Value(SessionJWTCtxKey).(string)
+func FromContext(ctx context.Context) (string, error) {
+ jwt, _ := ctx.Value(SessionCtxKey).(string)
err, _ := ctx.Value(ErrorCtxKey).(error)
- return state, jwt, err
+ return jwt, err
}
// contextKey is a value for use with context.WithValue. It's used as
@@ -75,7 +67,3 @@ func FromContext(ctx context.Context) (*State, string, error) {
type contextKey struct {
name string
}
-
-func (k *contextKey) String() string {
- return "context value " + k.name
-}
diff --git a/internal/sessions/middleware_test.go b/internal/sessions/middleware_test.go
index 67167cfda..584d7a74e 100644
--- a/internal/sessions/middleware_test.go
+++ b/internal/sessions/middleware_test.go
@@ -1,4 +1,4 @@
-package sessions
+package sessions_test
import (
"context"
@@ -11,22 +11,40 @@ import (
"github.com/google/go-cmp/cmp"
"gopkg.in/square/go-jose.v2/jwt"
+
+ "github.com/pomerium/pomerium/internal/cryptutil"
+ "github.com/pomerium/pomerium/internal/encoding/jws"
+ "github.com/pomerium/pomerium/internal/sessions"
+ "github.com/pomerium/pomerium/internal/sessions/mock"
)
func TestNewContext(t *testing.T) {
tests := []struct {
name string
ctx context.Context
- t *State
+ t *sessions.State
err error
want context.Context
}{
- {"simple", context.Background(), &State{Email: "bdd@pomerium.io"}, nil, nil},
+ {"simple", context.Background(), &sessions.State{Email: "bdd@pomerium.io"}, nil, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- ctxOut := NewContext(tt.ctx, tt.t, "", tt.err)
- stateOut, _, errOut := FromContext(ctxOut)
+ signer, err := jws.NewHS256Signer(cryptutil.NewKey(), "issuer")
+ if err != nil {
+ t.Fatal(err)
+ }
+ jwt, err := signer.Marshal(tt.t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctxOut := sessions.NewContext(tt.ctx, string(jwt), tt.err)
+ out, errOut := sessions.FromContext(ctxOut)
+ var stateOut sessions.State
+ err = signer.Unmarshal([]byte(out), &stateOut)
+ if err != nil {
+ t.Fatal(err)
+ }
if diff := cmp.Diff(tt.t.Email, stateOut.Email); diff != "" {
t.Errorf("NewContext() = %s", diff)
}
@@ -37,29 +55,9 @@ func TestNewContext(t *testing.T) {
}
}
-func Test_contextKey_String(t *testing.T) {
- tests := []struct {
- name string
- keyName string
- want string
- }{
- {"simple example", "test", "context value test"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- k := &contextKey{
- name: tt.keyName,
- }
- if got := k.String(); got != tt.want {
- t.Errorf("contextKey.String() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _, err := FromContext(r.Context())
+ _, err := sessions.FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@@ -68,31 +66,6 @@ func testAuthorizer(next http.Handler) http.Handler {
})
}
-var _ SessionStore = &store{}
-
-// Store is a mock implementation of the SessionStore interface
-type store struct {
- ResponseSession string
- Session *State
- SaveError error
- LoadError error
-}
-
-// ClearSession clears the ResponseSession
-func (ms *store) ClearSession(http.ResponseWriter, *http.Request) {
- ms.ResponseSession = ""
-}
-
-// LoadSession returns the session and a error
-func (ms store) LoadSession(*http.Request) (*State, string, error) {
- return ms.Session, "", ms.LoadError
-}
-
-// SaveSession returns a save error.
-func (ms store) SaveSession(http.ResponseWriter, *http.Request, interface{}) error {
- return ms.SaveError
-}
-
func TestVerifier(t *testing.T) {
fnh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -102,14 +75,13 @@ func TestVerifier(t *testing.T) {
tests := []struct {
name string
- store store
- state State
- wantBody string
+ store mock.Store
+ state sessions.State
wantStatus int
}{
- {"empty session", store{}, State{}, "internal/sessions: session is not found\n", 401},
- {"simple good load", store{Session: &State{Subject: "hi", Expiry: jwt.NewNumericDate(time.Now().Add(time.Second))}}, State{}, "OK", 200},
- {"empty session", store{LoadError: errors.New("err")}, State{}, "err\n", 401},
+ {"empty session", mock.Store{LoadError: sessions.ErrNoSessionFound}, sessions.State{}, 401},
+ {"simple good load", mock.Store{Session: &sessions.State{Subject: "hi", Expiry: jwt.NewNumericDate(time.Now().Add(time.Second))}}, sessions.State{}, 200},
+ {"session error", mock.Store{LoadError: errors.New("err")}, sessions.State{}, 401},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -118,15 +90,11 @@ func TestVerifier(t *testing.T) {
r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder()
- got := RetrieveSession(tt.store)(testAuthorizer((fnh)))
+ got := sessions.RetrieveSession(tt.store)(testAuthorizer((fnh)))
got.ServeHTTP(w, r)
- gotBody := w.Body.String()
gotStatus := w.Result().StatusCode
- if diff := cmp.Diff(gotBody, tt.wantBody); diff != "" {
- t.Errorf("RetrieveSession() = %v", diff)
- }
if diff := cmp.Diff(gotStatus, tt.wantStatus); diff != "" {
t.Errorf("RetrieveSession() = %v", diff)
}
diff --git a/internal/sessions/mock/mock_store.go b/internal/sessions/mock/mock_store.go
index dd13a20b3..3267a1075 100644
--- a/internal/sessions/mock/mock_store.go
+++ b/internal/sessions/mock/mock_store.go
@@ -4,6 +4,8 @@ package mock // import "github.com/pomerium/pomerium/internal/sessions/mock"
import (
"net/http"
+ "github.com/pomerium/pomerium/internal/encoding"
+ "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/sessions"
)
@@ -13,10 +15,11 @@ var _ sessions.SessionLoader = &Store{}
// Store is a mock implementation of the SessionStore interface
type Store struct {
ResponseSession string
- SessionJWT string
Session *sessions.State
SaveError error
LoadError error
+ Secret []byte
+ Encrypted bool
}
// ClearSession clears the ResponseSession
@@ -25,8 +28,11 @@ func (ms *Store) ClearSession(http.ResponseWriter, *http.Request) {
}
// LoadSession returns the session and a error
-func (ms Store) LoadSession(*http.Request) (*sessions.State, string, error) {
- return ms.Session, ms.SessionJWT, ms.LoadError
+func (ms Store) LoadSession(*http.Request) (string, error) {
+ var signer encoding.MarshalUnmarshaler
+ signer, _ = jws.NewHS256Signer(ms.Secret, "mock")
+ jwt, _ := signer.Marshal(ms.Session)
+ return string(jwt), ms.LoadError
}
// SaveSession returns a save error.
diff --git a/internal/sessions/mock/mock_store_test.go b/internal/sessions/mock/mock_store_test.go
index b58386286..9be214834 100644
--- a/internal/sessions/mock/mock_store_test.go
+++ b/internal/sessions/mock/mock_store_test.go
@@ -1,16 +1,17 @@
package mock // import "github.com/pomerium/pomerium/internal/sessions/mock"
import (
- "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/pomerium/pomerium/internal/sessions"
)
func TestStore(t *testing.T) {
tests := []struct {
name string
- mockCSRF *Store
+ store *Store
+ wantLoad string
saveSession *sessions.State
wantLoadErr bool
wantSaveErr bool
@@ -22,26 +23,27 @@ func TestStore(t *testing.T) {
SaveError: nil,
LoadError: nil,
},
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IiIsInByb2dyYW1hdGljIjpmYWxzZSwic3ViIjoiMDEwMSJ9.u0dzrEkbt-Bec7Rq85E8pbglE61D7UqGN33MFtfoCCM",
&sessions.State{Subject: "0101"},
false,
false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- ms := tt.mockCSRF
+ ms := tt.store
err := ms.SaveSession(nil, nil, tt.saveSession)
if (err != nil) != tt.wantSaveErr {
- t.Errorf("MockCSRFStore.GetCSRF() error = %v, wantSaveErr %v", err, tt.wantSaveErr)
+ t.Errorf("mockstore.SaveSession() error = %v, wantSaveErr %v", err, tt.wantSaveErr)
return
}
- got, _, err := ms.LoadSession(nil)
+ got, err := ms.LoadSession(nil)
if (err != nil) != tt.wantLoadErr {
- t.Errorf("MockCSRFStore.GetCSRF() error = %v, wantLoadErr %v", err, tt.wantLoadErr)
+ t.Errorf("mockstore.LoadSession() error = %v, wantLoadErr %v", err, tt.wantLoadErr)
return
}
- if !reflect.DeepEqual(got, tt.mockCSRF.Session) {
- t.Errorf("MockCSRFStore.GetCSRF() = %v, want %v", got, tt.mockCSRF.Session)
+ if diff := cmp.Diff(got, tt.wantLoad); diff != "" {
+ t.Errorf("mockstore.LoadSession() = %v", diff)
}
ms.ClearSession(nil, nil)
if ms.ResponseSession != "" {
diff --git a/internal/sessions/queryparam/middleware_test.go b/internal/sessions/queryparam/middleware_test.go
index 32ea9b3e3..f2009d84c 100644
--- a/internal/sessions/queryparam/middleware_test.go
+++ b/internal/sessions/queryparam/middleware_test.go
@@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _, err := sessions.FromContext(r.Context())
+ _, err := sessions.FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int
}{
{"good auth query param session", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, http.StatusText(http.StatusOK), http.StatusOK},
- {"expired auth query param", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: validation failed, token is expired (exp)\n", http.StatusUnauthorized},
- {"malformed auth query param", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is malformed\n", http.StatusUnauthorized},
{"empty auth query param", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized},
}
for _, tt := range tests {
diff --git a/internal/sessions/queryparam/query_store.go b/internal/sessions/queryparam/query_store.go
index 936b9c564..be6fbb7ca 100644
--- a/internal/sessions/queryparam/query_store.go
+++ b/internal/sessions/queryparam/query_store.go
@@ -41,16 +41,13 @@ func NewStore(enc encoding.MarshalUnmarshaler, qp string) *Store {
}
// LoadSession tries to retrieve the token string from URL query parameters.
-func (qp *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
+func (qp *Store) LoadSession(r *http.Request) (string, error) {
jwt := r.URL.Query().Get(qp.queryParamKey)
if jwt == "" {
- return nil, "", sessions.ErrNoSessionFound
+ return "", sessions.ErrNoSessionFound
}
- var session sessions.State
- if err := qp.decoder.Unmarshal([]byte(jwt), &session); err != nil {
- return nil, "", sessions.ErrMalformed
- }
- return &session, jwt, nil
+
+ return jwt, nil
}
// ClearSession clears the session cookie from a request's query param key `pomerium_session`.
diff --git a/internal/sessions/state.go b/internal/sessions/state.go
index 0bc2543e3..25bdde62a 100644
--- a/internal/sessions/state.go
+++ b/internal/sessions/state.go
@@ -13,11 +13,6 @@ import (
"gopkg.in/square/go-jose.v2/jwt"
)
-const (
- // DefaultLeeway defines the default leeway for matching NotBefore/Expiry claims.
- DefaultLeeway = 1.0 * time.Minute
-)
-
// timeNow is time.Now but pulled out as a variable for tests.
var timeNow = time.Now
@@ -120,20 +115,20 @@ func (s State) RouteSession() *State {
// Verify returns an error if the users's session state is not valid.
func (s *State) Verify(audience string) error {
- if s.NotBefore != nil && timeNow().Add(DefaultLeeway).Before(s.NotBefore.Time()) {
+ if s.NotBefore != nil && timeNow().Before(s.NotBefore.Time()) {
return ErrNotValidYet
}
- if s.Expiry != nil && timeNow().Add(-DefaultLeeway).After(s.Expiry.Time()) {
+ if s.Expiry != nil && timeNow().After(s.Expiry.Time()) {
return ErrExpired
}
- if s.IssuedAt != nil && timeNow().Add(DefaultLeeway).Before(s.IssuedAt.Time()) {
+ if s.IssuedAt != nil && !timeNow().Equal(s.IssuedAt.Time()) && timeNow().Before(s.IssuedAt.Time()) {
return ErrIssuedInTheFuture
}
// if we have an associated access token, check if that token has expired as well
- if s.AccessToken != nil && timeNow().Add(-DefaultLeeway).After(s.AccessToken.Expiry) {
+ if s.AccessToken != nil && timeNow().After(s.AccessToken.Expiry) {
return ErrExpired
}
diff --git a/internal/sessions/store.go b/internal/sessions/store.go
index d5fa5e3da..66e48ea2f 100644
--- a/internal/sessions/store.go
+++ b/internal/sessions/store.go
@@ -15,5 +15,5 @@ type SessionStore interface {
// SessionLoader defines an interface for loading a session.
type SessionLoader interface {
- LoadSession(*http.Request) (*State, string, error)
+ LoadSession(*http.Request) (string, error)
}
diff --git a/proxy/forward_auth.go b/proxy/forward_auth.go
index 7b9137d0b..b9097ef8f 100644
--- a/proxy/forward_auth.go
+++ b/proxy/forward_auth.go
@@ -40,7 +40,6 @@ func (p *Proxy) postSessionSetNOP(w http.ResponseWriter, r *http.Request) error
func (p *Proxy) nginxCallback(w http.ResponseWriter, r *http.Request) error {
encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
-
return httputil.NewError(http.StatusBadRequest, err)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -58,7 +57,6 @@ func (p *Proxy) traefikCallback(w http.ResponseWriter, r *http.Request) error {
encryptedSession := q.Get(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
-
return httputil.NewError(http.StatusBadRequest, err)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -73,6 +71,7 @@ func (p *Proxy) traefikCallback(w http.ResponseWriter, r *http.Request) error {
// 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 {
+ var err error
if status := r.FormValue("auth_status"); status == fmt.Sprint(http.StatusForbidden) {
return httputil.NewError(http.StatusForbidden, errors.New(http.StatusText(http.StatusForbidden)))
}
@@ -80,9 +79,8 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
-
- s, _, err := sessions.FromContext(r.Context())
- if errors.Is(err, sessions.ErrNoSessionFound) || errors.Is(err, sessions.ErrExpired) {
+ jwt, err := sessions.FromContext(r.Context())
+ if err != nil {
if verifyOnly {
return httputil.NewError(http.StatusUnauthorized, err)
}
@@ -94,18 +92,14 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
authN.RawQuery = q.Encode()
httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &authN).String(), http.StatusFound)
return nil
- } else if err != nil {
- return httputil.NewError(http.StatusUnauthorized, err)
}
- // depending on the configuration of the fronting proxy, the request Host
- // and/or `X-Forwarded-Host` may be untrustd or change so we reverify
- // the session's validity against the supplied uri
- if err := s.Verify(uri.Hostname()); err != nil {
- return httputil.NewError(http.StatusUnauthorized, err)
+ var s sessions.State
+ if err := p.encoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
}
- p.addPomeriumHeaders(w, r)
+
r.Host = uri.Host
- if err := p.authorize(r); err != nil {
+ if err := p.authorize(w, r); err != nil {
return err
}
diff --git a/proxy/forward_auth_test.go b/proxy/forward_auth_test.go
index 66ed2bc35..fe8ddc446 100644
--- a/proxy/forward_auth_test.go
+++ b/proxy/forward_auth_test.go
@@ -13,7 +13,9 @@ import (
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding"
+ "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/encoding/mock"
+ pb "github.com/pomerium/pomerium/internal/grpc/authorize"
"github.com/pomerium/pomerium/internal/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
@@ -42,29 +44,24 @@ func TestProxy_ForwardAuth(t *testing.T) {
wantStatus int
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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusOK, ""},
- {"bad claim", opts, nil, http.MethodGet, nil, nil, "/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{LoadError: sessions.ErrInvalidAudience}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"},
- {"not authorized", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"},
- {"not authorized verify endpoint", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"},
- {"not authorized expired, redirect to auth", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusFound, ""},
- {"not authorized expired, don't redirect!", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"},
+ {"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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusOK, ""},
+ {"bad naked domain uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"},
+ {"not authorized", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: false}}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: request denied\"}\n"},
+ {"not authorized verify endpoint", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: false}}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: request denied\"}\n"},
{"not authorized because of error", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeError: errors.New("authz error")}, http.StatusInternalServerError, "{\"Status\":500,\"Error\":\"Internal Server Error: authz error\"}\n"},
- {"not authorized expired, do not redirect to auth", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"},
- {"not authorized, bad audience request uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"not.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"},
- {"not authorized, bad audience verify uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://fwdauth.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"some.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"},
+ {"expired", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: false}}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: request denied\"}\n"},
// 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"bad traefik callback bad url", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: urlutil.QuerySessionEncrypted + ""}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- // nginx
- {"good nginx callback redirect", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""},
- {"good nginx callback set session okay but return unauthorized", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, ""},
- {"bad nginx callback failed to set session", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString + "nope"}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
+ {"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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, 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{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"bad traefik callback bad url", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: urlutil.QuerySessionEncrypted + ""}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ // // nginx
+ {"good nginx callback redirect", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusFound, ""},
+ {"good nginx callback set session okay but return unauthorized", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusUnauthorized, ""},
+ {"bad nginx callback failed to set session", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString + "nope"}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -72,9 +69,13 @@ func TestProxy_ForwardAuth(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- p.encoder = tt.cipher
p.sessionStore = tt.sessionStore
p.AuthorizeClient = tt.authorizer
+ signer, err := jws.NewHS256Signer(nil, "mock")
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.encoder = signer
p.UpdateOptions(tt.options)
uri, err := url.Parse(tt.requestURI)
if err != nil {
@@ -91,10 +92,10 @@ func TestProxy_ForwardAuth(t *testing.T) {
uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(tt.method, uri.String(), nil)
- state, _, _ := tt.sessionStore.LoadSession(r)
+ state, _ := tt.sessionStore.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
if len(tt.headers) != 0 {
diff --git a/proxy/handlers.go b/proxy/handlers.go
index 78c9c716e..2d0eb0cfe 100644
--- a/proxy/handlers.go
+++ b/proxy/handlers.go
@@ -33,8 +33,11 @@ func (p *Proxy) registerDashboardHandlers(r *mux.Router) *mux.Router {
))
// dashboard endpoints can be used by user's to view, or modify their session
h.Path("/").Handler(httputil.HandlerFunc(p.UserDashboard)).Methods(http.MethodGet)
- h.Path("/impersonate").Handler(httputil.HandlerFunc(p.Impersonate)).Methods(http.MethodPost)
h.Path("/sign_out").HandlerFunc(p.SignOut).Methods(http.MethodGet, http.MethodPost)
+ // admin endpoints authorization is also delegated to authorizer service
+ admin := h.PathPrefix("/admin").Subrouter()
+ admin.Use(p.AuthorizeSession)
+ admin.Path("/impersonate").Handler(httputil.HandlerFunc(p.Impersonate)).Methods(http.MethodPost)
// Authenticate service callback handlers and middleware
// callback used to set route-scoped session and redirect back to destination
@@ -85,21 +88,20 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
// UserDashboard lets users investigate, and refresh their current session.
// It also contains certain administrative actions like user impersonation.
+//
// Nota bene: This endpoint does authentication, not authorization.
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) error {
- session, jwt, err := sessions.FromContext(r.Context())
+ jwt, err := sessions.FromContext(r.Context())
if err != nil {
return err
}
-
- isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), jwt)
- if err != nil {
- return err
+ var s sessions.State
+ if err := p.encoder.Unmarshal([]byte(jwt), &s); err != nil {
+ return httputil.NewError(http.StatusBadRequest, err)
}
p.templates.ExecuteTemplate(w, "dashboard.html", map[string]interface{}{
- "Session": session,
- "IsAdmin": isAdmin,
+ "Session": s,
"csrfField": csrf.TemplateField(r),
"ImpersonateAction": urlutil.QueryImpersonateAction,
"ImpersonateEmail": urlutil.QueryImpersonateEmail,
@@ -112,18 +114,6 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) error {
// to the user's current user sessions state if the user is currently an
// administrative user. Requests are redirected back to the user dashboard.
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) error {
- session, jwt, err := sessions.FromContext(r.Context())
- if err != nil {
- return err
- }
- isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), jwt)
- if err != nil {
- return err
- }
- if !isAdmin {
- return httputil.NewError(http.StatusForbidden, fmt.Errorf("%s is not an administrator", session.RequestEmail()))
- }
- // OK to impersonation
redirectURL := urlutil.GetAbsoluteURL(r)
redirectURL.Path = dashboardURL // redirect back to the dashboard
signinURL := *p.authenticateSigninURL
diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go
index 3d4048c21..29d1374db 100644
--- a/proxy/handlers_test.go
+++ b/proxy/handlers_test.go
@@ -17,6 +17,7 @@ import (
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/mock"
+ pb "github.com/pomerium/pomerium/internal/grpc/authorize"
"github.com/pomerium/pomerium/internal/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
@@ -79,10 +80,9 @@ func TestProxy_UserDashboard(t *testing.T) {
wantAdminForm bool
wantStatus int
}{
- {"good", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, false, http.StatusOK},
+ {"good", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, true, http.StatusOK},
{"session context error", errors.New("error"), opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, false, http.StatusInternalServerError},
- {"want admin form good admin authorization", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK},
- {"is admin but authorization fails", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError},
+ {"bad encoder unmarshal", nil, opts, http.MethodGet, &mock.Encoder{UnmarshalError: errors.New("err")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, false, http.StatusBadRequest},
}
for _, tt := range tests {
@@ -96,9 +96,9 @@ func TestProxy_UserDashboard(t *testing.T) {
p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest(tt.method, "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, _ := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
@@ -137,11 +137,7 @@ func TestProxy_Impersonate(t *testing.T) {
wantStatus int
}{
{"good", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
- {"good", false, opts, errors.New("error"), http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
- {"session load error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{LoadError: errors.New("err"), Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
- {"non admin users rejected", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden},
- {"non admin users rejected on error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusInternalServerError},
- {"groups", false, opts, nil, http.MethodPost, "user@blah.com", "group1,group2", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
+ {"bad session state", false, opts, errors.New("error"), http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -159,9 +155,9 @@ func TestProxy_Impersonate(t *testing.T) {
uri := &url.URL{Path: "/"}
r := httptest.NewRequest(tt.method, uri.String(), bytes.NewBufferString(postForm.Encode()))
- state, _, _ := tt.sessionStore.LoadSession(r)
+ state, _ := tt.sessionStore.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
@@ -246,12 +242,12 @@ func TestProxy_Callback(t *testing.T) {
wantStatus int
wantBody string
}{
- {"good", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""},
- {"good programmatic", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""},
- {"bad decrypt", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "KBEjQ9rnCxaAX-GOqexGw9ivEQURqts3zZ2mNGy0wnVa3SbtM399KlBq2nZ-9wM21FfsZX52er4jlmC7kPEKM3P7uZ41zR0zeys1-_74a5tQp-vsf1WXZfRsgVOuBcWPkMiWEoc379JFHxGDudp5VhU8B-dcQt4f3_PtLTHARkuH54io1Va2gNMq4Hiy8sQ1MPGCQeltH_JMzzdDpXdmdusWrXUvCGkba24muvAV06D8XRVJj6Iu9eK94qFnqcHc7wzziEbb8ADBues9dwbtb6jl8vMWz5rN6XvXqA5YpZv_MQZlsrO4oXFFQDevdgB84cX1tVbVu6qZvK_yQBZqzpOjWA9uIaoSENMytoXuWAlFO_sXjswfX8JTNdGwzB7qQRNPqxVG_sM_tzY3QhPm8zqwEzsXG5DokxZfVt2I5WJRUEovFDb4BnK9KFnnkEzLEdMudixVnXeGmTtycgJvoTeTCQRPfDYkcgJ7oKf4tGea-W7z5UAVa2RduJM9ZoM6YtJX7jgDm__PvvqcE0knJUF87XHBzdcOjoDF-CUze9xDJgNBlvPbJqVshKrwoqSYpePSDH9GUCNKxGequW3Ma8GvlFfhwd0rK6IZG-XWkyk0XSWQIGkDSjAvhB1wsOusCCguDjbpVZpaW5MMyTkmx68pl6qlIKT5UCcrVPl4ix5ZEj91mUDF0O1t04haD7VZuLVFXVGmqtFrBKI76sdYN-zkokaa1_chPRTyqMQFlqu_8LD6-RiK3UccGM-dEmnX72i91NP9F9OK0WJr9Cheup1C_P0mjqAO4Cb8oIHm0Oxz_mRqv5QbTGJtb3xwPLPuVjVCiE4gGBcuU2ixpSVf5HUF7y1KicVMCKiX9ATCBtg8sTdQZQnPEtHcHHAvdsnDVwev1LGfqA-Gdvg="}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"bad save session", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"bad base64", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"malformed redirect", opts, http.MethodGet, "http", "example.com", "/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
+ {"good", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusFound, ""},
+ {"good programmatic", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusFound, ""},
+ {"bad decrypt", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "KBEjQ9rnCxaAX-GOqexGw9ivEQURqts3zZ2mNGy0wnVa3SbtM399KlBq2nZ-9wM21FfsZX52er4jlmC7kPEKM3P7uZ41zR0zeys1-_74a5tQp-vsf1WXZfRsgVOuBcWPkMiWEoc379JFHxGDudp5VhU8B-dcQt4f3_PtLTHARkuH54io1Va2gNMq4Hiy8sQ1MPGCQeltH_JMzzdDpXdmdusWrXUvCGkba24muvAV06D8XRVJj6Iu9eK94qFnqcHc7wzziEbb8ADBues9dwbtb6jl8vMWz5rN6XvXqA5YpZv_MQZlsrO4oXFFQDevdgB84cX1tVbVu6qZvK_yQBZqzpOjWA9uIaoSENMytoXuWAlFO_sXjswfX8JTNdGwzB7qQRNPqxVG_sM_tzY3QhPm8zqwEzsXG5DokxZfVt2I5WJRUEovFDb4BnK9KFnnkEzLEdMudixVnXeGmTtycgJvoTeTCQRPfDYkcgJ7oKf4tGea-W7z5UAVa2RduJM9ZoM6YtJX7jgDm__PvvqcE0knJUF87XHBzdcOjoDF-CUze9xDJgNBlvPbJqVshKrwoqSYpePSDH9GUCNKxGequW3Ma8GvlFfhwd0rK6IZG-XWkyk0XSWQIGkDSjAvhB1wsOusCCguDjbpVZpaW5MMyTkmx68pl6qlIKT5UCcrVPl4ix5ZEj91mUDF0O1t04haD7VZuLVFXVGmqtFrBKI76sdYN-zkokaa1_chPRTyqMQFlqu_8LD6-RiK3UccGM-dEmnX72i91NP9F9OK0WJr9Cheup1C_P0mjqAO4Cb8oIHm0Oxz_mRqv5QbTGJtb3xwPLPuVjVCiE4gGBcuU2ixpSVf5HUF7y1KicVMCKiX9ATCBtg8sTdQZQnPEtHcHHAvdsnDVwev1LGfqA-Gdvg="}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"bad save session", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"bad base64", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"malformed redirect", opts, http.MethodGet, "http", "example.com", "/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -387,12 +383,12 @@ func TestProxy_ProgrammaticCallback(t *testing.T) {
wantStatus int
wantBody string
}{
- {"good", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""},
- {"good programmatic", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""},
- {"bad decrypt", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString + cryptutil.NewBase64Key()}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"bad save session", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"bad base64", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
- {"malformed redirect", opts, http.MethodGet, "http://pomerium.io/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""},
+ {"good", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusFound, ""},
+ {"good programmatic", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusFound, ""},
+ {"bad decrypt", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString + cryptutil.NewBase64Key()}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"bad save session", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"bad base64", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
+ {"malformed redirect", opts, http.MethodGet, "http://pomerium.io/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: &pb.IsAuthorizedReply{Allow: true}}, http.StatusBadRequest, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/proxy/middleware.go b/proxy/middleware.go
index 56c3a0f2a..7fe15a66d 100644
--- a/proxy/middleware.go
+++ b/proxy/middleware.go
@@ -7,8 +7,10 @@ import (
"io"
"io/ioutil"
"net/http"
+ "strings"
+
+ "github.com/rs/zerolog"
- "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
@@ -16,17 +18,6 @@ import (
"github.com/pomerium/pomerium/internal/urlutil"
)
-const (
- // HeaderJWT is the header key containing JWT signed user details.
- HeaderJWT = "x-pomerium-jwt-assertion"
- // HeaderUserID is the header key containing the user's id.
- HeaderUserID = "x-pomerium-authenticated-user-id"
- // HeaderEmail is the header key containing the user's email.
- HeaderEmail = "x-pomerium-authenticated-user-email"
- // HeaderGroups is the header key containing the user's groups.
- HeaderGroups = "x-pomerium-authenticated-user-groups"
-)
-
// AuthenticateSession is middleware to enforce a valid authentication
// session state is retrieved from the users's request context.
func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler {
@@ -34,111 +25,74 @@ func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler {
ctx, span := trace.StartSpan(r.Context(), "proxy.AuthenticateSession")
defer span.End()
- _, _, err := sessions.FromContext(ctx)
- if errors.Is(err, sessions.ErrExpired) {
- ctx, err = p.refresh(ctx, w, r)
- if err != nil {
- log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
- return p.redirectToSignin(w, r)
- }
- log.FromRequest(r).Info().Msg("proxy: refresh success")
- } else if err != nil {
+ if _, err := sessions.FromContext(ctx); err != nil {
log.FromRequest(r).Debug().Err(err).Msg("proxy: session state")
return p.redirectToSignin(w, r)
}
- p.addPomeriumHeaders(w, r)
next.ServeHTTP(w, r.WithContext(ctx))
return nil
})
}
-func (p *Proxy) refresh(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) {
+func (p *Proxy) refresh(ctx context.Context, oldSession string) (string, error) {
ctx, span := trace.StartSpan(ctx, "proxy.AuthenticateSession/refresh")
defer span.End()
- s, _, err := sessions.FromContext(ctx)
- if !errors.Is(err, sessions.ErrExpired) || s == nil {
- return nil, errors.New("proxy: unexpected session state for refresh")
+ s := &sessions.State{}
+ if err := p.encoder.Unmarshal([]byte(oldSession), s); err != nil {
+ return "", httputil.NewError(http.StatusBadRequest, err)
}
+
// 1 - build a signed url to call refresh on authenticate service
refreshURI := *p.authenticateRefreshURL
q := refreshURI.Query()
- q.Set(urlutil.QueryAccessTokenID, s.AccessTokenID) // hash value points to parent token
- q.Set(urlutil.QueryAudience, urlutil.StripPort(r.Host)) // request's audience, this route
+ q.Set(urlutil.QueryAccessTokenID, s.AccessTokenID) // hash value points to parent token
+ q.Set(urlutil.QueryAudience, strings.Join(s.Audience, ",")) // request's audience, this route
refreshURI.RawQuery = q.Encode()
signedRefreshURL := urlutil.NewSignedURL(p.SharedKey, &refreshURI).String()
// 2 - http call to authenticate service
req, err := http.NewRequestWithContext(ctx, http.MethodGet, signedRefreshURL, nil)
if err != nil {
- return nil, fmt.Errorf("proxy: refresh request: %v", err)
+ return "", fmt.Errorf("proxy: refresh request: %v", err)
}
req.Header.Set("X-Requested-With", "XmlHttpRequest")
req.Header.Set("Accept", "application/json")
res, err := httputil.DefaultClient.Do(req)
if err != nil {
- return nil, fmt.Errorf("proxy: client err %s: %w", signedRefreshURL, err)
+ return "", fmt.Errorf("proxy: client err %s: %w", signedRefreshURL, err)
}
defer res.Body.Close()
- jwtBytes, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
+ newJwt, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
if err != nil {
- return nil, err
+ return "", err
}
// auth couldn't refersh the session, delete the session and reload via 302
if res.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("proxy: backend refresh failed: %s", jwtBytes)
+ return "", fmt.Errorf("proxy: backend refresh failed: %s", newJwt)
}
- // 3 - save refreshed session to the client's session store
- if err = p.sessionStore.SaveSession(w, r, jwtBytes); err != nil {
- return nil, err
- }
- // 4 - add refreshed session to the current request context
- var state sessions.State
- if err := p.encoder.Unmarshal(jwtBytes, &state); err != nil {
- return nil, err
- }
- if err := state.Verify(urlutil.StripPort(r.Host)); err != nil {
- return nil, err
- }
- return sessions.NewContext(r.Context(), &state, string(jwtBytes), err), nil
+ return string(newJwt), nil
}
func (p *Proxy) redirectToSignin(w http.ResponseWriter, r *http.Request) error {
- s, _, err := sessions.FromContext(r.Context())
- p.sessionStore.ClearSession(w, r)
- if s != nil && err != nil && s.Programmatic {
- return httputil.NewError(http.StatusUnauthorized, err)
- }
signinURL := *p.authenticateSigninURL
q := signinURL.Query()
q.Set(urlutil.QueryRedirectURI, urlutil.GetAbsoluteURL(r).String())
signinURL.RawQuery = q.Encode()
log.FromRequest(r).Debug().Str("url", signinURL.String()).Msg("proxy: redirectToSignin")
httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &signinURL).String(), http.StatusFound)
+ p.sessionStore.ClearSession(w, r)
return nil
}
-func (p *Proxy) addPomeriumHeaders(w http.ResponseWriter, r *http.Request) {
- s, _, err := sessions.FromContext(r.Context())
- if err == nil && s != nil {
- r.Header.Set(HeaderUserID, s.Subject)
- r.Header.Set(HeaderEmail, s.RequestEmail())
- r.Header.Set(HeaderGroups, s.RequestGroups())
- w.Header().Set(HeaderUserID, s.Subject)
- w.Header().Set(HeaderEmail, s.RequestEmail())
- w.Header().Set(HeaderGroups, s.RequestGroups())
- }
-}
-
// AuthorizeSession is middleware to enforce a user is authorized for a request.
// Session state is retrieved from the users's request context.
func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx, span := trace.StartSpan(r.Context(), "proxy.AuthorizeSession")
defer span.End()
- if err := p.authorize(r.WithContext(ctx)); err != nil {
- log.FromRequest(r).Debug().Err(err).Msg("proxy: AuthorizeSession")
+ if err := p.authorize(w, r); err != nil {
return err
}
next.ServeHTTP(w, r.WithContext(ctx))
@@ -146,43 +100,45 @@ func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler {
})
}
-func (p *Proxy) authorize(r *http.Request) error {
- s, jwt, err := sessions.FromContext(r.Context())
+func (p *Proxy) authorize(w http.ResponseWriter, r *http.Request) error {
+ ctx, span := trace.StartSpan(r.Context(), "proxy.authorize")
+ defer span.End()
+ jwt, err := sessions.FromContext(ctx)
if err != nil {
return httputil.NewError(http.StatusInternalServerError, err)
}
- authorized, err := p.AuthorizeClient.Authorize(r.Context(), jwt, r)
+ authz, err := p.AuthorizeClient.Authorize(ctx, jwt, r)
if err != nil {
- return err
- } else if !authorized {
- return httputil.NewError(http.StatusForbidden, fmt.Errorf("%s is not authorized for %s", s.RequestEmail(), r.Host))
+ return httputil.NewError(http.StatusInternalServerError, err)
}
- return nil
-}
+ if authz.GetSessionExpired() {
+ newJwt, err := p.refresh(ctx, jwt)
+ if err != nil {
+ p.sessionStore.ClearSession(w, r)
+ log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
+ return p.redirectToSignin(w, r)
+ }
+ if err = p.sessionStore.SaveSession(w, r, newJwt); err != nil {
+ return httputil.NewError(http.StatusUnauthorized, err)
+ }
-// SignRequest is middleware that signs a JWT that contains a user's id,
-// email, and group. Session state is retrieved from the users's request context
-func (p *Proxy) SignRequest(signer encoding.Marshaler) func(next http.Handler) http.Handler {
- return func(next http.Handler) http.Handler {
- return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
- ctx, span := trace.StartSpan(r.Context(), "proxy.SignRequest")
- defer span.End()
- s, _, err := sessions.FromContext(r.Context())
- if err != nil {
- return httputil.NewError(http.StatusForbidden, err)
- }
- newSession := s.NewSession(r.Host, []string{r.Host})
- jwt, err := signer.Marshal(newSession.RouteSession())
- if err != nil {
- log.FromRequest(r).Error().Err(err).Msg("proxy: failed signing jwt")
- } else {
- r.Header.Set(HeaderJWT, string(jwt))
- w.Header().Set(HeaderJWT, string(jwt))
- }
- next.ServeHTTP(w, r.WithContext(ctx))
- return nil
- })
+ authz, err = p.AuthorizeClient.Authorize(ctx, newJwt, r)
+ if err != nil {
+ return httputil.NewError(http.StatusUnauthorized, err)
+ }
}
+ if !authz.GetAllow() {
+ log.FromRequest(r).Warn().
+ Strs("reason", authz.GetDenyReasons()).
+ Bool("allow", authz.GetAllow()).
+ Bool("expired", authz.GetSessionExpired()).
+ Msg("proxy/authorize: deny")
+ return httputil.NewError(http.StatusUnauthorized, errors.New("request denied"))
+ }
+
+ r.Header.Set(httputil.HeaderPomeriumJWTAssertion, authz.GetSignedJwt())
+ w.Header().Set(httputil.HeaderPomeriumJWTAssertion, authz.GetSignedJwt())
+ return nil
}
// SetResponseHeaders sets a map of response headers.
@@ -198,3 +154,26 @@ func SetResponseHeaders(headers map[string]string) func(next http.Handler) http.
})
}
}
+
+func (p *Proxy) userDetailsLoggerMiddleware(next http.Handler) http.Handler {
+ return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ if jwt, err := sessions.FromContext(r.Context()); err == nil {
+ var s sessions.State
+ if err := p.encoder.Unmarshal([]byte(jwt), &s); err == nil {
+ l := log.Ctx(r.Context())
+ l.UpdateContext(func(c zerolog.Context) zerolog.Context {
+ return c.Strs("groups", s.Groups)
+ })
+ l.UpdateContext(func(c zerolog.Context) zerolog.Context {
+ return c.Str("email", s.Email)
+ })
+ l.UpdateContext(func(c zerolog.Context) zerolog.Context {
+ return c.Str("user-id", s.User)
+ })
+ }
+ }
+ next.ServeHTTP(w, r)
+ return nil
+ })
+
+}
diff --git a/proxy/middleware_test.go b/proxy/middleware_test.go
index 229592f75..28ad5bbfa 100644
--- a/proxy/middleware_test.go
+++ b/proxy/middleware_test.go
@@ -10,14 +10,14 @@ import (
"time"
"github.com/google/go-cmp/cmp"
- "gopkg.in/square/go-jose.v2/jwt"
-
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/mock"
+ "github.com/pomerium/pomerium/internal/grpc/authorize"
"github.com/pomerium/pomerium/internal/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/sessions"
mstore "github.com/pomerium/pomerium/internal/sessions/mock"
+ "gopkg.in/square/go-jose.v2/jwt"
)
func TestProxy_AuthenticateSession(t *testing.T) {
@@ -42,14 +42,7 @@ func TestProxy_AuthenticateSession(t *testing.T) {
}{
{"good", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, nil, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
{"invalid session", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
- {"expired", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
- {"expired and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
- {"invalid session and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusUnauthorized},
- {"expired and refreshed ok", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
- {"expired and save failed", 200, false, &mstore.Store{SaveError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
- {"expired and unmarshal failed", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{UnmarshalError: errors.New("err")}, "", http.StatusFound},
- {"expired and malformed session", 200, false, &mstore.Store{Session: nil}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
- {"expired and refresh failed", 500, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
+ {"expired", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -73,13 +66,13 @@ func TestProxy_AuthenticateSession(t *testing.T) {
encoder: tt.encoder,
}
r := httptest.NewRequest(http.MethodGet, "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, _ := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder()
- got := a.AuthenticateSession(fn)
+ got := a.userDetailsLoggerMiddleware(a.AuthenticateSession(fn))
got.ServeHTTP(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("AuthenticateSession() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
@@ -96,35 +89,45 @@ func TestProxy_AuthorizeSession(t *testing.T) {
w.WriteHeader(http.StatusOK)
})
tests := []struct {
- name string
- session sessions.SessionStore
- authzClient client.Authorizer
+ name string
+ refreshRespStatus int
+ session sessions.SessionStore
+ authzClient client.Authorizer
ctxError error
provider identity.Authenticator
wantStatus int
}{
- {"user is authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, nil, identity.MockProvider{}, http.StatusOK},
- {"user is not authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: false}, nil, identity.MockProvider{}, http.StatusForbidden},
- {"ctx error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError},
- {"authz client error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError},
+ {"user is authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, nil, identity.MockProvider{}, http.StatusOK},
+ {"user is not authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: false}}, nil, identity.MockProvider{}, http.StatusUnauthorized},
+ {"ctx error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError},
+ {"authz client error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError},
+ {"expired, reauth failed", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{SessionExpired: true}}, nil, identity.MockProvider{}, http.StatusUnauthorized},
+ //todo(bdd): it's a bit tricky to test the refresh flow
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
-
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(tt.refreshRespStatus)
+ fmt.Fprintln(w, "REFRESH GOOD")
+ }))
+ defer ts.Close()
+ rURL := ts.URL
a := Proxy{
- SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
- cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
- authenticateURL: uriParseHelper("https://authenticate.corp.example"),
- authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
- sessionStore: tt.session,
- AuthorizeClient: tt.authzClient,
+ SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
+ cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
+ authenticateURL: uriParseHelper("https://authenticate.corp.example"),
+ authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
+ authenticateRefreshURL: uriParseHelper(rURL),
+ sessionStore: tt.session,
+ AuthorizeClient: tt.authzClient,
+ encoder: &mock.Encoder{},
}
r := httptest.NewRequest(http.MethodGet, "/", nil)
- state, _, _ := tt.session.LoadSession(r)
+ state, _ := tt.session.LoadSession(r)
ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
+ ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder()
@@ -137,69 +140,6 @@ func TestProxy_AuthorizeSession(t *testing.T) {
}
}
-type mockJWTSigner struct {
- SignError error
-}
-
-// Sign implements the JWTSigner interface from the cryptutil package, but just
-// base64's the inputs instead for stesting.
-func (s *mockJWTSigner) Marshal(v interface{}) ([]byte, error) {
-
- return []byte("ok"), s.SignError
-}
-
-func TestProxy_SignRequest(t *testing.T) {
- t.Parallel()
- fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain; charset=utf-8")
- fmt.Fprint(w, http.StatusText(http.StatusOK))
- w.WriteHeader(http.StatusOK)
- })
-
- tests := []struct {
- name string
- session sessions.SessionStore
-
- signerError error
- ctxError error
-
- wantStatus int
- wantHeaders string
- }{
- {"good", &mstore.Store{Session: &sessions.State{Email: "test"}}, nil, nil, http.StatusOK, "ok"},
- {"invalid session", &mstore.Store{Session: &sessions.State{Email: "test"}}, nil, errors.New("err"), http.StatusForbidden, ""},
- {"signature failure, warn but ok", &mstore.Store{Session: &sessions.State{Email: "test"}}, errors.New("err"), nil, http.StatusOK, ""},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
-
- a := Proxy{
- SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
- cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
- authenticateURL: uriParseHelper("https://authenticate.corp.example"),
- authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
- sessionStore: tt.session,
- }
- r := httptest.NewRequest(http.MethodGet, "/", nil)
- state, _, _ := tt.session.LoadSession(r)
- ctx := r.Context()
- ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
- r = r.WithContext(ctx)
- r.Header.Set("Accept", "application/json")
- w := httptest.NewRecorder()
- signer := &mockJWTSigner{SignError: tt.signerError}
- got := a.SignRequest(signer)(fn)
- got.ServeHTTP(w, r)
- if status := w.Code; status != tt.wantStatus {
- t.Errorf("SignRequest() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
- }
- if headers := r.Header.Get(HeaderJWT); tt.wantHeaders != headers {
- t.Errorf("SignRequest() headers = %v, want %v", headers, tt.wantHeaders)
- }
- })
- }
-}
-
func TestProxy_SetResponseHeaders(t *testing.T) {
t.Parallel()
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -230,7 +170,7 @@ func TestProxy_SetResponseHeaders(t *testing.T) {
got := SetResponseHeaders(tt.setHeaders)(fn)
got.ServeHTTP(w, r)
if diff := cmp.Diff(w.Body.String(), tt.wantHeaders); diff != "" {
- t.Errorf("SignRequest() :\n %s", diff)
+ t.Errorf("SetResponseHeaders() :\n %s", diff)
}
})
}
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 130ab359c..c637fe157 100755
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -63,12 +63,6 @@ func ValidateOptions(o config.Options) error {
if err := urlutil.ValidateURL(o.AuthorizeURL); err != nil {
return fmt.Errorf("proxy: invalid 'AUTHORIZE_SERVICE_URL': %w", err)
}
-
- if len(o.SigningKey) != 0 {
- if _, err := jws.NewES256Signer(o.SigningKey, ""); err != nil {
- return fmt.Errorf("proxy: invalid 'SIGNING_KEY': %w", err)
- }
- }
return nil
}
@@ -95,7 +89,6 @@ type Proxy struct {
Handler http.Handler
sessionStore sessions.SessionStore
sessionLoaders []sessions.SessionLoader
- signingKey string
templates *template.Template
}
@@ -142,8 +135,7 @@ func New(opts config.Options) (*Proxy, error) {
cookieStore,
header.NewStore(encoder, "Pomerium"),
queryparam.NewStore(encoder, "pomerium_session")},
- signingKey: opts.SigningKey,
- templates: template.Must(frontend.NewTemplates()),
+ templates: template.Must(frontend.NewTemplates()),
}
// errors checked in ValidateOptions
p.authorizeURL, _ = urlutil.DeepCopy(opts.AuthorizeURL)
@@ -188,7 +180,6 @@ func (p *Proxy) UpdateOptions(o config.Options) error {
// UpdatePolicies updates the H basedon the configured policies
func (p *Proxy) UpdatePolicies(opts *config.Options) error {
- var err error
if len(opts.Policies) == 0 {
log.Warn().Msg("proxy: configuration has no policies")
}
@@ -212,16 +203,14 @@ func (p *Proxy) UpdatePolicies(opts *config.Options) error {
if err := policy.Validate(); err != nil {
return fmt.Errorf("proxy: invalid policy %w", err)
}
- r, err = p.reverseProxyHandler(r, policy)
- if err != nil {
- return err
- }
+ r = p.reverseProxyHandler(r, policy)
+
}
p.Handler = r
return nil
}
-func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) (*mux.Router, error) {
+func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) *mux.Router {
// 1. Create the reverse proxy connection
proxy := stdhttputil.NewSingleHostReverseProxy(policy.Destination)
// 2. Create a sublogger to handle any error logs
@@ -269,28 +258,21 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) (*mux.R
// Optional: if a public route, skip access control middleware
if policy.AllowPublicUnauthenticatedAccess {
log.Warn().Str("route", policy.String()).Msg("proxy: all access control disabled")
- return r, nil
+ return r
}
// 4. Retrieve the user session and add it to the request context
rp.Use(sessions.RetrieveSession(p.sessionLoaders...))
- // 5. AuthN - Verify the user is authenticated. Set email, group, & id headers
+ // 5. AuthN - Verify user session has been added to the request context
rp.Use(p.AuthenticateSession)
// 6. AuthZ - Verify the user is authorized for route
rp.Use(p.AuthorizeSession)
// 7. Strip the user session cookie from the downstream request
rp.Use(middleware.StripCookie(p.cookieOptions.Name))
+ // 8 . Add user details to the request logger context
+ rp.Use(p.userDetailsLoggerMiddleware)
- // Optional: Add a signed JWT attesting to the user's id, email, and group
- if len(p.signingKey) != 0 {
- signer, err := jws.NewES256Signer(p.signingKey, policy.Destination.Host)
- if err != nil {
- return nil, err
- }
- rp.Use(p.SignRequest(signer))
- }
-
- return r, nil
+ return r
}
// roundTripperFromPolicy adjusts the std library's `DefaultTransport RoundTripper`
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
index 526116944..bf6b3ab7a 100644
--- a/proxy/proxy_test.go
+++ b/proxy/proxy_test.go
@@ -51,8 +51,6 @@ func TestOptions_Validate(t *testing.T) {
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
- invalidSignKey := testOptions(t)
- invalidSignKey.SigningKey = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
badSharedKey := testOptions(t)
badSharedKey.SharedKey = ""
sharedKeyBadBas64 := testOptions(t)
@@ -75,7 +73,6 @@ func TestOptions_Validate(t *testing.T) {
{"invalid cookie secret", invalidCookieSecret, true},
{"short cookie secret", shortCookieLength, true},
{"no shared secret", badSharedKey, true},
- {"invalid signing key", invalidSignKey, true},
{"shared secret bad base64", sharedKeyBadBas64, true},
}
for _, tt := range tests {
@@ -94,8 +91,6 @@ func TestNew(t *testing.T) {
good := testOptions(t)
shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
- badRoutedProxy := testOptions(t)
- badRoutedProxy.SigningKey = "YmFkIGtleQo="
badCookie := testOptions(t)
badCookie.CookieName = ""
badPolicyURL := config.Policy{To: "http://", From: "http://bar.example"}
@@ -113,7 +108,6 @@ func TestNew(t *testing.T) {
{"good", good, true, false},
{"empty options", config.Options{}, false, true},
{"short secret/validate sanity check", shortCookieLength, false, true},
- {"invalid ec key, valid base64 though", badRoutedProxy, false, true},
{"invalid cookie name, empty", badCookie, false, true},
{"bad policy, bad policy url", badNewPolicy, false, true},
}
@@ -186,30 +180,27 @@ func Test_UpdateOptions(t *testing.T) {
name string
originalOptions config.Options
updatedOptions config.Options
- signingKey string
host string
wantErr bool
wantRoute bool
}{
- {"good no change", good, good, "", "https://corp.example.example", false, true},
- {"changed", good, newPolicies, "", "https://bar.example", false, true},
- {"changed and missing", good, newPolicies, "", "https://corp.example.example", false, false},
- {"bad signing key", good, newPolicies, "^bad base 64", "https://corp.example.example", true, false},
- {"good signing key", good, newPolicies, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQ==", "https://corp.example.example", false, true},
- {"bad change bad policy url", good, badNewPolicy, "", "https://bar.example", true, false},
- {"disable tls verification", good, disableTLSPolicies, "", "https://bar.example", false, true},
- {"custom root ca", good, customCAPolicies, "", "https://bar.example", false, true},
- {"bad custom root ca base64", good, badCustomCAPolicies, "", "https://bar.example", true, false},
- {"good client certs", good, goodClientCertPolicies, "", "https://bar.example", false, true},
- {"custom server name", customServerName, customServerName, "", "https://bar.example", false, true},
- {"good no policies to start", emptyPolicies, good, "", "https://corp.example.example", false, true},
- {"allow websockets", good, allowWebSockets, "", "https://corp.example.example", false, true},
- {"no websockets, custom timeout", good, customTimeout, "", "https://corp.example.example", false, true},
- {"enable cors preflight", good, corsPreflight, "", "https://corp.example.example", false, true},
- {"disable auth", good, disableAuth, "", "https://corp.example.example", false, true},
- {"enable forward auth", good, fwdAuth, "", "https://corp.example.example", false, true},
- {"set request headers", good, reqHeaders, "", "https://corp.example.example", false, true},
- {"preserve host headers", preserveHostHeader, preserveHostHeader, "", "https://corp.example.example", false, true},
+ {"good no change", good, good, "https://corp.example.example", false, true},
+ {"changed", good, newPolicies, "https://bar.example", false, true},
+ {"changed and missing", good, newPolicies, "https://corp.example.example", false, false},
+ {"bad change bad policy url", good, badNewPolicy, "https://bar.example", true, false},
+ {"disable tls verification", good, disableTLSPolicies, "https://bar.example", false, true},
+ {"custom root ca", good, customCAPolicies, "https://bar.example", false, true},
+ {"bad custom root ca base64", good, badCustomCAPolicies, "https://bar.example", true, false},
+ {"good client certs", good, goodClientCertPolicies, "https://bar.example", false, true},
+ {"custom server name", customServerName, customServerName, "https://bar.example", false, true},
+ {"good no policies to start", emptyPolicies, good, "https://corp.example.example", false, true},
+ {"allow websockets", good, allowWebSockets, "https://corp.example.example", false, true},
+ {"no websockets, custom timeout", good, customTimeout, "https://corp.example.example", false, true},
+ {"enable cors preflight", good, corsPreflight, "https://corp.example.example", false, true},
+ {"disable auth", good, disableAuth, "https://corp.example.example", false, true},
+ {"enable forward auth", good, fwdAuth, "https://corp.example.example", false, true},
+ {"set request headers", good, reqHeaders, "https://corp.example.example", false, true},
+ {"preserve host headers", preserveHostHeader, preserveHostHeader, "https://corp.example.example", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -218,7 +209,6 @@ func Test_UpdateOptions(t *testing.T) {
t.Fatal(err)
}
- p.signingKey = tt.signingKey
err = p.UpdateOptions(tt.updatedOptions)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateOptions: err = %v, wantErr = %v", err, tt.wantErr)
@@ -269,10 +259,8 @@ func TestNewReverseProxy(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- proxyHandler, err := p.reverseProxyHandler(mux.NewRouter(), newPolicy)
- if err != nil {
- t.Fatal(err)
- }
+ proxyHandler := p.reverseProxyHandler(mux.NewRouter(), newPolicy)
+
ts.Config.Handler = proxyHandler
getReq, _ := http.NewRequest("GET", newPolicy.From, nil)