mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 15:47:36 +02:00
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:
parent
a477af9378
commit
8d1732582e
61 changed files with 1083 additions and 1264 deletions
2
Makefile
2
Makefile
|
@ -27,7 +27,7 @@ CTIMEVAR=-X $(PKG)/internal/version.GitCommit=$(GITCOMMIT) \
|
|||
-X $(PKG)/internal/version.ProjectURL=$(PKG)
|
||||
GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)"
|
||||
GOOSARCHES = linux/amd64 darwin/amd64 windows/amd64
|
||||
GOLANGCI_VERSION = v1.21.0 # .... for some reason v1.18.0 misses?
|
||||
GOLANGCI_VERSION = v1.21.0
|
||||
|
||||
.PHONY: all
|
||||
all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, lint, test, and vet.
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
64
authorize/evaluator/mock_evaluator/mock.go
Normal file
64
authorize/evaluator/mock_evaluator/mock.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
116
authorize/evaluator/opa/policy/authz.rego
Normal file
116
authorize/evaluator/opa/policy/authz.rego
Normal 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
|
||||
}
|
16
authorize/evaluator/opa/policy/statik.go
Normal file
16
authorize/evaluator/opa/policy/statik.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)}
|
||||
`
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -116,8 +116,6 @@ module.exports = {
|
|||
"reference/impersonation",
|
||||
"reference/programmatic-access",
|
||||
"reference/getting-users-identity",
|
||||
"reference/signed-headers",
|
||||
// "reference/examples",
|
||||
"reference/production-deployment"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -6,36 +6,86 @@ description: >-
|
|||
|
||||
# Getting the user's identity
|
||||
|
||||
This article describes how to retrieve a user's identity from a pomerium managed application.
|
||||
This article describes how to retrieve a user's identity from a pomerium managed application. Pomerium uses JSON Web Tokens (JWT) to attest that a given request was handled by Pomerium's authorizer service.
|
||||
|
||||
## Headers
|
||||
## Prerequisites
|
||||
|
||||
By default, pomerium passes the following [response headers] to it's downstream applications to identify the requesting users.
|
||||
To secure your app with signed headers, you'll need the following:
|
||||
|
||||
| Header | description |
|
||||
| :------------------------------------- | -------------------------------------------------------------- |
|
||||
| `x-pomerium-authenticated-user-id` | Subject is the user's id. |
|
||||
| `x-pomerium-authenticated-user-email` | Email is the user's email. |
|
||||
| `x-pomerium-authenticated-user-groups` | Groups is the user's groups. |
|
||||
| `x-pomerium-iap-jwt-assertion` | **Recommended** Contains the user's details as a signed [JWT]. |
|
||||
- An application you want users to connect to.
|
||||
- A [JWT] library that supports the `ES256` algorithm.
|
||||
|
||||
In an ideal environment, the cryptographic authenticity of the user's identifying headers should be enforced at the protocol level using mTLS.
|
||||
## Verification
|
||||
|
||||
### Recommended : Signed JWT header
|
||||
|
||||
For whatever reason, (e.g. an attacker bypasses pomerium's protocol encryption, or it is accidentally turned off), it is possible that the `x-pomerium-authenticated-user-{email,id,groups}` headers could be forged. Therefore, it is highly recommended to use and validate the [JWT] assertion header which adds an additional layer of authenticity.
|
||||
|
||||
Verify that the [JWT assertion header](./signed-headers.md) conforms to the following constraints:
|
||||
A JWT attesting to the authorization of a given request is added to the downstream HTTP request header `x-pomerium-jwt-assertion`. You should verify that the JWT contains at least the following claims:
|
||||
|
||||
| [JWT] | description |
|
||||
| :------: | ------------------------------------------------------------------------------------------------------ |
|
||||
| `exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew. |
|
||||
| `iat` | Issued-at time in seconds since the UNIX epoch. Allow 1 minute for skew. |
|
||||
| `aud` | The client's final domain e.g. `httpbin.corp.example.com`. |
|
||||
| `iss` | Issuer must be `pomerium-proxy`. |
|
||||
| `iss` | Issuer must be the URL of your authentication domain e.g. `authenticate.corp.example`. |
|
||||
| `sub` | Subject is the user's id. Can be used instead of the `x-pomerium-authenticated-user-id` header. |
|
||||
| `email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header. |
|
||||
| `groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header. |
|
||||
|
||||
[jwt]: https://jwt.io
|
||||
[response headers]: https://developer.mozilla.org/en-US/docs/Glossary/Response_header
|
||||
### Manual verification
|
||||
|
||||
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
|
||||
|
||||
1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
|
||||
|
||||
```bash
|
||||
# see ./scripts/generate_self_signed_signing_key.sh
|
||||
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
|
||||
openssl req -x509 -new -key ec_private.pem -days 1000000 -out ec_public.pem -subj "/CN=unused"
|
||||
# careful! this will output your private key in terminal
|
||||
cat ec_private.pem | base64
|
||||
```
|
||||
|
||||
Copy the base64 encoded value of your private key to `pomerium-proxy`'s environmental configuration variable `SIGNING_KEY`.
|
||||
|
||||
```bash
|
||||
SIGNING_KEY=ZxqyyIPPX0oWrrOwsxXgl0hHnTx3mBVhQ2kvW1YB4MM=
|
||||
```
|
||||
|
||||
2. Reload `pomerium-proxy`. Navigate to httpbin (by default, `https://httpbin.corp.${YOUR-DOMAIN}.com`), and login as usual. Click **request inspection**. Select `/headers'. Click **try it out** and then **execute**. You should see something like the following.
|
||||
|
||||

|
||||
|
||||
3. `X-Pomerium-Jwt-Assertion` is the signature value. It's less scary than it looks and basically just a compressed, json blob as described above. Navigate to [jwt.io] which provides a helpful GUI to manually verify JWT values.
|
||||
|
||||
4. Paste the value of `X-Pomerium-Jwt-Assertion` header token into the `Encoded` form. You should notice that the decoded values look much more familiar.
|
||||
|
||||

|
||||
|
||||
5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and past the output of `cat ec_public.pem`.
|
||||
|
||||

|
||||
|
||||
**Viola!** Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.
|
||||
|
||||
::: warning
|
||||
|
||||
In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing pomerium-proxy's public signing key to client apps (typically, a [key management service]).
|
||||
|
||||
:::
|
||||
|
||||
### Automatic verification
|
||||
|
||||
In the future, we will be adding example client implementations for:
|
||||
|
||||
- Python
|
||||
- Go
|
||||
- Java
|
||||
- C#
|
||||
- PHP
|
||||
|
||||
[developer tools]: https://developers.google.com/web/tools/chrome-devtools/open
|
||||
[docker-compose.yml]: https://github.com/pomerium/pomerium/blob/master/docker-compose.yml
|
||||
[httpbin]: https://httpbin.org/
|
||||
[jwt]: https://jwt.io/introduction/
|
||||
[jwt.io]: https://jwt.io/
|
||||
[key management service]: https://en.wikipedia.org/wiki/Key_management
|
||||
[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
|
||||
[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
---
|
||||
title: Signed Headers
|
||||
description: >-
|
||||
This article describes how to secure your app with signed headers. When
|
||||
configured, pomerium uses JSON Web Tokens (JWT) to make sure that a request to
|
||||
your app is authorized.
|
||||
---
|
||||
|
||||
# Securing your app with signed headers
|
||||
|
||||
This page describes how to add an additional layer of security to your apps with signed headers. When configured, pomerium uses JSON Web Tokens (JWT) to make sure that a given request was handled by pomerium and the request to your app is authorized.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To secure your app with signed headers, you'll need the following:
|
||||
|
||||
- An application you want users to connect to.
|
||||
- A [JWT] library that supports the `ES256` algorithm.
|
||||
|
||||
## Rationale
|
||||
|
||||
Signed headers provide **secondary** security in case someone bypasses mTLS and network segmentation. This protects your app from the following kind of risks:
|
||||
|
||||
- Pomerium is accidentally disabled
|
||||
- Misconfigured firewalls
|
||||
- Mutually-authenticated TLS
|
||||
- Access from within the project.
|
||||
|
||||
To properly secure your app, you must use signed headers for all app types.
|
||||
|
||||
## Verification
|
||||
|
||||
To secure your app with JWT, cryptographically verify the header, payload, and signature of the JWT. The JWT is in the HTTP request header `x-pomerium-iap-jwt-assertion`. If an attacker bypasses pomerium, they can forge the unsigned identity headers, `x-pomerium-authenticated-user-{email,id,groups}`. JWT provides a more secure alternative.
|
||||
|
||||
Note that pomerium it strips the `x-pomerium-*` headers provided by the client when the request goes through the serving infrastructure.
|
||||
|
||||
Verify that the JWT's header conforms to the following constraints:
|
||||
|
||||
[JWT] | description
|
||||
:------: | ------------------------------------------------------------------------------------------------------
|
||||
`exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew.
|
||||
`iat` | Issued-at time in seconds since the UNIX epoch. Allow 1 minute for skew.
|
||||
`aud` | The client's final domain e.g. `httpbin.corp.example.com`.
|
||||
`iss` | Issuer must be `pomerium-proxy`.
|
||||
`sub` | Subject is the user's id. Can be used instead of the `x-pomerium-authenticated-user-id` header.
|
||||
`email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header.
|
||||
`groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header.
|
||||
|
||||
### Manual verification
|
||||
|
||||
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
|
||||
|
||||
1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
|
||||
|
||||
```bash
|
||||
# see ./scripts/generate_self_signed_signing_key.sh
|
||||
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
|
||||
openssl req -x509 -new -key ec_private.pem -days 1000000 -out ec_public.pem -subj "/CN=unused"
|
||||
# careful! this will output your private key in terminal
|
||||
cat ec_private.pem | base64
|
||||
```
|
||||
|
||||
Copy the base64 encoded value of your private key to `pomerium-proxy`'s environmental configuration variable `SIGNING_KEY`.
|
||||
|
||||
```bash
|
||||
SIGNING_KEY=ZxqyyIPPX0oWrrOwsxXgl0hHnTx3mBVhQ2kvW1YB4MM=
|
||||
```
|
||||
|
||||
2. Reload `pomerium-proxy`. Navigate to httpbin (by default, `https://httpbin.corp.${YOUR-DOMAIN}.com`), and login as usual. Click **request inspection**. Select `/headers'. Click **try it out** and then **execute**. You should see something like the following.
|
||||
|
||||

|
||||
|
||||
3. `X-Pomerium-Jwt-Assertion` is the signature value. It's less scary than it looks and basically just a compressed, json blob as described above. Navigate to [jwt.io] which provides a helpful GUI to manually verify JWT values.
|
||||
|
||||
4. Paste the value of `X-Pomerium-Jwt-Assertion` header token into the `Encoded` form. You should notice that the decoded values look much more familiar.
|
||||
|
||||

|
||||
|
||||
5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and past the output of `cat ec_public.pem`.
|
||||
|
||||

|
||||
|
||||
**Viola!** Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.
|
||||
|
||||
::: warning
|
||||
|
||||
In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing pomerium-proxy's public signing key to client apps (typically, a [key management service]).
|
||||
|
||||
:::
|
||||
|
||||
### Automatic verification
|
||||
|
||||
In the future, we will be adding example client implementations for:
|
||||
|
||||
- Python
|
||||
- Go
|
||||
- Java
|
||||
- C#
|
||||
- PHP
|
||||
|
||||
[developer tools]: https://developers.google.com/web/tools/chrome-devtools/open
|
||||
[docker-compose.yml]: https://github.com/pomerium/pomerium/blob/master/docker-compose.yml
|
||||
[httpbin]: https://httpbin.org/
|
||||
[jwt]: https://jwt.io/introduction/
|
||||
[jwt.io]: https://jwt.io/
|
||||
[key management service]: https://en.wikipedia.org/wiki/Key_management
|
||||
[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
|
||||
[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations
|
|
@ -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
5
go.mod
|
@ -10,6 +10,7 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-redis/redis/v7 v7.2.0
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/golang/protobuf v1.3.4
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/gorilla/mux v1.7.4
|
||||
|
@ -40,8 +41,8 @@ require (
|
|||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
google.golang.org/api v0.20.0
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect
|
||||
google.golang.org/grpc v1.27.1
|
||||
google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178 // indirect
|
||||
google.golang.org/grpc v1.27.0
|
||||
gopkg.in/ini.v1 v1.51.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.4.1
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
|
|
55
go.sum
55
go.sum
|
@ -23,8 +23,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
|||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.3 h1:wS8NNaIgtzapuArKIAjsyXtEN/IUjQkbw90xszUdS40=
|
||||
github.com/OneOfOne/xxhash v1.2.3/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
|
||||
github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
@ -45,8 +43,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
|||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
|
||||
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
|
@ -63,7 +59,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
|
@ -107,6 +102,7 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
|
|||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
|
@ -130,8 +126,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
|||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
|
@ -165,16 +159,14 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
|
|||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/memberlist v0.1.5 h1:AYBsgJOW9gab/toO5tEB8lWetVgDKZycqkebJ8xxpqM=
|
||||
github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/memberlist v0.1.7 h1:5WLszGdi8FjebYTCKuX9zaGNl7pD31uv6Bj6657HxqQ=
|
||||
github.com/hashicorp/memberlist v0.1.7/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
|
@ -184,6 +176,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
|
|||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
|
@ -195,10 +188,10 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR
|
|||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39 h1:0E3wlIAcvD6zt/8UJgTd4JMT6UQhsnYyjCIqllyVLbs=
|
||||
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
|
@ -208,6 +201,7 @@ github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9
|
|||
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae h1:yIn3M+2nBaa+i9jUVoO+YmFjdczHt/BgReCj4EJOYOo=
|
||||
github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -215,6 +209,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
@ -223,10 +218,6 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
|
|||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
|
||||
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/open-policy-agent/opa v0.16.2 h1:Fdt1ysSA3p7z88HVHmUFiPM6hqqXbLDDZF9cQFYaIP0=
|
||||
github.com/open-policy-agent/opa v0.16.2/go.mod h1:P0xUE/GQAAgnvV537GzA0Ikw4+icPELRT327QJPkaKY=
|
||||
github.com/open-policy-agent/opa v0.17.1 h1:FchWeIevMohOKLM7oyFunUHEMp3gAOUCu+NNo/c6FjA=
|
||||
github.com/open-policy-agent/opa v0.17.1/go.mod h1:P0xUE/GQAAgnvV537GzA0Ikw4+icPELRT327QJPkaKY=
|
||||
github.com/open-policy-agent/opa v0.17.3 h1:Irk+/pTpN8bipJ7/XpEbFTg82v6Cmx9+8S/uS6V8MoM=
|
||||
github.com/open-policy-agent/opa v0.17.3/go.mod h1:6pC1cMYDI92i9EY/GoA2m+HcZlcCrh3jbfny5F7JVTA=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -236,6 +227,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
|
|||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d h1:zapSxdmZYY6vJWXFKLQ+MkI+agc+HQyfrCGowDSHiKs=
|
||||
github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
||||
github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
@ -245,8 +237,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e h1:kD8uNYLnc/MGAGfFzCEDQPVKulBEBY3FlVHvMJkkmQ4=
|
||||
github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e/go.mod h1:4GKkzpe75Db5IosO0CrSuFTDr3X0E0rkteGJiY4L9aE=
|
||||
github.com/pomerium/autocache v0.0.0-20200214161708-6c66ed582edc h1:8eatzx+SFs0jh6QQ4QSEcpW/TDMEbht0V2b+U6/MnAo=
|
||||
github.com/pomerium/autocache v0.0.0-20200214161708-6c66ed582edc/go.mod h1:8YuqYfLW/ZIavspMvQvH0UrPusRuvdm/r338GoSu2/k=
|
||||
github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 h1:FmzFXnCAepHZwl6QPhTFqBHcbcGevdiEQjutK+M5bj4=
|
||||
|
@ -262,8 +252,6 @@ github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4
|
|||
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
|
||||
github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
|
||||
github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8=
|
||||
github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
|
@ -279,8 +267,6 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7q
|
|||
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
|
@ -288,12 +274,9 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R
|
|||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
|
||||
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
|
||||
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
|
||||
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
|
||||
|
@ -305,8 +288,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
|
||||
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
|
@ -314,6 +295,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt
|
|||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
|
@ -329,6 +311,7 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
|||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0 h1:BgSbPgT2Zu8hDen1jJDGLWO8voaSRVrwsk18Q/uSh5M=
|
||||
github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
|
@ -368,15 +351,12 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -400,16 +380,17 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
|
|||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -447,7 +428,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
|
@ -461,11 +441,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs=
|
||||
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50=
|
||||
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
|
@ -498,6 +475,7 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -528,18 +506,15 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 h1:51D++eCgOHufw5VfDE9Uzqyyc+OyQIjb9hkYy9LN5Fk=
|
||||
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178 h1:4mrurAiSXsNNb6GoJatrIsnI+JqKHAVQQ1SbMS5OtDI=
|
||||
google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
49
internal/cryptutil/sign.go
Normal file
49
internal/cryptutil/sign.go
Normal 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)
|
||||
}
|
60
internal/cryptutil/sign_test.go
Normal file
60
internal/cryptutil/sign_test.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
12
internal/grpc/cache/cache.pb.go
vendored
12
internal/grpc/cache/cache.pb.go
vendored
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
16
internal/sessions/cache/cache_store.go
vendored
16
internal/sessions/cache/cache_store.go
vendored
|
@ -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.
|
||||
|
|
9
internal/sessions/cache/cache_store_test.go
vendored
9
internal/sessions/cache/cache_store_test.go
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue