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

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

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

View file

@ -27,7 +27,7 @@ CTIMEVAR=-X $(PKG)/internal/version.GitCommit=$(GITCOMMIT) \
-X $(PKG)/internal/version.ProjectURL=$(PKG) -X $(PKG)/internal/version.ProjectURL=$(PKG)
GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)" GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)"
GOOSARCHES = linux/amd64 darwin/amd64 windows/amd64 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 .PHONY: all
all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, lint, test, and vet. all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, lint, test, and vet.

View file

@ -74,20 +74,28 @@ func (a *Authenticate) Handler() http.Handler {
// session state is attached to the users's request context. // session state is attached to the users's request context.
func (a *Authenticate) VerifySession(next http.Handler) http.Handler { func (a *Authenticate) VerifySession(next http.Handler) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
state, _, err := sessions.FromContext(r.Context()) ctx := r.Context()
if errors.Is(err, sessions.ErrExpired) { jwt, err := sessions.FromContext(ctx)
ctx, err := a.refresh(w, r, state) 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 { if err != nil {
log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session, refresh") log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session, refresh")
return a.reauthenticateOrFail(w, r, err) return a.reauthenticateOrFail(w, r, err)
} }
next.ServeHTTP(w, r.WithContext(ctx))
return nil
} else if err != nil { } else if err != nil {
log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session") log.FromRequest(r).Info().Err(err).Msg("authenticate: verify session")
return a.reauthenticateOrFail(w, r, err) return a.reauthenticateOrFail(w, r, err)
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r.WithContext(ctx))
return nil 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 { if err := a.sessionStore.SaveSession(w, r, newSession); err != nil {
return nil, fmt.Errorf("authenticate: refresh save failed: %w", err) 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 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. // 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) jwtAudience = append(jwtAudience, fwdAuth)
} }
s, _, err := sessions.FromContext(r.Context()) jwt, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
return httputil.NewError(http.StatusBadRequest, err) 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 // user impersonation
if impersonate := r.FormValue(urlutil.QueryImpersonateAction); impersonate != "" { if impersonate := r.FormValue(urlutil.QueryImpersonateAction); impersonate != "" {
s.SetImpersonation(r.FormValue(urlutil.QueryImpersonateEmail), r.FormValue(urlutil.QueryImpersonateGroups)) s.SetImpersonation(r.FormValue(urlutil.QueryImpersonateEmail), r.FormValue(urlutil.QueryImpersonateGroups))
} }
// re-persist the session, useful when session was evicted from session // 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) 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 // SignOut signs the user out and attempts to revoke the user's identity session
// Handles both GET and POST. // Handles both GET and POST.
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
return httputil.NewError(http.StatusBadRequest, err) 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) a.sessionStore.ClearSession(w, r)
err = a.provider.Revoke(r.Context(), session.AccessToken) err = a.provider.Revoke(r.Context(), s.AccessToken)
if err != nil { if err != nil {
return httputil.NewError(http.StatusBadRequest, err) 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 // tokens and state with the identity provider. If successful, a new signed JWT
// and refresh token (`refresh_token`) are returned as JSON // and refresh token (`refresh_token`) are returned as JSON
func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error { 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) { if err != nil && !errors.Is(err, sessions.ErrExpired) {
return httputil.NewError(http.StatusBadRequest, err) 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 { if err != nil {
return err 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 // middleware. This handler is responsible for creating a new route scoped
// session and returning it. // session and returning it.
func (a *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
return httputil.NewError(http.StatusBadRequest, err) return httputil.NewError(http.StatusBadRequest, err)
} }
var s sessions.State
routeSession := s.NewSession(r.Host, []string{r.Host, r.FormValue(urlutil.QueryAudience)}) 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 routeSession.AccessTokenID = s.AccessTokenID
signedJWT, err := a.sharedEncoder.Marshal(routeSession.RouteSession()) signedJWT, err := a.sharedEncoder.Marshal(routeSession.RouteSession())

View file

@ -13,6 +13,7 @@ import (
"github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding" "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/encoding/mock"
"github.com/pomerium/pomerium/internal/frontend" "github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
@ -151,9 +152,9 @@ func TestAuthenticate_SignIn(t *testing.T) {
uri.RawQuery = queryString.Encode() uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(http.MethodGet, uri.String(), nil) r := httptest.NewRequest(http.MethodGet, uri.String(), nil)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
state, _, err := tt.session.LoadSession(r) state, err := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", err) ctx = sessions.NewContext(ctx, state, err)
r = r.WithContext(ctx) r = r.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -187,17 +188,19 @@ func TestAuthenticate_SignOut(t *testing.T) {
wantCode int wantCode int
wantBody string 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, ""}, {"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{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"}, {"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{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"}, {"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{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"}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
a := &Authenticate{ a := &Authenticate{
sessionStore: tt.sessionStore, sessionStore: tt.sessionStore,
provider: tt.provider, provider: tt.provider,
templates: template.Must(frontend.NewTemplates()), encryptedEncoder: mock.Encoder{},
templates: template.Must(frontend.NewTemplates()),
} }
u, _ := url.Parse("/sign_out") u, _ := url.Parse("/sign_out")
params, _ := url.ParseQuery(u.RawQuery) params, _ := url.ParseQuery(u.RawQuery)
@ -206,9 +209,12 @@ func TestAuthenticate_SignOut(t *testing.T) {
params.Add(urlutil.QueryRedirectURI, tt.redirectURL) params.Add(urlutil.QueryRedirectURI, tt.redirectURL)
u.RawQuery = params.Encode() u.RawQuery = params.Encode()
r := httptest.NewRequest(tt.method, u.String(), nil) 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 := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") 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}, {"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}, {"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,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,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}, {"expired XHR,refresh error", map[string]string{"X-Requested-With": "XmlHttpRequest"}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{RefreshError: errors.New("error")}, http.StatusUnauthorized},
@ -340,18 +346,26 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
signer, err := jws.NewHS256Signer(nil, "mock")
if err != nil {
t.Fatal(err)
}
a := Authenticate{ a := Authenticate{
sharedKey: cryptutil.NewBase64Key(), sharedKey: cryptutil.NewBase64Key(),
cookieSecret: cryptutil.NewKey(), cookieSecret: cryptutil.NewKey(),
RedirectURL: uriParseHelper("https://authenticate.corp.beyondperimeter.com"), RedirectURL: uriParseHelper("https://authenticate.corp.beyondperimeter.com"),
sessionStore: tt.session, sessionStore: tt.session,
provider: tt.provider, provider: tt.provider,
cookieCipher: aead, cookieCipher: aead,
encryptedEncoder: signer,
} }
r := httptest.NewRequest("GET", "/", nil) 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 := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
@ -408,9 +422,9 @@ func TestAuthenticate_RefreshAPI(t *testing.T) {
cookieCipher: aead, cookieCipher: aead,
} }
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
state, _, _ := tt.session.LoadSession(r) state, _ := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
@ -459,9 +473,9 @@ func TestAuthenticate_Refresh(t *testing.T) {
cookieCipher: aead, cookieCipher: aead,
} }
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
state, _, _ := tt.session.LoadSession(r) state, _ := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")

View file

@ -4,8 +4,11 @@ package authorize // import "github.com/pomerium/pomerium/authorize"
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"gopkg.in/square/go-jose.v2"
"github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/authorize/evaluator/opa" "github.com/pomerium/pomerium/authorize/evaluator/opa"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
@ -48,12 +51,37 @@ func newPolicyEvaluator(opts *config.Options) (evaluator.Evaluator, error) {
ctx := context.Background() ctx := context.Background()
ctx, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator") ctx, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator")
defer span.End() 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{}{ data := map[string]interface{}{
"shared_key": opts.SharedKey, "shared_key": opts.SharedKey,
"route_policies": opts.Policies, "route_policies": opts.Policies,
"admins": opts.Administrators, "admins": opts.Administrators,
"signing_key": jwk,
} }
return opa.New(ctx, &opa.Options{Data: data}) return opa.New(ctx, &opa.Options{Data: data})
} }

View file

@ -3,8 +3,6 @@ package authorize
import ( import (
"testing" "testing"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/authorize/evaluator/mock"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
) )
@ -50,25 +48,3 @@ func testPolicies(t *testing.T) []config.Policy {
} }
return policies return policies
} }
func TestAuthorize_UpdateOptions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pe evaluator.Evaluator
opts config.Options
wantErr bool
}{
{"good", &mock.PolicyEvaluator{}, config.Options{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authorize{
pe: tt.pe,
}
if err := a.UpdateOptions(tt.opts); (err != nil) != tt.wantErr {
t.Errorf("Authorize.UpdateOptions() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -1,15 +1,18 @@
//go:generate mockgen -destination mock_evaluator/mock.go github.com/pomerium/pomerium/authorize/evaluator Evaluator
// Package evaluator defines a Evaluator interfaces that can be implemented by // Package evaluator defines a Evaluator interfaces that can be implemented by
// a policy evaluator framework. // a policy evaluator framework.
package evaluator package evaluator
import ( import (
"context" "context"
pb "github.com/pomerium/pomerium/internal/grpc/authorize"
) )
// Evaluator specifies the interface for a policy engine. // Evaluator specifies the interface for a policy engine.
type Evaluator interface { type Evaluator interface {
IsAuthorized(ctx context.Context, input interface{}) (bool, error) IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error)
IsAdmin(ctx context.Context, input interface{}) (bool, error)
PutData(ctx context.Context, data map[string]interface{}) error PutData(ctx context.Context, data map[string]interface{}) error
} }
@ -45,7 +48,5 @@ type Request struct {
// Device context // Device context
// //
// todo(bdd): // todo(bdd): Use the peer TLS certificate to bind device state with a request
// Use the peer TLS certificate as the basis for binding device
// identity with a request context !
} }

View file

@ -1,35 +0,0 @@
// Package mock implements the policy evaluator interface to make authorization
// decisions.
package mock
import (
"context"
"github.com/pomerium/pomerium/authorize/evaluator"
)
var _ evaluator.Evaluator = &PolicyEvaluator{}
// PolicyEvaluator is the mock implementation of Evaluator
type PolicyEvaluator struct {
IsAuthorizedResponse bool
IsAuthorizedErr error
IsAdminResponse bool
IsAdminErr error
PutDataErr error
}
// IsAuthorized is the mock implementation of IsAuthorized
func (pe PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) {
return pe.IsAuthorizedResponse, pe.IsAuthorizedErr
}
// IsAdmin is the mock implementation of IsAdmin
func (pe PolicyEvaluator) IsAdmin(ctx context.Context, input interface{}) (bool, error) {
return pe.IsAdminResponse, pe.IsAdminErr
}
// PutData is the mock implementation of PutData
func (pe PolicyEvaluator) PutData(ctx context.Context, data map[string]interface{}) error {
return pe.PutDataErr
}

View file

@ -0,0 +1,64 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/pomerium/pomerium/authorize/evaluator (interfaces: Evaluator)
// Package mock_evaluator is a generated GoMock package.
package mock_evaluator
import (
context "context"
gomock "github.com/golang/mock/gomock"
authorize "github.com/pomerium/pomerium/internal/grpc/authorize"
reflect "reflect"
)
// MockEvaluator is a mock of Evaluator interface
type MockEvaluator struct {
ctrl *gomock.Controller
recorder *MockEvaluatorMockRecorder
}
// MockEvaluatorMockRecorder is the mock recorder for MockEvaluator
type MockEvaluatorMockRecorder struct {
mock *MockEvaluator
}
// NewMockEvaluator creates a new mock instance
func NewMockEvaluator(ctrl *gomock.Controller) *MockEvaluator {
mock := &MockEvaluator{ctrl: ctrl}
mock.recorder = &MockEvaluatorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockEvaluator) EXPECT() *MockEvaluatorMockRecorder {
return m.recorder
}
// IsAuthorized mocks base method
func (m *MockEvaluator) IsAuthorized(arg0 context.Context, arg1 interface{}) (*authorize.IsAuthorizedReply, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsAuthorized", arg0, arg1)
ret0, _ := ret[0].(*authorize.IsAuthorizedReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsAuthorized indicates an expected call of IsAuthorized
func (mr *MockEvaluatorMockRecorder) IsAuthorized(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthorized", reflect.TypeOf((*MockEvaluator)(nil).IsAuthorized), arg0, arg1)
}
// PutData mocks base method
func (m *MockEvaluator) PutData(arg0 context.Context, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutData", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// PutData indicates an expected call of PutData
func (mr *MockEvaluatorMockRecorder) PutData(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutData", reflect.TypeOf((*MockEvaluator)(nil).PutData), arg0, arg1)
}

View file

@ -1,3 +1,5 @@
//go:generate statik -src=./policy -include=*.rego -ns rego -p policy
// Package opa implements the policy evaluator interface to make authorization // Package opa implements the policy evaluator interface to make authorization
// decisions. // decisions.
package opa package opa
@ -6,15 +8,22 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"sync" "sync"
"github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/storage/inmem" "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"
_ "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" "github.com/pomerium/pomerium/internal/telemetry/trace"
) )
const statikNamespace = "rego"
var _ evaluator.Evaluator = &PolicyEvaluator{} var _ evaluator.Evaluator = &PolicyEvaluator{}
// PolicyEvaluator implements the evaluator interface using the open policy // PolicyEvaluator implements the evaluator interface using the open policy
@ -28,7 +37,6 @@ type PolicyEvaluator struct {
mu sync.RWMutex mu sync.RWMutex
store storage.Store store storage.Store
isAuthorized rego.PreparedEvalQuery isAuthorized rego.PreparedEvalQuery
isAdmin rego.PreparedEvalQuery
} }
// Options represent OPA's evaluator configurations. // Options represent OPA's evaluator configurations.
@ -37,10 +45,6 @@ type Options struct {
// apply custom authorization policy. // apply custom authorization policy.
// Defaults to authorization policy defined in config.yaml's policy. // Defaults to authorization policy defined in config.yaml's policy.
AuthorizationPolicy string 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 maps data that will be bound and
Data map[string]interface{} Data map[string]interface{}
} }
@ -49,19 +53,21 @@ type Options struct {
func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) { func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) {
var pe PolicyEvaluator var pe PolicyEvaluator
pe.store = inmem.New() pe.store = inmem.New()
if opts.Data == nil { if opts.Data == nil {
return nil, errors.New("opa: cannot create new evaluator without data") return nil, errors.New("opa: cannot create new evaluator without data")
} }
if opts.AuthorizationPolicy == "" { if opts.AuthorizationPolicy == "" {
opts.AuthorizationPolicy = defaultAuthorization b, err := readPolicy("/authz.rego")
} if err != nil {
if opts.PAMPolicy == "" { return nil, err
opts.PAMPolicy = defaultPAM }
opts.AuthorizationPolicy = string(b)
} }
if err := pe.PutData(ctx, opts.Data); err != nil { if err := pe.PutData(ctx, opts.Data); err != nil {
return nil, err 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 nil, err
} }
return &pe, nil 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 // UpdatePolicy takes authorization and privilege access management rego code
// as an input and updates the prepared policy evaluator. // 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") ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.UpdatePolicy")
defer span.End() defer span.End()
@ -80,38 +86,20 @@ func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz, pam string)
r := rego.New( r := rego.New(
rego.Store(pe.store), rego.Store(pe.store),
rego.Module("pomerium.authz", authz), rego.Module("pomerium.authz", authz),
// rego.Query("data.pomerium.authz"), rego.Query("result = data.pomerium.authz"),
rego.Query("result = data.pomerium.authz.allow"),
) )
pe.isAuthorized, err = r.PrepareForEval(ctx) pe.isAuthorized, err = r.PrepareForEval(ctx)
if err != nil { if err != nil {
return fmt.Errorf("opa: prepare policy: %w", err) 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 return nil
} }
// IsAuthorized determines if a given request input is authorized. // IsAuthorized determines if a given request input is authorized.
func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) { func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error) {
ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.PutData") ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAuthorized")
defer span.End() defer span.End()
return pe.runBoolQuery(ctx, input, pe.isAuthorized) 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 // 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 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() pe.mu.RLock()
defer pe.mu.RUnlock() defer pe.mu.RUnlock()
rs, err := q.Eval( rs, err := q.Eval(ctx, rego.EvalInput(input))
ctx,
rego.EvalInput(input),
)
if err != nil { if err != nil {
return false, fmt.Errorf("opa: eval query: %w", err) return nil, fmt.Errorf("eval query: %w", err)
} else if len(rs) != 1 { } else if len(rs) == 0 {
return false, fmt.Errorf("opa: eval result set: %v, expected len 1", rs) return nil, fmt.Errorf("empty eval result set %v", 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
} }
bindings := rs[0].Bindings.WithoutWildcards()["result"]
return decisionFromInterface(bindings)
}
func readPolicy(fn string) ([]byte, error) {
statikFS, err := fs.NewWithNamespace(statikNamespace)
if err != nil {
return nil, err
}
r, err := statikFS.Open(fn)
if err != nil {
return nil, err
}
defer r.Close()
return ioutil.ReadAll(r)
} }

View file

@ -100,8 +100,8 @@ func Test_Eval(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got != tt.want { if got.GetAllow() != tt.want {
t.Errorf("pe.Eval() = %v, want %v", got, tt.want) t.Errorf("pe.Eval() = %v, want %v", got.GetAllow(), tt.want)
} }
}) })
} }

View file

@ -0,0 +1,116 @@
package pomerium.authz
import data.route_policies
import data.shared_key
default allow = false
# allow by email
allow {
some route
input.host = route_policies[route].source
token.payload.email = route_policies[route].allowed_users[_]
token.valid
count(deny)==0
}
# allow group
allow {
some route
input.host = route_policies[route].source
token.payload.groups[_] = route_policies[route].allowed_groups[_]
token.valid
count(deny)==0
}
# allow by impersonate email
allow {
some route
input.host = route_policies[route].source
token.payload.impersonate_email = route_policies[route].allowed_users[_]
token.valid
count(deny)==0
}
# allow by impersonate group
allow {
some route
input.host = route_policies[route].source
token.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_]
token.valid
count(deny)==0
}
# allow by domain
allow {
some route
input.host = route_policies[route].source
allowed_user_domain(token.payload.email)
token.valid
count(deny)==0
}
# allow by impersonate domain
allow {
some route
input.host = route_policies[route].source
allowed_user_domain(token.payload.impersonate_email)
token.valid
count(deny)==0
}
allowed_user_domain(email){
x := split(email, "@")
count(x)=2
x[1] == route_policies[route].allowed_domains[_]
}
default expired = false
expired {
now_seconds:=time.now_ns()/1e9
[header, payload, _] := io.jwt.decode(input.user)
payload.exp < now_seconds
}
deny["token is expired (exp)"]{
now_seconds:=time.now_ns()/1e9
[header, payload, _] := io.jwt.decode(input.user)
payload.exp < now_seconds
}
# allow user is admin
allow {
element_in_list(data.admins, token.payload.email)
token.valid
count(deny)==0
contains(input.url,".pomerium/admin")
}
# deny non-admin users from accesing admin routes
deny["user is not admin"]{
not element_in_list(data.admins, token.payload.email)
contains(input.url,".pomerium/admin")
}
token = {"payload": payload, "valid": valid} {
[valid, header, payload] := io.jwt.decode_verify(
input.user, {
"secret": shared_key,
"aud": input.host,
}
)
}
user:=token.payload.user
email:=token.payload.email
groups:=token.payload.groups
signed_jwt:=io.jwt.encode_sign({"alg": "ES256"}, token.payload, data.signing_key)
element_in_list(list, elem) {
list[_] = elem
}

View file

@ -0,0 +1,16 @@
// Code generated by statik. DO NOT EDIT.
package policy
import (
"github.com/rakyll/statik/fs"
)
const Rego = "rego" // static asset namespace
func init() {
data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x84\xa6dP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00authz.regoUT\x05\x00\x01\xf9\x14`^\xc4V\xddn\xda0\x14\xbe\xf6y\x8a#\xef&\x91\xb2t\xad\xb4I\x8b\x16i7{\x82]\"\x14\xb9\xf1\x01\xdc&v\x14;\x05\x86x\xf7\xc9v \x94vj\xbb\x8d\xed\x06\xc89\xf6\xe7\xef\xc7\xb1\xe9D}/\x96\x84\x9di\xa9WC\x9b\x8b\xc1\xad~\x00\xa8\xb63\xbdC)\x9c\xc8{38\xaa:\xd3\xa8Z\x91}\xd4\xb2+\xd1\x93\xac\xeei\x0b i!\x86\xc6\xa1h\x1a\xb3\xc6\x12\x17\xa2\xb1\x04\xf0n,\xdcn\x91Z\xa1\x1a\x88\x8f;`\xd6\xb4\x84\x01\x1c\x98\xd2\xdd\xe0\xf2\x95\xb1\x0eK|\xbc\xe0,<\xcesk\x86\xbe&`\xce\xdc\x93\xce;\xb1m\x8c\x90y\xc0\xfc\xe5\x9c\xb0\x16\xc9j\xb0\xd4\xdbY5?\xcc~\x10\x8d\x92\xc0j3h\x97H\xd2\xdb\xb4,?\xc0~b\xbb\xec\xcd\xd0\xfde\xaa\x01\xd3\xb3x\x91\xeeq\xe4\xab\xf9\xdenQ\xb5\x1d\xf5\xd6h\xe1\xe8\"N\x9f\xe0W\x17r\xfdL\xc5%B8Uq\xe1@\xa4i\x85\xd2\xf8\xc7\x02N\xed\xac\"h\xf2\xccK\x90\xbe\x85\xdb\xa9\xcd\x11\xf2\x1f\xd0|\xb2\x83^\xa6\xfc\x1ch\x9c\xba\x03\xb6\xc1\xa2D\xdb5\xca\xc5Z\x86\xfc+O\x0f0\x9b\xb4\xbc\x01\xb6\x99]\xcf\xb1|)\xdf\x88\x1c\x02\xde\xc3t\x94\xd1\xa6S=\xc9\xe90;\x14v\xc0\xb4YW\x96j\xa3\xa5-J\xa7Z\xca}E\xdb$\xbd\xba\xa6\xcf\xc0f+\x12\x92\xfa\x0cG\xf5\x19Vs\xcfW\x99\xfcn\xedrI\xb5\x91\x94D\x7f\xbd\xba\x14\xd81\xceM\x87_\xf0d\x01O\xca\xbb2\xe3\xc1.T\xf6H-\xa1M\x97\xf2\xf9\x7f t\xdcN~\xb0\xa7$d{\xba\x8d\xa8\xa1\x96\xb4\xab\x94\xae\x1ae]\x12.\x8c0\xc6f\xf8\xf6\xfd\xcbj\xa3\x9d\x0f\xe9@\xb1o2\x9e\x1f\xee\xad\xab\x00\xcc\xd3\x91\x98\x9f\x85\xda\xe8\xf7\xa1\x1c\x18Z\\\xf4\xa6EQ\xd7d\x95^F\xb6q[\xd8\xd1\xdc\x83\x10m\\l\x8f\xbe:\xfc\x0d-\xaf\xa6\x1b#-q\xc7G\x08^L\x11\xf1`\x06/0|\xef\xbd\xaf\xb3\xf03\xc3\xb38\x9ffY=P\xaf\x16\xdb\x04\x18\x9bR\xcd<\x04c\xdcR\xdd\x93\xe3\x05NWx\x16\x1ab\xf0\xcbM\xef\xbd\xaf\xee\x81\x05\xaa\x1e\xa0(\x1f\xeb\xf55\x08\xa2\xcf;\xf1\x12\x8aG\xe7y/V\xc1\xaa\xa5&Y\xdd\xad]Q\x8e\xdcI\x07\xee\xbe\x93\xec\xb8h\x96\xbc@\xfe\xed\xfb\xcd\xc7O|\x7f\xe6u6\xfe QK\xad\xf4\xd2KH\x01\xe0<+\xff\x91\x85\x04S\xdc\x01\xa2\x7f\x8eg\xbe\xaf\xc1\xfeg\x00\x00\x00\xff\xffPK\x07\x08\xd4XXE|\x02\x00\x00\x03 \x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x84\xa6dP\xd4XXE|\x02\x00\x00\x03 \x00\x00\n\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00authz.regoUT\x05\x00\x01\xf9\x14`^PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00A\x00\x00\x00\xbd\x02\x00\x00\x00\x00"
fs.RegisterWithNamespace("rego", data)
}

View file

@ -1,87 +0,0 @@
package opa
//todo(bdd): embed source files directly, and setup tests.
const defaultAuthorization = `
package pomerium.authz
import data.route_policies
import data.shared_key
default allow = false
# allow by email
allow {
some route
input.host = route_policies[route].source
jwt.payload.email = route_policies[route].allowed_users[_]
jwt.valid
}
# allow group
allow {
some route
input.host = route_policies[route].source
jwt.payload.groups[_] = route_policies[route].allowed_groups[_]
jwt.valid
}
# allow by impersonate email
allow {
some route
input.host = route_policies[route].source
jwt.payload.impersonate_email = route_policies[route].allowed_users[_]
jwt.valid
}
# allow by impersonate group
allow {
some route
input.host = route_policies[route].source
jwt.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_]
jwt.valid
}
# allow by domain
allow {
some route
input.host = route_policies[route].source
x := split(jwt.payload.email, "@")
count(x)=2
x[1] = route_policies[route].allowed_domains[_]
jwt.valid
}
# allow by impersonate domain
allow {
some route
input.host = route_policies[route].source
x := split(jwt.payload.impersonate_email, "@")
count(x)=2
x[1] == route_policies[route].allowed_domains[_]
jwt.valid
}
jwt = {"payload": payload, "valid": valid} {
[valid, header, payload] := io.jwt.decode_verify(
input.user, {
"secret": shared_key,
"aud": input.host,
}
)
}
`
const defaultPAM = `
package pomerium.pam
import data.admins
import data.shared_key
default is_admin = false
is_admin{
io.jwt.verify_hs256(input.user,shared_key)
jwt.payload.email = admins[_]
}
jwt = {"payload": payload} {[header, payload, signature] := io.jwt.decode(input.user)}
`

View file

@ -11,7 +11,7 @@ import (
// IsAuthorized checks to see if a given user is authorized to make a request. // 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) { 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() defer span.End()
req := &evaluator.Request{ req := &evaluator.Request{
@ -23,25 +23,7 @@ func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorized
RemoteAddr: in.GetRequestRemoteAddr(), RemoteAddr: in.GetRequestRemoteAddr(),
URL: in.GetRequestUrl(), URL: in.GetRequestUrl(),
} }
ok, err := a.pe.IsAuthorized(ctx, req) return 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
} }
type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers

View file

@ -1,74 +1,49 @@
//go:generate protoc -I ../internal/grpc/authorize/ --go_out=plugins=grpc:../internal/grpc/authorize/ ../internal/grpc/authorize/authorize.proto
package authorize package authorize
import ( import (
"context" "context"
"errors" "errors"
"reflect"
"testing" "testing"
"github.com/pomerium/pomerium/authorize/evaluator" "github.com/golang/mock/gomock"
"github.com/pomerium/pomerium/authorize/evaluator/mock" "github.com/google/go-cmp/cmp"
"github.com/pomerium/pomerium/authorize/evaluator/mock_evaluator"
"github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/grpc/authorize"
) )
func TestAuthorize_IsAuthorized(t *testing.T) { func TestAuthorize_IsAuthorized(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
name string name string
pe evaluator.Evaluator retDec *authorize.IsAuthorizedReply
retErr error
ctx context.Context
in *authorize.IsAuthorizedRequest in *authorize.IsAuthorizedRequest
want *authorize.IsAuthorizedReply want *authorize.IsAuthorizedReply
wantErr bool wantErr bool
}{ }{
{"want false", &mock.PolicyEvaluator{}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: false}, false}, {"good", &authorize.IsAuthorizedReply{}, nil, context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good"}, &authorize.IsAuthorizedReply{}, false},
{"want true", &mock.PolicyEvaluator{IsAuthorizedResponse: true}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: true}, false}, {"error", &authorize.IsAuthorizedReply{}, errors.New("error"), context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good"}, &authorize.IsAuthorizedReply{}, true},
{"want err", &mock.PolicyEvaluator{IsAuthorizedErr: errors.New("err")}, &authorize.IsAuthorizedRequest{}, nil, true}, {"headers", &authorize.IsAuthorizedReply{}, nil, context.TODO(), &authorize.IsAuthorizedRequest{UserToken: "good", RequestHeaders: nil}, &authorize.IsAuthorizedReply{}, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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{ 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 { if (err != nil) != tt.wantErr {
t.Errorf("Authorize.IsAuthorized() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("Authorize.IsAuthorized() error = %v, wantErr %v", err, tt.wantErr)
return 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) t.Errorf("Authorize.IsAuthorized() = %v, want %v", got, tt.want)
} }
}) })
} }
} }
func TestAuthorize_IsAdmin(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pe evaluator.Evaluator
in *authorize.IsAdminRequest
want *authorize.IsAdminReply
wantErr bool
}{
{"want false", &mock.PolicyEvaluator{}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: false}, false},
{"want true", &mock.PolicyEvaluator{IsAdminResponse: true}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: true}, false},
{"want err", &mock.PolicyEvaluator{IsAdminErr: errors.New("err")}, &authorize.IsAdminRequest{}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authorize{
pe: tt.pe,
}
got, err := a.IsAdmin(context.TODO(), tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authorize.IsAdmin() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -189,8 +189,8 @@ func newGlobalRouter(o *config.Options) *mux.Router {
Dur("duration", duration). Dur("duration", duration).
Int("size", size). Int("size", size).
Int("status", status). Int("status", status).
Str("email", r.Header.Get(proxy.HeaderEmail)). // Str("email", r.Header.Get(httputil.HeaderPomeriumEmail)).
Str("group", r.Header.Get(proxy.HeaderGroups)). // Str("group", r.Header.Get(httputil.HeaderPomeriumGroups)).
Str("method", r.Method). Str("method", r.Method).
Str("service", o.Services). Str("service", o.Services).
Str("host", r.Host). Str("host", r.Host).

View file

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

View file

@ -200,7 +200,7 @@ Enable grpc DNS based round robin load balancing. This method uses DNS to resolv
#### GRPC Server Max Connection Age #### GRPC Server Max Connection Age
Set max connection age for GRPC servers. After this interval, servers ask clients to reconnect and perform any rediscovery for new/updated endpoints from DNS. Set max connection age for GRPC servers. After this interval, servers ask clients to reconnect and perform any rediscovery for new/updated endpoints from DNS.
See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for details See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for details
@ -209,7 +209,6 @@ See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for deta
- Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string` - Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string`
- Default: `5m` - Default: `5m`
#### GRPC Server Max Connection Age Grace #### GRPC Server Max Connection Age Grace
Additive period with `grpc_server_max_connection_age`, after which servers will force connections to close. 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` - Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string`
- Default: `5m` - Default: `5m`
### Cookie options ### Cookie options
These settings control the Pomerium session cookies sent to users's These settings control the Pomerium session cookies sent to users's
@ -359,8 +357,8 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
| Config Key | Description | Required | | Config Key | Description | Required |
| :--------------- | :---------------------------------------------------------------- | -------- | | :--------------- | :---------------------------------------------------------------- | -------- |
| tracing_provider | The name of the tracing provider. (e.g. jaeger) | ✅ | | tracing_provider | The name of the tracing provider. (e.g. jaeger) | ✅ |
| tracing_debug | Will disable [sampling](https://opencensus.io/tracing/sampling/). | ❌ | | tracing_debug | Will disable [sampling](https://opencensus.io/tracing/sampling/). | ❌ |
#### Jaeger #### Jaeger
@ -374,8 +372,8 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
| Config Key | Description | Required | | Config Key | Description | Required |
| :-------------------------------- | :------------------------------------------ | -------- | | :-------------------------------- | :------------------------------------------ | -------- |
| tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅ | | tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅ |
| tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅ | | tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅ |
#### Example #### Example
@ -547,15 +545,6 @@ See also:
## Proxy Service ## 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 ### Authenticate Service URL
- Environmental Variable: `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). 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 [base64 encoded]: https://en.wikipedia.org/wiki/Base64
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable [environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
[identity provider]: ./identity-providers.md [identity provider]: ./identity-providers.md

View file

@ -2,12 +2,12 @@
## v0.6.0 ## v0.6.0
## New ### New
- authenticate: support backend refresh @desimone [GH-438] - authenticate: support backend refresh @desimone [GH-438]
- cache: add cache service @desimone [GH-457] - cache: add cache service @desimone [GH-457]
## Changed ### Changed
- authorize: consolidate gRPC packages @desimone [GH-443] - authorize: consolidate gRPC packages @desimone [GH-443]
- config: added yaml tags to all options struct fields @travisgroth [GH-394],[gh-397] - 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] - config: validate that `shared_key` does not contain whitespace @travisgroth [GH-427]
- httputil : wrap handlers for additional context @desimone [GH-413] - httputil : wrap handlers for additional context @desimone [GH-413]
## Fixed ### Fixed
- proxy: fix unauthorized redirect loop for forward auth @desimone [GH-448] - 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) - proxy: fixed regression preventing policy reload [GH-396](https://github.com/pomerium/pomerium/pull/396)
## Documentation ### Documentation
- add cookie settings @danderson [GH-429] - add cookie settings @danderson [GH-429]
- fix typo in forward auth nginx example @travisgroth [GH-445] - fix typo in forward auth nginx example @travisgroth [GH-445]
- improved sentence flow and other stuff @Rio [GH-422] - improved sentence flow and other stuff @Rio [GH-422]
- rename fwdauth to be forwardauth @desimone [GH-447] - 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 61a8779 @renovate [GH-452]
- chore(deps): update golang.org/x/crypto commit hash to 530e935 @renovate [GH-458] - chore(deps): update golang.org/x/crypto commit hash to 530e935 @renovate [GH-458]

View file

@ -6,36 +6,86 @@ description: >-
# Getting the user's identity # 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 | - An application you want users to connect to.
| :------------------------------------- | -------------------------------------------------------------- | - A [JWT] library that supports the `ES256` algorithm.
| `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]. |
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 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:
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:
| [JWT] | description | | [JWT] | description |
| :------: | ------------------------------------------------------------------------------------------------------ | | :------: | ------------------------------------------------------------------------------------------------------ |
| `exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew. | | `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. | | `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`. | | `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. | | `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. | | `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. | | `groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header. |
[jwt]: https://jwt.io ### Manual verification
[response headers]: https://developer.mozilla.org/en-US/docs/Glossary/Response_header
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
```bash
# see ./scripts/generate_self_signed_signing_key.sh
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
openssl req -x509 -new -key ec_private.pem -days 1000000 -out ec_public.pem -subj "/CN=unused"
# careful! this will output your private key in terminal
cat ec_private.pem | base64
```
Copy the base64 encoded value of your private key to `pomerium-proxy`'s environmental configuration variable `SIGNING_KEY`.
```bash
SIGNING_KEY=ZxqyyIPPX0oWrrOwsxXgl0hHnTx3mBVhQ2kvW1YB4MM=
```
2. Reload `pomerium-proxy`. Navigate to httpbin (by default, `https://httpbin.corp.${YOUR-DOMAIN}.com`), and login as usual. Click **request inspection**. Select `/headers'. Click **try it out** and then **execute**. You should see something like the following.
![httpbin displaying jwt headers](./img/inspect-headers.png)
3. `X-Pomerium-Jwt-Assertion` is the signature value. It's less scary than it looks and basically just a compressed, json blob as described above. Navigate to [jwt.io] which provides a helpful GUI to manually verify JWT values.
4. Paste the value of `X-Pomerium-Jwt-Assertion` header token into the `Encoded` form. You should notice that the decoded values look much more familiar.
![httpbin displaying decoded jwt](./img/verifying-headers-1.png)
5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and past the output of `cat ec_public.pem`.
![httpbin displaying verified jwt](./img/verifying-headers-2.png)
**Viola!** Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.
::: warning
In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing pomerium-proxy's public signing key to client apps (typically, a [key management service]).
:::
### Automatic verification
In the future, we will be adding example client implementations for:
- Python
- Go
- Java
- C#
- PHP
[developer tools]: https://developers.google.com/web/tools/chrome-devtools/open
[docker-compose.yml]: https://github.com/pomerium/pomerium/blob/master/docker-compose.yml
[httpbin]: https://httpbin.org/
[jwt]: https://jwt.io/introduction/
[jwt.io]: https://jwt.io/
[key management service]: https://en.wikipedia.org/wiki/Key_management
[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations

View file

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

View file

@ -7,6 +7,15 @@ description: >-
# Upgrade Guide # 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 ## Since 0.5.0
### Breaking ### Breaking

5
go.mod
View file

@ -10,6 +10,7 @@ require (
github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify v1.4.7
github.com/go-redis/redis/v7 v7.2.0 github.com/go-redis/redis/v7 v7.2.0
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e 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/golang/protobuf v1.3.4
github.com/google/go-cmp v0.4.0 github.com/google/go-cmp v0.4.0
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
@ -40,8 +41,8 @@ require (
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.20.0 google.golang.org/api v0.20.0
google.golang.org/appengine v1.6.5 // indirect google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178 // indirect
google.golang.org/grpc v1.27.1 google.golang.org/grpc v1.27.0
gopkg.in/ini.v1 v1.51.1 // indirect gopkg.in/ini.v1 v1.51.1 // indirect
gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/square/go-jose.v2 v2.4.1
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8

55
go.sum
View file

@ -23,8 +23,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/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 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 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 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= 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= 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/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 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 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 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= 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/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/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/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/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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 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.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 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 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= 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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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 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 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 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/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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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 h1:5WLszGdi8FjebYTCKuX9zaGNl7pD31uv6Bj6657HxqQ=
github.com/hashicorp/memberlist v0.1.7/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= 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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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.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/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/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= 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/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/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.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/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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 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/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-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-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/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 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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.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 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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/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 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/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/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/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/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.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/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.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 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 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 h1:Irk+/pTpN8bipJ7/XpEbFTg82v6Cmx9+8S/uS6V8MoM=
github.com/open-policy-agent/opa v0.17.3/go.mod h1:6pC1cMYDI92i9EY/GoA2m+HcZlcCrh3jbfny5F7JVTA= 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= 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.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 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 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/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.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.0/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 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/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= 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 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 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.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 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8=
github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 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= 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 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 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.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 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 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= 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 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 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.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 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 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/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 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 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= 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 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 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/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 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= 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= 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/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.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.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/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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 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.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 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/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/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 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= 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.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 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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/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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 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-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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-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-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-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/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-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/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.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.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/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-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-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-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-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-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-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-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-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-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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 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-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-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-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 h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs=
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 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-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 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-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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-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-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-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-20200204235621-fb4a7afc5178 h1:4mrurAiSXsNNb6GoJatrIsnI+JqKHAVQQ1SbMS5OtDI=
google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 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.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 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/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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -0,0 +1,49 @@
package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"math/big"
)
// NewSigningKey generates a random P-256 ECDSA private key.
// Go's P-256 is constant-time (which prevents certain types of attacks)
// while its P-384 and P-521 are not.
func NewSigningKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
// Sign signs arbitrary data using ECDSA.
func Sign(data []byte, privkey *ecdsa.PrivateKey) ([]byte, error) {
// hash message
digest := sha256.Sum256(data)
// sign the hash
r, s, err := ecdsa.Sign(rand.Reader, privkey, digest[:])
if err != nil {
return nil, err
}
// encode the signature {R, S}
// big.Int.Bytes() will need padding in the case of leading zero bytes
params := privkey.Curve.Params()
curveOrderByteSize := params.P.BitLen() / 8
rBytes, sBytes := r.Bytes(), s.Bytes()
signature := make([]byte, curveOrderByteSize*2)
copy(signature[curveOrderByteSize-len(rBytes):], rBytes)
copy(signature[curveOrderByteSize*2-len(sBytes):], sBytes)
return signature, nil
}
// Verify checks a raw ECDSA signature.
// Returns true if it's valid and false if not.
func Verify(data, signature []byte, pubkey *ecdsa.PublicKey) bool {
// hash message
digest := sha256.Sum256(data)
curveOrderByteSize := pubkey.Curve.Params().P.BitLen() / 8
r, s := new(big.Int), new(big.Int)
r.SetBytes(signature[:curveOrderByteSize])
s.SetBytes(signature[curveOrderByteSize:])
return ecdsa.Verify(pubkey, digest[:], r, s)
}

View file

@ -0,0 +1,60 @@
package cryptutil
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
)
func TestSign(t *testing.T) {
message := []byte("Hello, world!")
key, err := NewSigningKey()
if err != nil {
t.Error(err)
return
}
signature, err := Sign(message, key)
if err != nil {
t.Error(err)
return
}
if !Verify(message, signature, &key.PublicKey) {
t.Error("signature was not correct")
return
}
message[0] ^= 0xff
if Verify(message, signature, &key.PublicKey) {
t.Error("signature was good for altered message")
}
}
func TestSignWithP384(t *testing.T) {
message := []byte("Hello, world!")
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Error(err)
return
}
signature, err := Sign(message, key)
if err != nil {
t.Error(err)
return
}
if !Verify(message, signature, &key.PublicKey) {
t.Error("signature was not correct")
return
}
message[0] ^= 0xff
if Verify(message, signature, &key.PublicKey) {
t.Error("signature was good for altered message")
}
}

View file

@ -3,9 +3,6 @@
package jws // import "github.com/pomerium/pomerium/internal/encoding/jws" package jws // import "github.com/pomerium/pomerium/internal/encoding/jws"
import ( import (
"encoding/base64"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
jose "gopkg.in/square/go-jose.v2" 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 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. // Marshal signs, and serializes a JWT.
func (c *JSONWebSigner) Marshal(x interface{}) ([]byte, error) { func (c *JSONWebSigner) Marshal(x interface{}) ([]byte, error) {
s, err := jwt.Signed(c.Signer).Claims(x).CompactSerialize() s, err := jwt.Signed(c.Signer).Claims(x).CompactSerialize()

View file

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

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
//go:generate statik -src=./assets -include=*.svg,*.html,*.css,*.js //go:generate statik -src=./assets -include=*.svg,*.html,*.css,*.js -ns web
// Package frontend handles the generation, and instantiation of Pomerium's // Package frontend handles the generation, and instantiation of Pomerium's
// html templates. // html templates.
@ -16,10 +16,12 @@ import (
_ "github.com/pomerium/pomerium/internal/frontend/statik" // load static assets _ "github.com/pomerium/pomerium/internal/frontend/statik" // load static assets
) )
const statikNamespace = "web"
// NewTemplates loads pomerium's templates. Panics on failure. // NewTemplates loads pomerium's templates. Panics on failure.
func NewTemplates() (*template.Template, error) { func NewTemplates() (*template.Template, error) {
t := template.New("pomerium-templates") t := template.New("pomerium-templates")
statikFS, err := fs.New() statikFS, err := fs.NewWithNamespace(statikNamespace)
if err != nil { if err != nil {
return nil, fmt.Errorf("internal/frontend: error creating new file system: %w", err) 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 // 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 // if the error is non-nil. It is intended for use in variable initializations
func MustAssetHandler() http.Handler { func MustAssetHandler() http.Handler {
statikFS, err := fs.New() statikFS, err := fs.NewWithNamespace(statikNamespace)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View file

@ -1,63 +0,0 @@
package frontend
import (
"html/template"
"reflect"
"testing"
_ "github.com/pomerium/pomerium/internal/frontend/statik"
"github.com/rakyll/statik/fs"
)
func TestTemplatesCompile(t *testing.T) {
templates := template.Must(NewTemplates())
if templates == nil {
t.Errorf("unexpected nil value %#v", templates)
}
}
func TestNewTemplates(t *testing.T) {
tests := []struct {
name string
testData string
want *template.Template
wantErr bool
}{
{"empty statik fs", "", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs.Register(tt.testData)
got, err := NewTemplates()
if (err != nil) != tt.wantErr {
t.Errorf("NewTemplates() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewTemplates() = %v, want %v", got, tt.want)
}
})
}
}
func TestMustAssetHandler(t *testing.T) {
tests := []struct {
name string
testData string
wantPanic bool
}{
{"empty statik fs", "", true},
{"empty statik fs", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
MustAssetHandler()
})
}
}

View file

@ -163,7 +163,13 @@ func (m *IsAuthorizedRequest_Headers) GetValue() []string {
} }
type IsAuthorizedReply struct { 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_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -194,89 +200,53 @@ func (m *IsAuthorizedReply) XXX_DiscardUnknown() {
var xxx_messageInfo_IsAuthorizedReply proto.InternalMessageInfo var xxx_messageInfo_IsAuthorizedReply proto.InternalMessageInfo
func (m *IsAuthorizedReply) GetIsValid() bool { func (m *IsAuthorizedReply) GetAllow() bool {
if m != nil { if m != nil {
return m.IsValid return m.Allow
} }
return false return false
} }
type IsAdminRequest struct { func (m *IsAuthorizedReply) GetSessionExpired() bool {
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 {
if m != nil { 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 "" return ""
} }
type IsAdminReply struct { func (m *IsAuthorizedReply) GetUser() string {
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 {
if m != nil { 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() { func init() {
@ -284,62 +254,63 @@ func init() {
proto.RegisterMapType((map[string]*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.RequestHeadersEntry") proto.RegisterMapType((map[string]*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.RequestHeadersEntry")
proto.RegisterType((*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.Headers") proto.RegisterType((*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.Headers")
proto.RegisterType((*IsAuthorizedReply)(nil), "authorize.IsAuthorizedReply") 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{ var fileDescriptor_ffbc3c71370bee9a = []byte{
// 390 bytes of a gzipped FileDescriptorProto // 431 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0xc1, 0x8e, 0xda, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x93, 0xcf, 0x6e, 0x13, 0x31,
0x10, 0x86, 0x1b, 0x52, 0x08, 0x19, 0x28, 0x14, 0x53, 0xa9, 0x26, 0x6a, 0x0b, 0x8d, 0xd4, 0x8a, 0x10, 0xc6, 0xd9, 0x6e, 0x9b, 0x76, 0x27, 0xa5, 0xa1, 0x13, 0x84, 0xac, 0x08, 0x68, 0x88, 0x04,
0x5e, 0x52, 0x29, 0xbd, 0x54, 0x55, 0xa5, 0x8a, 0x43, 0x25, 0x38, 0xb4, 0x87, 0xa8, 0xed, 0xa5, 0xe4, 0x94, 0x43, 0xb8, 0x20, 0xc4, 0xa5, 0x87, 0x4a, 0x05, 0x09, 0x0e, 0x16, 0x9c, 0x40, 0x5a,
0x87, 0x28, 0x2b, 0x5b, 0xc2, 0x22, 0x60, 0xd6, 0x76, 0x90, 0xb2, 0xef, 0xb2, 0xef, 0xb7, 0x8f, 0x2d, 0xf2, 0xa8, 0x59, 0xea, 0xac, 0x17, 0xdb, 0x4b, 0x58, 0x1e, 0x94, 0x67, 0xe0, 0x31, 0x90,
0xb1, 0x8a, 0xb1, 0xc3, 0xb2, 0x62, 0xd9, 0x3d, 0xe1, 0xf9, 0xe7, 0x9b, 0xdf, 0xe3, 0x5f, 0x04, 0xff, 0xa5, 0x14, 0x15, 0x38, 0xad, 0xe7, 0xe7, 0xcf, 0xe3, 0x99, 0xf9, 0xbc, 0x30, 0xaa, 0x3a,
0xfa, 0x59, 0xa1, 0x96, 0x5c, 0xb0, 0x2b, 0x1a, 0x6d, 0x05, 0x57, 0x1c, 0xf9, 0xb5, 0x10, 0xde, 0xbb, 0x52, 0xba, 0xfe, 0x4e, 0x8b, 0x56, 0x2b, 0xab, 0xb0, 0xd8, 0x82, 0xd9, 0xcf, 0x1c, 0xc6,
0xb8, 0x30, 0x5c, 0xc8, 0x99, 0xad, 0x49, 0x42, 0x2f, 0x0b, 0x2a, 0x15, 0x7a, 0x0b, 0x50, 0x48, 0xaf, 0xcc, 0x69, 0x8a, 0x05, 0xa7, 0x2f, 0x1d, 0x19, 0x8b, 0x0f, 0x00, 0x3a, 0x43, 0xba, 0xb4,
0x2a, 0x52, 0xc5, 0x57, 0x74, 0x83, 0x9d, 0x89, 0x33, 0xf5, 0x13, 0xbf, 0x52, 0xfe, 0x54, 0x02, 0xea, 0x92, 0x1a, 0x96, 0x4d, 0xb3, 0x79, 0xc1, 0x0b, 0x47, 0xde, 0x39, 0x80, 0x8f, 0xe1, 0x48,
0xfa, 0x00, 0x3d, 0xb1, 0x27, 0xd3, 0x35, 0x55, 0x4b, 0x4e, 0x70, 0x43, 0x23, 0x2f, 0x8c, 0xfa, 0x07, 0x65, 0xb9, 0x26, 0xbb, 0x52, 0x82, 0xed, 0x78, 0xc9, 0xed, 0x48, 0xdf, 0x78, 0x88, 0x27,
0x4b, 0x8b, 0x68, 0x0c, 0x1d, 0x8b, 0x15, 0x22, 0xc7, 0xae, 0x66, 0xc0, 0x48, 0x7f, 0x45, 0x8e, 0x30, 0x4c, 0xb2, 0x4e, 0x4b, 0x96, 0x7b, 0x0d, 0x44, 0xf4, 0x5e, 0x4b, 0x7c, 0x04, 0x87, 0x49,
0xde, 0x43, 0xd7, 0x02, 0x4b, 0x2e, 0x15, 0x7e, 0xae, 0x09, 0x3b, 0x34, 0xe7, 0x52, 0xa1, 0x08, 0xb0, 0x52, 0xc6, 0xb2, 0x5d, 0xaf, 0x48, 0x87, 0xce, 0x95, 0xb1, 0xb8, 0x80, 0x71, 0x92, 0x5c,
0x86, 0x16, 0x39, 0x78, 0x31, 0xdc, 0xd4, 0xe4, 0xc0, 0x48, 0x89, 0xb5, 0x64, 0xc7, 0xfc, 0x9a, 0xe5, 0xaa, 0xd9, 0x9e, 0x57, 0x1e, 0x47, 0xc4, 0x53, 0xca, 0xfa, 0xba, 0x7e, 0xad, 0x2c, 0x95,
0x2b, 0x9a, 0x66, 0x84, 0x08, 0xdc, 0xba, 0xc7, 0x57, 0x9d, 0x19, 0x21, 0x02, 0xfd, 0x87, 0x7e, 0x95, 0x10, 0x9a, 0x0d, 0xfe, 0xd0, 0xbb, 0x9d, 0x53, 0x21, 0x34, 0x7e, 0x80, 0xd1, 0xb6, 0x04,
0xbd, 0x02, 0xcd, 0x08, 0x15, 0x12, 0x7b, 0x13, 0x77, 0xda, 0x89, 0xe3, 0xe8, 0x90, 0xdb, 0x89, 0xaa, 0x04, 0x69, 0xc3, 0xf6, 0xa7, 0xf9, 0x7c, 0xb8, 0x5c, 0x2e, 0xae, 0xe6, 0x76, 0xc3, 0x88,
0x88, 0x22, 0xf3, 0x3b, 0xdf, 0x0f, 0xfd, 0xdc, 0x28, 0x51, 0x26, 0x36, 0x15, 0x23, 0x06, 0x63, 0x16, 0xf1, 0x7b, 0x1e, 0x0e, 0x9d, 0x35, 0x56, 0xf7, 0x3c, 0x4d, 0x25, 0xc2, 0xc9, 0x09, 0xec,
0xf0, 0xcc, 0x11, 0xbd, 0x82, 0xe6, 0x2e, 0xcb, 0x0b, 0x8a, 0x9d, 0x89, 0x3b, 0xf5, 0x93, 0x7d, 0xc7, 0x25, 0xde, 0x85, 0xbd, 0xaf, 0x95, 0xec, 0x88, 0x65, 0xd3, 0x7c, 0x5e, 0xf0, 0x10, 0x4c,
0x11, 0x30, 0x18, 0x9e, 0xf0, 0x41, 0x2f, 0xc1, 0x5d, 0xd1, 0xd2, 0xe4, 0x5e, 0x1d, 0xd1, 0x77, 0x6a, 0x18, 0xdf, 0x90, 0x07, 0xef, 0x40, 0x7e, 0x49, 0x7d, 0x9c, 0xbb, 0x5b, 0xe2, 0xcb, 0x74,
0x3b, 0x5e, 0x05, 0xdd, 0x89, 0x3f, 0x3e, 0xb2, 0x9c, 0x71, 0x33, 0xd7, 0x7c, 0x6b, 0x7c, 0x75, 0xdc, 0x0d, 0x7a, 0xb8, 0x7c, 0xf2, 0x9f, 0xe2, 0x62, 0xb6, 0x78, 0xcd, 0x8b, 0x9d, 0xe7, 0xd9,
0xc2, 0x08, 0x06, 0xc7, 0xe4, 0x36, 0x2f, 0xd1, 0x08, 0xda, 0x4c, 0xa6, 0xbb, 0x2c, 0x67, 0x44, 0xec, 0x47, 0x06, 0xc7, 0xd7, 0xa5, 0xad, 0xec, 0x5d, 0x59, 0x95, 0x94, 0x6a, 0xe3, 0xef, 0x3a,
0xdf, 0xd6, 0x4e, 0x3c, 0x26, 0xff, 0x55, 0x65, 0xf8, 0x19, 0x7a, 0x0b, 0x39, 0x23, 0x6b, 0xb6, 0xe0, 0x21, 0xc0, 0xa7, 0x30, 0x32, 0x64, 0x4c, 0xad, 0x9a, 0x92, 0xbe, 0xb5, 0xb5, 0xa6, 0x60,
0x79, 0xda, 0x9f, 0x22, 0xfc, 0x04, 0xdd, 0x7a, 0xe0, 0xbc, 0x77, 0x7c, 0xed, 0x00, 0xd4, 0xab, 0xf0, 0x01, 0x3f, 0x8a, 0xf8, 0x2c, 0x50, 0x67, 0xa0, 0xa0, 0xa6, 0x2f, 0x35, 0x55, 0x46, 0x35,
0x08, 0xf4, 0x5b, 0x4f, 0xd6, 0xab, 0xa1, 0x77, 0xe7, 0x5f, 0x17, 0xbc, 0x79, 0xb0, 0xbf, 0xcd, 0x86, 0xe5, 0xbe, 0xb9, 0xa1, 0x63, 0x3c, 0x20, 0xf7, 0x94, 0x4c, 0x7d, 0xd1, 0x90, 0x28, 0x3f,
0xcb, 0xf0, 0x19, 0xfa, 0x01, 0x9e, 0xd9, 0x04, 0x8d, 0x8e, 0xd1, 0x3b, 0xcf, 0x09, 0x5e, 0x9f, 0x6f, 0x92, 0xc3, 0x45, 0x20, 0xaf, 0x37, 0x16, 0x11, 0x76, 0xdd, 0xbb, 0x8a, 0x86, 0xfa, 0xb5,
0x6a, 0x69, 0x83, 0x8b, 0x96, 0xfe, 0x50, 0xbe, 0xdc, 0x06, 0x00, 0x00, 0xff, 0xff, 0x72, 0x3a, 0x2b, 0x8a, 0xd6, 0x55, 0x2d, 0xa3, 0x6b, 0x21, 0xc0, 0x7b, 0x30, 0xb8, 0xd0, 0xaa, 0x6b, 0x83,
0xa3, 0xe0, 0x3b, 0x03, 0x00, 0x00, 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. // Reference imports to suppress errors if they are not otherwise used.
var _ context.Context var _ context.Context
var _ grpc.ClientConn var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file // This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against. // 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. // 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. // 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 { type AuthorizerClient interface {
IsAuthorized(ctx context.Context, in *IsAuthorizedRequest, opts ...grpc.CallOption) (*IsAuthorizedReply, error) 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 { type authorizerClient struct {
cc *grpc.ClientConn cc grpc.ClientConnInterface
} }
func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient { func NewAuthorizerClient(cc grpc.ClientConnInterface) AuthorizerClient {
return &authorizerClient{cc} return &authorizerClient{cc}
} }
@ -352,19 +323,9 @@ func (c *authorizerClient) IsAuthorized(ctx context.Context, in *IsAuthorizedReq
return out, nil 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. // AuthorizerServer is the server API for Authorizer service.
type AuthorizerServer interface { type AuthorizerServer interface {
IsAuthorized(context.Context, *IsAuthorizedRequest) (*IsAuthorizedReply, error) IsAuthorized(context.Context, *IsAuthorizedRequest) (*IsAuthorizedReply, error)
IsAdmin(context.Context, *IsAdminRequest) (*IsAdminReply, error)
} }
// UnimplementedAuthorizerServer can be embedded to have forward compatible implementations. // 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) { func (*UnimplementedAuthorizerServer) IsAuthorized(ctx context.Context, req *IsAuthorizedRequest) (*IsAuthorizedReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented") 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) { func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
s.RegisterService(&_Authorizer_serviceDesc, srv) 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) 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{ var _Authorizer_serviceDesc = grpc.ServiceDesc{
ServiceName: "authorize.Authorizer", ServiceName: "authorize.Authorizer",
HandlerType: (*AuthorizerServer)(nil), HandlerType: (*AuthorizerServer)(nil),
@ -426,10 +366,6 @@ var _Authorizer_serviceDesc = grpc.ServiceDesc{
MethodName: "IsAuthorized", MethodName: "IsAuthorized",
Handler: _Authorizer_IsAuthorized_Handler, Handler: _Authorizer_IsAuthorized_Handler,
}, },
{
MethodName: "IsAdmin",
Handler: _Authorizer_IsAdmin_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "authorize.proto", Metadata: "authorize.proto",

View file

@ -4,7 +4,6 @@ package authorize;
service Authorizer { service Authorizer {
rpc IsAuthorized(IsAuthorizedRequest) returns (IsAuthorizedReply) {} rpc IsAuthorized(IsAuthorizedRequest) returns (IsAuthorizedReply) {}
rpc IsAdmin(IsAdminRequest) returns (IsAdminReply) {}
} }
message IsAuthorizedRequest { message IsAuthorizedRequest {
@ -30,8 +29,13 @@ message IsAuthorizedRequest {
map<string, Headers> request_headers = 7; map<string, Headers> request_headers = 7;
} }
message IsAuthorizedReply { bool is_valid = 1; } message IsAuthorizedReply {
bool allow = 1;
bool session_expired = 2; // special case
repeated string deny_reasons = 3;
string signed_jwt = 4;
string user = 5;
string email = 6;
repeated string groups = 7;
}
message IsAdminRequest { string user_token = 1; }
message IsAdminReply { bool is_valid = 1; }

View file

@ -16,31 +16,28 @@ import (
type Authorizer interface { type Authorizer interface {
// Authorize takes a route and user session and returns whether the // Authorize takes a route and user session and returns whether the
// request is valid per access policy // request is valid per access policy
Authorize(ctx context.Context, user string, r *http.Request) (bool, error) Authorize(ctx context.Context, user string, r *http.Request) (*pb.IsAuthorizedReply, error)
// IsAdmin takes a session and returns whether the user is an administrator
IsAdmin(ctx context.Context, user string) (bool, error)
// Close closes the auth connection if any. // Close closes the auth connection if any.
Close() error Close() error
} }
// Client is a gRPC implementation of an authenticator (authorize client) // Client is a gRPC implementation of an authenticator (authorize client)
type Client struct { type Client struct {
Conn *grpc.ClientConn conn *grpc.ClientConn
client pb.AuthorizerClient client pb.AuthorizerClient
} }
// New returns a new authorize service client. // New returns a new authorize service client.
func New(conn *grpc.ClientConn) (p *Client, err error) { 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 // Authorize takes a route and user session and returns whether the
// request is valid per access policy // 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") ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.Authorize")
defer span.End() defer span.End()
// var h map[string]&structpb.ListValue{} in := &pb.IsAuthorizedRequest{
response, err := c.client.IsAuthorized(ctx, &pb.IsAuthorizedRequest{
UserToken: user, UserToken: user,
RequestHost: r.Host, RequestHost: r.Host,
RequestMethod: r.Method, RequestMethod: r.Method,
@ -48,25 +45,13 @@ func (c *Client) Authorize(ctx context.Context, user string, r *http.Request) (b
RequestRemoteAddr: r.RemoteAddr, RequestRemoteAddr: r.RemoteAddr,
RequestRequestUri: r.RequestURI, RequestRequestUri: r.RequestURI,
RequestUrl: r.URL.String(), RequestUrl: r.URL.String(),
}) }
return response.GetIsValid(), err return c.client.IsAuthorized(ctx, in)
}
// 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
} }
// Close tears down the ClientConn and all underlying connections. // Close tears down the ClientConn and all underlying connections.
func (c *Client) Close() error { func (c *Client) Close() error {
return c.Conn.Close() return c.conn.Close()
} }
type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers

View file

@ -3,13 +3,15 @@ package client
import ( import (
"context" "context"
"net/http" "net/http"
pb "github.com/pomerium/pomerium/internal/grpc/authorize"
) )
var _ Authorizer = &MockAuthorize{} var _ Authorizer = &MockAuthorize{}
// MockAuthorize provides a mocked implementation of the authorizer interface. // MockAuthorize provides a mocked implementation of the authorizer interface.
type MockAuthorize struct { type MockAuthorize struct {
AuthorizeResponse bool AuthorizeResponse *pb.IsAuthorizedReply
AuthorizeError error AuthorizeError error
IsAdminResponse bool IsAdminResponse bool
IsAdminError error IsAdminError error
@ -20,11 +22,6 @@ type MockAuthorize struct {
func (a MockAuthorize) Close() error { return a.CloseError } func (a MockAuthorize) Close() error { return a.CloseError }
// Authorize is a mocked authorizer client function. // 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 return a.AuthorizeResponse, a.AuthorizeError
} }
// IsAdmin is a mocked IsAdmin function.
func (a MockAuthorize) IsAdmin(ctx context.Context, user string) (bool, error) {
return a.IsAdminResponse, a.IsAdminError
}

View file

@ -195,7 +195,9 @@ func init() {
proto.RegisterType((*SetReply)(nil), "cache.SetReply") 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{ var fileDescriptor_5fca3b110c9bbf3a = []byte{
// 176 bytes of a gzipped FileDescriptorProto // 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. // Reference imports to suppress errors if they are not otherwise used.
var _ context.Context var _ context.Context
var _ grpc.ClientConn var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file // This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against. // 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. // CacheClient is the client API for Cache service.
// //
@ -229,10 +231,10 @@ type CacheClient interface {
} }
type cacheClient struct { type cacheClient struct {
cc *grpc.ClientConn cc grpc.ClientConnInterface
} }
func NewCacheClient(cc *grpc.ClientConn) CacheClient { func NewCacheClient(cc grpc.ClientConnInterface) CacheClient {
return &cacheClient{cc} return &cacheClient{cc}
} }

View file

@ -1,11 +1,14 @@
package httputil // import "github.com/pomerium/pomerium/internal/httputil" package httputil // import "github.com/pomerium/pomerium/internal/httputil"
// Pomerium headers contain information added to a request.
const ( const (
// HeaderPomeriumResponse is set when pomerium itself creates a response, // HeaderPomeriumResponse is set when pomerium itself creates a response,
// as opposed to the downstream application and can be used to distinguish // as opposed to the downstream application and can be used to distinguish
// between an application error, and a pomerium related error when debugging. // between an application error, and a pomerium related error when debugging.
// Especially useful when working with single page apps (SPA). // Especially useful when working with single page apps (SPA).
HeaderPomeriumResponse = "x-pomerium-intercepted-response" 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 // HeadersContentSecurityPolicy are the content security headers added to the service's handlers

View file

@ -63,9 +63,8 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
} }
userAPI.Path = "/api/v1/users/" userAPI.Path = "/api/v1/users/"
oktaProvider.userAPI = userAPI oktaProvider.userAPI = userAPI
} else { } 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 return &oktaProvider, nil

View file

@ -51,26 +51,22 @@ func NewStore(o *Options) *Store {
// LoadSession looks for a preset query parameter in the request body // LoadSession looks for a preset query parameter in the request body
// representing the key to lookup from the cache. // 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 // look for our cache's key in the default query param
sessionID := r.URL.Query().Get(s.queryParam) sessionID := r.URL.Query().Get(s.queryParam)
if sessionID == "" { if sessionID == "" {
return nil, "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
exists, val, err := s.cache.Get(r.Context(), sessionID) exists, val, err := s.cache.Get(r.Context(), sessionID)
if err != nil { if err != nil {
log.FromRequest(r).Debug().Msg("sessions/cache: miss, trying wrapped loader") log.FromRequest(r).Debug().Msg("sessions/cache: miss, trying wrapped loader")
return nil, "", err return "", err
} }
if !exists { if !exists {
return nil, "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
var session sessions.State
if err := s.encoder.Unmarshal(val, &session); err != nil { return string(val), nil
log.FromRequest(r).Error().Err(err).Msg("sessions/cache: unmarshal")
return nil, "", sessions.ErrMalformed
}
return &session, string(val), nil
} }
// ClearSession clears the session from the wrapped store. // ClearSession clears the session from the wrapped store.

View file

@ -163,13 +163,6 @@ func TestStore_LoadSession(t *testing.T) {
defaultOptions.QueryParam, defaultOptions.QueryParam,
&mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}},
true}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -187,7 +180,7 @@ func TestStore_LoadSession(t *testing.T) {
r.URL.RawQuery = q.Encode() r.URL.RawQuery = q.Encode()
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
_, _, err := s.LoadSession(r) _, err := s.LoadSession(r)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("Store.LoadSession() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("Store.LoadSession() error = %v, wantErr %v", err, tt.wantErr)
return return

View file

@ -125,10 +125,10 @@ func getCookies(r *http.Request, name string) []*http.Cookie {
} }
// LoadSession returns a State from the cookie in the request. // 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) cookies := getCookies(r, cs.Name)
if len(cookies) == 0 { if len(cookies) == 0 {
return nil, "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
for _, cookie := range cookies { for _, cookie := range cookies {
jwt := loadChunkedCookie(r, cookie) jwt := loadChunkedCookie(r, cookie)
@ -136,10 +136,10 @@ func (cs *Store) LoadSession(r *http.Request) (*sessions.State, string, error) {
session := &sessions.State{} session := &sessions.State{}
err := cs.decoder.Unmarshal([]byte(jwt), session) err := cs.decoder.Unmarshal([]byte(jwt), session)
if err == nil { 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. // SaveSession saves a session state to a request's cookie store.

View file

@ -99,8 +99,7 @@ func TestStore_SaveSession(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
tests := []struct { tests := []struct {
name string name string
// State *State
State interface{} State interface{}
encoder encoding.Marshaler encoder encoding.Marshaler
decoder encoding.Unmarshaler decoder encoding.Unmarshaler
@ -138,16 +137,20 @@ func TestStore_SaveSession(t *testing.T) {
r.AddCookie(cookie) r.AddCookie(cookie)
} }
state, _, err := s.LoadSession(r) enc := ecjson.New(c)
jwt, err := s.LoadSession(r)
if (err != nil) != tt.wantLoadErr { if (err != nil) != tt.wantLoadErr {
t.Errorf("LoadSession() error = %v, wantErr %v", err, tt.wantLoadErr) t.Errorf("LoadSession() error = %v, wantErr %v", err, tt.wantLoadErr)
return return
} }
var state sessions.State
enc.Unmarshal([]byte(jwt), &state)
cmpOpts := []cmp.Option{ cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(sessions.State{}), cmpopts.IgnoreUnexported(sessions.State{}),
} }
if err == nil { 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) t.Errorf("Store.LoadSession() got = %s", diff)
} }
} }

View file

@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler { func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _, err := sessions.FromContext(r.Context()) _, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int 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}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -42,16 +42,12 @@ func NewStore(enc encoding.Unmarshaler, headerType string) *Store {
} }
// LoadSession tries to retrieve the token string from the Authorization header. // 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) jwt := TokenFromHeader(r, as.authHeader, as.authType)
if jwt == "" { if jwt == "" {
return nil, "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
var session sessions.State return jwt, nil
if err := as.encoder.Unmarshal([]byte(jwt), &session); err != nil {
return nil, "", sessions.ErrMalformed
}
return &session, jwt, nil
} }
// TokenFromHeader retrieves the value of the authorization header from a given // TokenFromHeader retrieves the value of the authorization header from a given

View file

@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler { func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _, err := sessions.FromContext(r.Context()) _, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int 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}, {"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}, {"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}, {"bad auth type", "bees ", sessions.State{Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized},
} }

View file

@ -4,15 +4,12 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"github.com/pomerium/pomerium/internal/urlutil"
) )
// Context keys // Context keys
var ( var (
SessionCtxKey = &contextKey{"Session"} SessionCtxKey = &contextKey{"Session"}
SessionJWTCtxKey = &contextKey{"SessionJWT"} ErrorCtxKey = &contextKey{"Error"}
ErrorCtxKey = &contextKey{"Error"}
) )
// RetrieveSession takes a slice of session loaders and tries to find a valid // RetrieveSession takes a slice of session loaders and tries to find a valid
@ -27,8 +24,8 @@ func retrieve(s ...SessionLoader) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
hfn := func(w http.ResponseWriter, r *http.Request) { hfn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
state, jwt, err := retrieveFromRequest(r, s...) jwt, err := retrieveFromRequest(r, s...)
ctx = NewContext(ctx, state, jwt, err) ctx = NewContext(ctx, jwt, err)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
} }
return http.HandlerFunc(hfn) 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 // retrieveFromRequest extracts sessions state from the request by calling
// token find functions in the order they where provided. // 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 { for _, s := range sessions {
state, jwt, err := s.LoadSession(r) jwt, err := s.LoadSession(r)
if err != nil && !errors.Is(err, ErrNoSessionFound) { if err != nil && !errors.Is(err, ErrNoSessionFound) {
return state, jwt, err return "", err
} } else if err == nil {
if state != nil { return jwt, nil
//todo(bdd): have authz verify
err := state.Verify(urlutil.StripPort(r.Host))
return state, jwt, err // N.B.: state is _not_ nil
} }
} }
return nil, "", ErrNoSessionFound return "", ErrNoSessionFound
} }
// NewContext sets context values for the user session state and error. // NewContext sets context values for the user session state and error.
func NewContext(ctx context.Context, t *State, jwt string, err error) context.Context { func NewContext(ctx context.Context, jwt string, err error) context.Context {
ctx = context.WithValue(ctx, SessionCtxKey, t) ctx = context.WithValue(ctx, SessionCtxKey, jwt)
ctx = context.WithValue(ctx, SessionJWTCtxKey, jwt)
ctx = context.WithValue(ctx, ErrorCtxKey, err) ctx = context.WithValue(ctx, ErrorCtxKey, err)
return ctx return ctx
} }
// FromContext retrieves context values for the user session state and error. // FromContext retrieves context values for the user session state and error.
func FromContext(ctx context.Context) (*State, string, error) { func FromContext(ctx context.Context) (string, error) {
state, _ := ctx.Value(SessionCtxKey).(*State) jwt, _ := ctx.Value(SessionCtxKey).(string)
jwt, _ := ctx.Value(SessionJWTCtxKey).(string)
err, _ := ctx.Value(ErrorCtxKey).(error) 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 // 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 { type contextKey struct {
name string name string
} }
func (k *contextKey) String() string {
return "context value " + k.name
}

View file

@ -1,4 +1,4 @@
package sessions package sessions_test
import ( import (
"context" "context"
@ -11,22 +11,40 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"gopkg.in/square/go-jose.v2/jwt" "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) { func TestNewContext(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
ctx context.Context ctx context.Context
t *State t *sessions.State
err error err error
want context.Context 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctxOut := NewContext(tt.ctx, tt.t, "", tt.err) signer, err := jws.NewHS256Signer(cryptutil.NewKey(), "issuer")
stateOut, _, errOut := FromContext(ctxOut) 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 != "" { if diff := cmp.Diff(tt.t.Email, stateOut.Email); diff != "" {
t.Errorf("NewContext() = %s", 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 { func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _, err := FromContext(r.Context()) _, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return 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) { func TestVerifier(t *testing.T) {
fnh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fnh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@ -102,14 +75,13 @@ func TestVerifier(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
store store store mock.Store
state State state sessions.State
wantBody string
wantStatus int wantStatus int
}{ }{
{"empty session", store{}, State{}, "internal/sessions: session is not found\n", 401}, {"empty session", mock.Store{LoadError: sessions.ErrNoSessionFound}, sessions.State{}, 401},
{"simple good load", store{Session: &State{Subject: "hi", Expiry: jwt.NewNumericDate(time.Now().Add(time.Second))}}, State{}, "OK", 200}, {"simple good load", mock.Store{Session: &sessions.State{Subject: "hi", Expiry: jwt.NewNumericDate(time.Now().Add(time.Second))}}, sessions.State{}, 200},
{"empty session", store{LoadError: errors.New("err")}, State{}, "err\n", 401}, {"session error", mock.Store{LoadError: errors.New("err")}, sessions.State{}, 401},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -118,15 +90,11 @@ func TestVerifier(t *testing.T) {
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder() w := httptest.NewRecorder()
got := RetrieveSession(tt.store)(testAuthorizer((fnh))) got := sessions.RetrieveSession(tt.store)(testAuthorizer((fnh)))
got.ServeHTTP(w, r) got.ServeHTTP(w, r)
gotBody := w.Body.String()
gotStatus := w.Result().StatusCode 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 != "" { if diff := cmp.Diff(gotStatus, tt.wantStatus); diff != "" {
t.Errorf("RetrieveSession() = %v", diff) t.Errorf("RetrieveSession() = %v", diff)
} }

View file

@ -4,6 +4,8 @@ package mock // import "github.com/pomerium/pomerium/internal/sessions/mock"
import ( import (
"net/http" "net/http"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
) )
@ -13,10 +15,11 @@ var _ sessions.SessionLoader = &Store{}
// Store is a mock implementation of the SessionStore interface // Store is a mock implementation of the SessionStore interface
type Store struct { type Store struct {
ResponseSession string ResponseSession string
SessionJWT string
Session *sessions.State Session *sessions.State
SaveError error SaveError error
LoadError error LoadError error
Secret []byte
Encrypted bool
} }
// ClearSession clears the ResponseSession // ClearSession clears the ResponseSession
@ -25,8 +28,11 @@ func (ms *Store) ClearSession(http.ResponseWriter, *http.Request) {
} }
// LoadSession returns the session and a error // LoadSession returns the session and a error
func (ms Store) LoadSession(*http.Request) (*sessions.State, string, error) { func (ms Store) LoadSession(*http.Request) (string, error) {
return ms.Session, ms.SessionJWT, ms.LoadError 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. // SaveSession returns a save error.

View file

@ -1,16 +1,17 @@
package mock // import "github.com/pomerium/pomerium/internal/sessions/mock" package mock // import "github.com/pomerium/pomerium/internal/sessions/mock"
import ( import (
"reflect"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
) )
func TestStore(t *testing.T) { func TestStore(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mockCSRF *Store store *Store
wantLoad string
saveSession *sessions.State saveSession *sessions.State
wantLoadErr bool wantLoadErr bool
wantSaveErr bool wantSaveErr bool
@ -22,26 +23,27 @@ func TestStore(t *testing.T) {
SaveError: nil, SaveError: nil,
LoadError: nil, LoadError: nil,
}, },
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IiIsInByb2dyYW1hdGljIjpmYWxzZSwic3ViIjoiMDEwMSJ9.u0dzrEkbt-Bec7Rq85E8pbglE61D7UqGN33MFtfoCCM",
&sessions.State{Subject: "0101"}, &sessions.State{Subject: "0101"},
false, false,
false}, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ms := tt.mockCSRF ms := tt.store
err := ms.SaveSession(nil, nil, tt.saveSession) err := ms.SaveSession(nil, nil, tt.saveSession)
if (err != nil) != tt.wantSaveErr { 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 return
} }
got, _, err := ms.LoadSession(nil) got, err := ms.LoadSession(nil)
if (err != nil) != tt.wantLoadErr { 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 return
} }
if !reflect.DeepEqual(got, tt.mockCSRF.Session) { if diff := cmp.Diff(got, tt.wantLoad); diff != "" {
t.Errorf("MockCSRFStore.GetCSRF() = %v, want %v", got, tt.mockCSRF.Session) t.Errorf("mockstore.LoadSession() = %v", diff)
} }
ms.ClearSession(nil, nil) ms.ClearSession(nil, nil)
if ms.ResponseSession != "" { if ms.ResponseSession != "" {

View file

@ -18,7 +18,7 @@ import (
func testAuthorizer(next http.Handler) http.Handler { func testAuthorizer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _, err := sessions.FromContext(r.Context()) _, err := sessions.FromContext(r.Context())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
@ -42,8 +42,6 @@ func TestVerifier(t *testing.T) {
wantStatus int 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}, {"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}, {"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 { for _, tt := range tests {

View file

@ -41,16 +41,13 @@ func NewStore(enc encoding.MarshalUnmarshaler, qp string) *Store {
} }
// LoadSession tries to retrieve the token string from URL query parameters. // 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) jwt := r.URL.Query().Get(qp.queryParamKey)
if jwt == "" { if jwt == "" {
return nil, "", sessions.ErrNoSessionFound return "", sessions.ErrNoSessionFound
} }
var session sessions.State
if err := qp.decoder.Unmarshal([]byte(jwt), &session); err != nil { return jwt, nil
return nil, "", sessions.ErrMalformed
}
return &session, jwt, nil
} }
// ClearSession clears the session cookie from a request's query param key `pomerium_session`. // ClearSession clears the session cookie from a request's query param key `pomerium_session`.

View file

@ -13,11 +13,6 @@ import (
"gopkg.in/square/go-jose.v2/jwt" "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. // timeNow is time.Now but pulled out as a variable for tests.
var timeNow = time.Now 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. // Verify returns an error if the users's session state is not valid.
func (s *State) Verify(audience string) error { 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 return ErrNotValidYet
} }
if s.Expiry != nil && timeNow().Add(-DefaultLeeway).After(s.Expiry.Time()) { if s.Expiry != nil && timeNow().After(s.Expiry.Time()) {
return ErrExpired 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 return ErrIssuedInTheFuture
} }
// if we have an associated access token, check if that token has expired as well // 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 return ErrExpired
} }

View file

@ -15,5 +15,5 @@ type SessionStore interface {
// SessionLoader defines an interface for loading a session. // SessionLoader defines an interface for loading a session.
type SessionLoader interface { type SessionLoader interface {
LoadSession(*http.Request) (*State, string, error) LoadSession(*http.Request) (string, error)
} }

View file

@ -40,7 +40,6 @@ func (p *Proxy) postSessionSetNOP(w http.ResponseWriter, r *http.Request) error
func (p *Proxy) nginxCallback(w http.ResponseWriter, r *http.Request) error { func (p *Proxy) nginxCallback(w http.ResponseWriter, r *http.Request) error {
encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted) encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil { if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
return httputil.NewError(http.StatusBadRequest, err) return httputil.NewError(http.StatusBadRequest, err)
} }
w.Header().Set("Content-Type", "text/plain; charset=utf-8") 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) encryptedSession := q.Get(urlutil.QuerySessionEncrypted)
if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil { if _, err := p.saveCallbackSession(w, r, encryptedSession); err != nil {
return httputil.NewError(http.StatusBadRequest, err) return httputil.NewError(http.StatusBadRequest, err)
} }
w.Header().Set("Content-Type", "text/plain; charset=utf-8") 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. // provider. If the user is unauthorized, a `401` error is returned.
func (p *Proxy) Verify(verifyOnly bool) http.Handler { func (p *Proxy) Verify(verifyOnly bool) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { 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) { if status := r.FormValue("auth_status"); status == fmt.Sprint(http.StatusForbidden) {
return httputil.NewError(http.StatusForbidden, errors.New(http.StatusText(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 { if err != nil {
return httputil.NewError(http.StatusBadRequest, err) return httputil.NewError(http.StatusBadRequest, err)
} }
jwt, err := sessions.FromContext(r.Context())
s, _, err := sessions.FromContext(r.Context()) if err != nil {
if errors.Is(err, sessions.ErrNoSessionFound) || errors.Is(err, sessions.ErrExpired) {
if verifyOnly { if verifyOnly {
return httputil.NewError(http.StatusUnauthorized, err) return httputil.NewError(http.StatusUnauthorized, err)
} }
@ -94,18 +92,14 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
authN.RawQuery = q.Encode() authN.RawQuery = q.Encode()
httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &authN).String(), http.StatusFound) httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &authN).String(), http.StatusFound)
return nil return nil
} else if err != nil {
return httputil.NewError(http.StatusUnauthorized, err)
} }
// depending on the configuration of the fronting proxy, the request Host var s sessions.State
// and/or `X-Forwarded-Host` may be untrustd or change so we reverify if err := p.encoder.Unmarshal([]byte(jwt), &s); err != nil {
// the session's validity against the supplied uri return httputil.NewError(http.StatusBadRequest, err)
if err := s.Verify(uri.Hostname()); err != nil {
return httputil.NewError(http.StatusUnauthorized, err)
} }
p.addPomeriumHeaders(w, r)
r.Host = uri.Host r.Host = uri.Host
if err := p.authorize(r); err != nil { if err := p.authorize(w, r); err != nil {
return err return err
} }

View file

@ -13,7 +13,9 @@ import (
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding" "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/encoding/mock"
pb "github.com/pomerium/pomerium/internal/grpc/authorize"
"github.com/pomerium/pomerium/internal/grpc/authorize/client" "github.com/pomerium/pomerium/internal/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
@ -42,29 +44,24 @@ func TestProxy_ForwardAuth(t *testing.T) {
wantStatus int wantStatus int
wantBody string 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 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: true}, http.StatusOK, ""}, {"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 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: &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", 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: &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: 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", 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: &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: 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", 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: &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: 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"},
{"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 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"}, {"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"},
{"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"},
// traefik // 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, ""}, {"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: true}, http.StatusBadRequest, ""}, {"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: 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 // // 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 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: true}, http.StatusUnauthorized, ""}, {"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: true}, http.StatusBadRequest, ""}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -72,9 +69,13 @@ func TestProxy_ForwardAuth(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
p.encoder = tt.cipher
p.sessionStore = tt.sessionStore p.sessionStore = tt.sessionStore
p.AuthorizeClient = tt.authorizer p.AuthorizeClient = tt.authorizer
signer, err := jws.NewHS256Signer(nil, "mock")
if err != nil {
t.Fatal(err)
}
p.encoder = signer
p.UpdateOptions(tt.options) p.UpdateOptions(tt.options)
uri, err := url.Parse(tt.requestURI) uri, err := url.Parse(tt.requestURI)
if err != nil { if err != nil {
@ -91,10 +92,10 @@ func TestProxy_ForwardAuth(t *testing.T) {
uri.RawQuery = queryString.Encode() uri.RawQuery = queryString.Encode()
r := httptest.NewRequest(tt.method, uri.String(), nil) r := httptest.NewRequest(tt.method, uri.String(), nil)
state, _, _ := tt.sessionStore.LoadSession(r) state, _ := tt.sessionStore.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
if len(tt.headers) != 0 { if len(tt.headers) != 0 {

View file

@ -33,8 +33,11 @@ func (p *Proxy) registerDashboardHandlers(r *mux.Router) *mux.Router {
)) ))
// dashboard endpoints can be used by user's to view, or modify their session // 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("/").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) 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 // Authenticate service callback handlers and middleware
// callback used to set route-scoped session and redirect back to destination // 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. // UserDashboard lets users investigate, and refresh their current session.
// It also contains certain administrative actions like user impersonation. // It also contains certain administrative actions like user impersonation.
//
// Nota bene: This endpoint does authentication, not authorization. // Nota bene: This endpoint does authentication, not authorization.
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
return err return err
} }
var s sessions.State
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), jwt) if err := p.encoder.Unmarshal([]byte(jwt), &s); err != nil {
if err != nil { return httputil.NewError(http.StatusBadRequest, err)
return err
} }
p.templates.ExecuteTemplate(w, "dashboard.html", map[string]interface{}{ p.templates.ExecuteTemplate(w, "dashboard.html", map[string]interface{}{
"Session": session, "Session": s,
"IsAdmin": isAdmin,
"csrfField": csrf.TemplateField(r), "csrfField": csrf.TemplateField(r),
"ImpersonateAction": urlutil.QueryImpersonateAction, "ImpersonateAction": urlutil.QueryImpersonateAction,
"ImpersonateEmail": urlutil.QueryImpersonateEmail, "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 // to the user's current user sessions state if the user is currently an
// administrative user. Requests are redirected back to the user dashboard. // administrative user. Requests are redirected back to the user dashboard.
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) error { 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 := urlutil.GetAbsoluteURL(r)
redirectURL.Path = dashboardURL // redirect back to the dashboard redirectURL.Path = dashboardURL // redirect back to the dashboard
signinURL := *p.authenticateSigninURL signinURL := *p.authenticateSigninURL

View file

@ -17,6 +17,7 @@ import (
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/mock" "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/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
@ -79,10 +80,9 @@ func TestProxy_UserDashboard(t *testing.T) {
wantAdminForm bool wantAdminForm bool
wantStatus int 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}, {"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}, {"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},
{"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},
} }
for _, tt := range tests { for _, tt := range tests {
@ -96,9 +96,9 @@ func TestProxy_UserDashboard(t *testing.T) {
p.AuthorizeClient = tt.authorizer p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest(tt.method, "/", nil) r := httptest.NewRequest(tt.method, "/", nil)
state, _, _ := tt.session.LoadSession(r) state, _ := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
@ -137,11 +137,7 @@ func TestProxy_Impersonate(t *testing.T) {
wantStatus int 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, 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}, {"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},
{"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},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -159,9 +155,9 @@ func TestProxy_Impersonate(t *testing.T) {
uri := &url.URL{Path: "/"} uri := &url.URL{Path: "/"}
r := httptest.NewRequest(tt.method, uri.String(), bytes.NewBufferString(postForm.Encode())) 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 := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
@ -246,12 +242,12 @@ func TestProxy_Callback(t *testing.T) {
wantStatus int wantStatus int
wantBody string 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", 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: 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: true}, http.StatusBadRequest, ""}, {"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: 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: 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: 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -387,12 +383,12 @@ func TestProxy_ProgrammaticCallback(t *testing.T) {
wantStatus int wantStatus int
wantBody string 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", 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: 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: true}, http.StatusBadRequest, ""}, {"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: 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: 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: 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -7,8 +7,10 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"github.com/rs/zerolog"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
@ -16,17 +18,6 @@ import (
"github.com/pomerium/pomerium/internal/urlutil" "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 // AuthenticateSession is middleware to enforce a valid authentication
// session state is retrieved from the users's request context. // session state is retrieved from the users's request context.
func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler { 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") ctx, span := trace.StartSpan(r.Context(), "proxy.AuthenticateSession")
defer span.End() defer span.End()
_, _, err := sessions.FromContext(ctx) if _, err := sessions.FromContext(ctx); err != nil {
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 {
log.FromRequest(r).Debug().Err(err).Msg("proxy: session state") log.FromRequest(r).Debug().Err(err).Msg("proxy: session state")
return p.redirectToSignin(w, r) return p.redirectToSignin(w, r)
} }
p.addPomeriumHeaders(w, r)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
return nil 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") ctx, span := trace.StartSpan(ctx, "proxy.AuthenticateSession/refresh")
defer span.End() defer span.End()
s, _, err := sessions.FromContext(ctx) s := &sessions.State{}
if !errors.Is(err, sessions.ErrExpired) || s == nil { if err := p.encoder.Unmarshal([]byte(oldSession), s); err != nil {
return nil, errors.New("proxy: unexpected session state for refresh") return "", httputil.NewError(http.StatusBadRequest, err)
} }
// 1 - build a signed url to call refresh on authenticate service // 1 - build a signed url to call refresh on authenticate service
refreshURI := *p.authenticateRefreshURL refreshURI := *p.authenticateRefreshURL
q := refreshURI.Query() q := refreshURI.Query()
q.Set(urlutil.QueryAccessTokenID, s.AccessTokenID) // hash value points to parent token 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() refreshURI.RawQuery = q.Encode()
signedRefreshURL := urlutil.NewSignedURL(p.SharedKey, &refreshURI).String() signedRefreshURL := urlutil.NewSignedURL(p.SharedKey, &refreshURI).String()
// 2 - http call to authenticate service // 2 - http call to authenticate service
req, err := http.NewRequestWithContext(ctx, http.MethodGet, signedRefreshURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, signedRefreshURL, nil)
if err != 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("X-Requested-With", "XmlHttpRequest")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
res, err := httputil.DefaultClient.Do(req) res, err := httputil.DefaultClient.Do(req)
if err != nil { 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() 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 { if err != nil {
return nil, err return "", err
} }
// auth couldn't refersh the session, delete the session and reload via 302 // auth couldn't refersh the session, delete the session and reload via 302
if res.StatusCode != http.StatusOK { 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 return string(newJwt), nil
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
} }
func (p *Proxy) redirectToSignin(w http.ResponseWriter, r *http.Request) error { 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 signinURL := *p.authenticateSigninURL
q := signinURL.Query() q := signinURL.Query()
q.Set(urlutil.QueryRedirectURI, urlutil.GetAbsoluteURL(r).String()) q.Set(urlutil.QueryRedirectURI, urlutil.GetAbsoluteURL(r).String())
signinURL.RawQuery = q.Encode() signinURL.RawQuery = q.Encode()
log.FromRequest(r).Debug().Str("url", signinURL.String()).Msg("proxy: redirectToSignin") log.FromRequest(r).Debug().Str("url", signinURL.String()).Msg("proxy: redirectToSignin")
httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &signinURL).String(), http.StatusFound) httputil.Redirect(w, r, urlutil.NewSignedURL(p.SharedKey, &signinURL).String(), http.StatusFound)
p.sessionStore.ClearSession(w, r)
return nil 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. // AuthorizeSession is middleware to enforce a user is authorized for a request.
// Session state is retrieved from the users's request context. // Session state is retrieved from the users's request context.
func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler { func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx, span := trace.StartSpan(r.Context(), "proxy.AuthorizeSession") ctx, span := trace.StartSpan(r.Context(), "proxy.AuthorizeSession")
defer span.End() defer span.End()
if err := p.authorize(r.WithContext(ctx)); err != nil { if err := p.authorize(w, r); err != nil {
log.FromRequest(r).Debug().Err(err).Msg("proxy: AuthorizeSession")
return err return err
} }
next.ServeHTTP(w, r.WithContext(ctx)) 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 { func (p *Proxy) authorize(w http.ResponseWriter, r *http.Request) error {
s, jwt, err := sessions.FromContext(r.Context()) ctx, span := trace.StartSpan(r.Context(), "proxy.authorize")
defer span.End()
jwt, err := sessions.FromContext(ctx)
if err != nil { if err != nil {
return httputil.NewError(http.StatusInternalServerError, err) 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 { if err != nil {
return err return httputil.NewError(http.StatusInternalServerError, err)
} else if !authorized {
return httputil.NewError(http.StatusForbidden, fmt.Errorf("%s is not authorized for %s", s.RequestEmail(), r.Host))
} }
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, authz, err = p.AuthorizeClient.Authorize(ctx, newJwt, r)
// email, and group. Session state is retrieved from the users's request context if err != nil {
func (p *Proxy) SignRequest(signer encoding.Marshaler) func(next http.Handler) http.Handler { return httputil.NewError(http.StatusUnauthorized, err)
return func(next http.Handler) http.Handler { }
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx, span := trace.StartSpan(r.Context(), "proxy.SignRequest")
defer span.End()
s, _, err := sessions.FromContext(r.Context())
if err != nil {
return httputil.NewError(http.StatusForbidden, err)
}
newSession := s.NewSession(r.Host, []string{r.Host})
jwt, err := signer.Marshal(newSession.RouteSession())
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed signing jwt")
} else {
r.Header.Set(HeaderJWT, string(jwt))
w.Header().Set(HeaderJWT, string(jwt))
}
next.ServeHTTP(w, r.WithContext(ctx))
return nil
})
} }
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. // SetResponseHeaders sets a map of response headers.
@ -198,3 +154,26 @@ func SetResponseHeaders(headers map[string]string) func(next http.Handler) http.
}) })
} }
} }
func (p *Proxy) userDetailsLoggerMiddleware(next http.Handler) http.Handler {
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
if jwt, err := sessions.FromContext(r.Context()); err == nil {
var s sessions.State
if err := p.encoder.Unmarshal([]byte(jwt), &s); err == nil {
l := log.Ctx(r.Context())
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Strs("groups", s.Groups)
})
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("email", s.Email)
})
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("user-id", s.User)
})
}
}
next.ServeHTTP(w, r)
return nil
})
}

View file

@ -10,14 +10,14 @@ import (
"time" "time"
"github.com/google/go-cmp/cmp" "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"
"github.com/pomerium/pomerium/internal/encoding/mock" "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/grpc/authorize/client"
"github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
mstore "github.com/pomerium/pomerium/internal/sessions/mock" mstore "github.com/pomerium/pomerium/internal/sessions/mock"
"gopkg.in/square/go-jose.v2/jwt"
) )
func TestProxy_AuthenticateSession(t *testing.T) { 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}, {"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}, {"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", 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},
{"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},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -73,13 +66,13 @@ func TestProxy_AuthenticateSession(t *testing.T) {
encoder: tt.encoder, encoder: tt.encoder,
} }
r := httptest.NewRequest(http.MethodGet, "/", nil) r := httptest.NewRequest(http.MethodGet, "/", nil)
state, _, _ := tt.session.LoadSession(r) state, _ := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder() w := httptest.NewRecorder()
got := a.AuthenticateSession(fn) got := a.userDetailsLoggerMiddleware(a.AuthenticateSession(fn))
got.ServeHTTP(w, r) got.ServeHTTP(w, r)
if status := w.Code; status != tt.wantStatus { if status := w.Code; status != tt.wantStatus {
t.Errorf("AuthenticateSession() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String()) t.Errorf("AuthenticateSession() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
@ -96,35 +89,45 @@ func TestProxy_AuthorizeSession(t *testing.T) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}) })
tests := []struct { tests := []struct {
name string name string
session sessions.SessionStore refreshRespStatus int
authzClient client.Authorizer session sessions.SessionStore
authzClient client.Authorizer
ctxError error ctxError error
provider identity.Authenticator provider identity.Authenticator
wantStatus int 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 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", &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}, {"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", &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}, {"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", &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}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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{ a := Proxy{
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="), cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
authenticateURL: uriParseHelper("https://authenticate.corp.example"), authenticateURL: uriParseHelper("https://authenticate.corp.example"),
authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"), authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
sessionStore: tt.session, authenticateRefreshURL: uriParseHelper(rURL),
AuthorizeClient: tt.authzClient, sessionStore: tt.session,
AuthorizeClient: tt.authzClient,
encoder: &mock.Encoder{},
} }
r := httptest.NewRequest(http.MethodGet, "/", nil) r := httptest.NewRequest(http.MethodGet, "/", nil)
state, _, _ := tt.session.LoadSession(r) state, _ := tt.session.LoadSession(r)
ctx := r.Context() ctx := r.Context()
ctx = sessions.NewContext(ctx, state, "", tt.ctxError) ctx = sessions.NewContext(ctx, state, tt.ctxError)
r = r.WithContext(ctx) r = r.WithContext(ctx)
r.Header.Set("Accept", "application/json") r.Header.Set("Accept", "application/json")
w := httptest.NewRecorder() 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) { func TestProxy_SetResponseHeaders(t *testing.T) {
t.Parallel() t.Parallel()
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 := SetResponseHeaders(tt.setHeaders)(fn)
got.ServeHTTP(w, r) got.ServeHTTP(w, r)
if diff := cmp.Diff(w.Body.String(), tt.wantHeaders); diff != "" { if diff := cmp.Diff(w.Body.String(), tt.wantHeaders); diff != "" {
t.Errorf("SignRequest() :\n %s", diff) t.Errorf("SetResponseHeaders() :\n %s", diff)
} }
}) })
} }

View file

@ -63,12 +63,6 @@ func ValidateOptions(o config.Options) error {
if err := urlutil.ValidateURL(o.AuthorizeURL); err != nil { if err := urlutil.ValidateURL(o.AuthorizeURL); err != nil {
return fmt.Errorf("proxy: invalid 'AUTHORIZE_SERVICE_URL': %w", err) 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 return nil
} }
@ -95,7 +89,6 @@ type Proxy struct {
Handler http.Handler Handler http.Handler
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
sessionLoaders []sessions.SessionLoader sessionLoaders []sessions.SessionLoader
signingKey string
templates *template.Template templates *template.Template
} }
@ -142,8 +135,7 @@ func New(opts config.Options) (*Proxy, error) {
cookieStore, cookieStore,
header.NewStore(encoder, "Pomerium"), header.NewStore(encoder, "Pomerium"),
queryparam.NewStore(encoder, "pomerium_session")}, queryparam.NewStore(encoder, "pomerium_session")},
signingKey: opts.SigningKey, templates: template.Must(frontend.NewTemplates()),
templates: template.Must(frontend.NewTemplates()),
} }
// errors checked in ValidateOptions // errors checked in ValidateOptions
p.authorizeURL, _ = urlutil.DeepCopy(opts.AuthorizeURL) p.authorizeURL, _ = urlutil.DeepCopy(opts.AuthorizeURL)
@ -188,7 +180,6 @@ func (p *Proxy) UpdateOptions(o config.Options) error {
// UpdatePolicies updates the H basedon the configured policies // UpdatePolicies updates the H basedon the configured policies
func (p *Proxy) UpdatePolicies(opts *config.Options) error { func (p *Proxy) UpdatePolicies(opts *config.Options) error {
var err error
if len(opts.Policies) == 0 { if len(opts.Policies) == 0 {
log.Warn().Msg("proxy: configuration has no policies") 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 { if err := policy.Validate(); err != nil {
return fmt.Errorf("proxy: invalid policy %w", err) return fmt.Errorf("proxy: invalid policy %w", err)
} }
r, err = p.reverseProxyHandler(r, policy) r = p.reverseProxyHandler(r, policy)
if err != nil {
return err
}
} }
p.Handler = r p.Handler = r
return nil 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 // 1. Create the reverse proxy connection
proxy := stdhttputil.NewSingleHostReverseProxy(policy.Destination) proxy := stdhttputil.NewSingleHostReverseProxy(policy.Destination)
// 2. Create a sublogger to handle any error logs // 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 // Optional: if a public route, skip access control middleware
if policy.AllowPublicUnauthenticatedAccess { if policy.AllowPublicUnauthenticatedAccess {
log.Warn().Str("route", policy.String()).Msg("proxy: all access control disabled") 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 // 4. Retrieve the user session and add it to the request context
rp.Use(sessions.RetrieveSession(p.sessionLoaders...)) 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) rp.Use(p.AuthenticateSession)
// 6. AuthZ - Verify the user is authorized for route // 6. AuthZ - Verify the user is authorized for route
rp.Use(p.AuthorizeSession) rp.Use(p.AuthorizeSession)
// 7. Strip the user session cookie from the downstream request // 7. Strip the user session cookie from the downstream request
rp.Use(middleware.StripCookie(p.cookieOptions.Name)) 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 return r
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
} }
// roundTripperFromPolicy adjusts the std library's `DefaultTransport RoundTripper` // roundTripperFromPolicy adjusts the std library's `DefaultTransport RoundTripper`

View file

@ -51,8 +51,6 @@ func TestOptions_Validate(t *testing.T) {
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^" invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
shortCookieLength := testOptions(t) shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
invalidSignKey := testOptions(t)
invalidSignKey.SigningKey = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
badSharedKey := testOptions(t) badSharedKey := testOptions(t)
badSharedKey.SharedKey = "" badSharedKey.SharedKey = ""
sharedKeyBadBas64 := testOptions(t) sharedKeyBadBas64 := testOptions(t)
@ -75,7 +73,6 @@ func TestOptions_Validate(t *testing.T) {
{"invalid cookie secret", invalidCookieSecret, true}, {"invalid cookie secret", invalidCookieSecret, true},
{"short cookie secret", shortCookieLength, true}, {"short cookie secret", shortCookieLength, true},
{"no shared secret", badSharedKey, true}, {"no shared secret", badSharedKey, true},
{"invalid signing key", invalidSignKey, true},
{"shared secret bad base64", sharedKeyBadBas64, true}, {"shared secret bad base64", sharedKeyBadBas64, true},
} }
for _, tt := range tests { for _, tt := range tests {
@ -94,8 +91,6 @@ func TestNew(t *testing.T) {
good := testOptions(t) good := testOptions(t)
shortCookieLength := testOptions(t) shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
badRoutedProxy := testOptions(t)
badRoutedProxy.SigningKey = "YmFkIGtleQo="
badCookie := testOptions(t) badCookie := testOptions(t)
badCookie.CookieName = "" badCookie.CookieName = ""
badPolicyURL := config.Policy{To: "http://", From: "http://bar.example"} badPolicyURL := config.Policy{To: "http://", From: "http://bar.example"}
@ -113,7 +108,6 @@ func TestNew(t *testing.T) {
{"good", good, true, false}, {"good", good, true, false},
{"empty options", config.Options{}, false, true}, {"empty options", config.Options{}, false, true},
{"short secret/validate sanity check", shortCookieLength, 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}, {"invalid cookie name, empty", badCookie, false, true},
{"bad policy, bad policy url", badNewPolicy, false, true}, {"bad policy, bad policy url", badNewPolicy, false, true},
} }
@ -186,30 +180,27 @@ func Test_UpdateOptions(t *testing.T) {
name string name string
originalOptions config.Options originalOptions config.Options
updatedOptions config.Options updatedOptions config.Options
signingKey string
host string host string
wantErr bool wantErr bool
wantRoute bool wantRoute bool
}{ }{
{"good no change", good, good, "", "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", good, newPolicies, "https://bar.example", false, true},
{"changed and missing", good, newPolicies, "", "https://corp.example.example", false, false}, {"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}, {"bad change bad policy url", good, badNewPolicy, "https://bar.example", true, false},
{"good signing key", good, newPolicies, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQ==", "https://corp.example.example", false, true}, {"disable tls verification", good, disableTLSPolicies, "https://bar.example", false, true},
{"bad change bad policy url", good, badNewPolicy, "", "https://bar.example", true, false}, {"custom root ca", good, customCAPolicies, "https://bar.example", false, true},
{"disable tls verification", good, disableTLSPolicies, "", "https://bar.example", false, true}, {"bad custom root ca base64", good, badCustomCAPolicies, "https://bar.example", true, false},
{"custom root ca", good, customCAPolicies, "", "https://bar.example", false, true}, {"good client certs", good, goodClientCertPolicies, "https://bar.example", false, true},
{"bad custom root ca base64", good, badCustomCAPolicies, "", "https://bar.example", true, false}, {"custom server name", customServerName, customServerName, "https://bar.example", false, true},
{"good client certs", good, goodClientCertPolicies, "", "https://bar.example", false, true}, {"good no policies to start", emptyPolicies, good, "https://corp.example.example", false, true},
{"custom server name", customServerName, customServerName, "", "https://bar.example", false, true}, {"allow websockets", good, allowWebSockets, "https://corp.example.example", false, true},
{"good no policies to start", emptyPolicies, good, "", "https://corp.example.example", false, true}, {"no websockets, custom timeout", good, customTimeout, "https://corp.example.example", false, true},
{"allow websockets", good, allowWebSockets, "", "https://corp.example.example", false, true}, {"enable cors preflight", good, corsPreflight, "https://corp.example.example", false, true},
{"no websockets, custom timeout", good, customTimeout, "", "https://corp.example.example", false, true}, {"disable auth", good, disableAuth, "https://corp.example.example", false, true},
{"enable cors preflight", good, corsPreflight, "", "https://corp.example.example", false, true}, {"enable forward auth", good, fwdAuth, "https://corp.example.example", false, true},
{"disable auth", good, disableAuth, "", "https://corp.example.example", false, true}, {"set request headers", good, reqHeaders, "https://corp.example.example", false, true},
{"enable forward auth", good, fwdAuth, "", "https://corp.example.example", false, true}, {"preserve host headers", preserveHostHeader, preserveHostHeader, "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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -218,7 +209,6 @@ func Test_UpdateOptions(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
p.signingKey = tt.signingKey
err = p.UpdateOptions(tt.updatedOptions) err = p.UpdateOptions(tt.updatedOptions)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("UpdateOptions: err = %v, wantErr = %v", err, tt.wantErr) t.Errorf("UpdateOptions: err = %v, wantErr = %v", err, tt.wantErr)
@ -269,10 +259,8 @@ func TestNewReverseProxy(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
proxyHandler, err := p.reverseProxyHandler(mux.NewRouter(), newPolicy) proxyHandler := p.reverseProxyHandler(mux.NewRouter(), newPolicy)
if err != nil {
t.Fatal(err)
}
ts.Config.Handler = proxyHandler ts.Config.Handler = proxyHandler
getReq, _ := http.NewRequest("GET", newPolicy.From, nil) getReq, _ := http.NewRequest("GET", newPolicy.From, nil)