authorize: use jwt insead of state struct (#514)

authenticate: unmarshal and verify state from jwt, instead of middleware
authorize: embed opa policy using statik
authorize: have IsAuthorized handle authorization for all routes
authorize: if no signing key is provided, one is generated
authorize: remove IsAdmin grpc endpoint
authorize/client: return authorize decision struct
cmd/pomerium: main logger no longer contains email and group
cryptutil: add ECDSA signing methods
dashboard: have impersonate form show up for all users, but have api gated by authz
docs: fix typo in signed jwt header
encoding/jws: remove unused es256 signer
frontend: namespace static web assets
internal/sessions: remove leeway to match authz policy
proxy:  move signing functionality to authz
proxy: remove jwt attestation from proxy (authZ does now)
proxy: remove non-signed headers from headers
proxy: remove special handling of x-forwarded-host
sessions: do not verify state in middleware
sessions: remove leeway from state to match authz
sessions/{all}: store jwt directly instead of state

Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
Bobby DeSimone 2020-03-10 11:19:26 -07:00 committed by GitHub
parent a477af9378
commit 8d1732582e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1083 additions and 1264 deletions

View file

@ -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.

View file

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

View file

@ -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,16 +188,18 @@ 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,
encryptedEncoder: mock.Encoder{},
templates: template.Must(frontend.NewTemplates()),
}
u, _ := url.Parse("/sign_out")
@ -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,6 +346,10 @@ 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(),
@ -347,11 +357,15 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) {
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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
b, err := readPolicy("/authz.rego")
if err != nil {
return nil, err
}
if opts.PAMPolicy == "" {
opts.PAMPolicy = defaultPAM
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -116,8 +116,6 @@ module.exports = {
"reference/impersonation",
"reference/programmatic-access",
"reference/getting-users-identity",
"reference/signed-headers",
// "reference/examples",
"reference/production-deployment"
]
}

View file

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

View file

@ -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]

View file

@ -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.
![httpbin displaying jwt headers](./img/inspect-headers.png)
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.
![httpbin displaying decoded jwt](./img/verifying-headers-1.png)
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`.
![httpbin displaying verified jwt](./img/verifying-headers-2.png)
**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

View file

@ -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.
![httpbin displaying jwt headers](./img/inspect-headers.png)
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.
![httpbin displaying decoded jwt](./img/verifying-headers-1.png)
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`.
![httpbin displaying verified jwt](./img/verifying-headers-2.png)
**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

View file

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

5
go.mod
View file

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

55
go.sum
View file

@ -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=

View file

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

View file

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

View file

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

View file

@ -191,7 +191,6 @@
</form>
</div>
</div>
{{if .IsAdmin}}
<div id="info-box">
<div class="card">
@ -204,7 +203,7 @@
/>
</div>
<form method="POST" action="/.pomerium/impersonate">
<form method="POST" action="/.pomerium/admin/impersonate">
<section>
<p class="message">
Administrators can temporarily impersonate another user.
@ -244,7 +243,6 @@
</button>
</div>
</form>
{{ end }}
</div>
</div>
</div>

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -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",

View file

@ -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<string, Headers> 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; }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.

View file

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

View file

@ -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.

View file

@ -100,7 +100,6 @@ func TestStore_SaveSession(t *testing.T) {
}
tests := []struct {
name string
// State *State
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)
}
}

View file

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

View file

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

View file

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

View file

@ -4,14 +4,11 @@ import (
"context"
"errors"
"net/http"
"github.com/pomerium/pomerium/internal/urlutil"
)
// Context keys
var (
SessionCtxKey = &contextKey{"Session"}
SessionJWTCtxKey = &contextKey{"SessionJWT"}
ErrorCtxKey = &contextKey{"Error"}
)
@ -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
}

View file

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

View file

@ -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.

View file

@ -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 != "" {

View file

@ -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 {

View file

@ -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`.

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

@ -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.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())
authz, err = p.AuthorizeClient.Authorize(ctx, newJwt, r)
if err != nil {
return httputil.NewError(http.StatusForbidden, err)
return httputil.NewError(http.StatusUnauthorized, 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))
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
})
}

View file

@ -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())
@ -97,6 +90,7 @@ func TestProxy_AuthorizeSession(t *testing.T) {
})
tests := []struct {
name string
refreshRespStatus int
session sessions.SessionStore
authzClient client.Authorizer
@ -105,26 +99,35 @@ func TestProxy_AuthorizeSession(t *testing.T) {
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"),
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)
}
})
}

View file

@ -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,7 +135,6 @@ 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()),
}
// errors checked in ValidateOptions
@ -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`

View file

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