mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-28 08:27:26 +02:00
authorize: use jwt insead of state struct (#514)
authenticate: unmarshal and verify state from jwt, instead of middleware authorize: embed opa policy using statik authorize: have IsAuthorized handle authorization for all routes authorize: if no signing key is provided, one is generated authorize: remove IsAdmin grpc endpoint authorize/client: return authorize decision struct cmd/pomerium: main logger no longer contains email and group cryptutil: add ECDSA signing methods dashboard: have impersonate form show up for all users, but have api gated by authz docs: fix typo in signed jwt header encoding/jws: remove unused es256 signer frontend: namespace static web assets internal/sessions: remove leeway to match authz policy proxy: move signing functionality to authz proxy: remove jwt attestation from proxy (authZ does now) proxy: remove non-signed headers from headers proxy: remove special handling of x-forwarded-host sessions: do not verify state in middleware sessions: remove leeway from state to match authz sessions/{all}: store jwt directly instead of state Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
a477af9378
commit
8d1732582e
61 changed files with 1083 additions and 1264 deletions
|
@ -10,14 +10,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
"github.com/pomerium/pomerium/internal/encoding/mock"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize/client"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
mstore "github.com/pomerium/pomerium/internal/sessions/mock"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func TestProxy_AuthenticateSession(t *testing.T) {
|
||||
|
@ -42,14 +42,7 @@ func TestProxy_AuthenticateSession(t *testing.T) {
|
|||
}{
|
||||
{"good", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, nil, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
|
||||
{"invalid session", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
|
||||
{"expired", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
|
||||
{"expired and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
|
||||
{"invalid session and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusUnauthorized},
|
||||
{"expired and refreshed ok", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK},
|
||||
{"expired and save failed", 200, false, &mstore.Store{SaveError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
|
||||
{"expired and unmarshal failed", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{UnmarshalError: errors.New("err")}, "", http.StatusFound},
|
||||
{"expired and malformed session", 200, false, &mstore.Store{Session: nil}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
|
||||
{"expired and refresh failed", 500, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
|
||||
{"expired", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -73,13 +66,13 @@ func TestProxy_AuthenticateSession(t *testing.T) {
|
|||
encoder: tt.encoder,
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
state, _, _ := tt.session.LoadSession(r)
|
||||
state, _ := tt.session.LoadSession(r)
|
||||
ctx := r.Context()
|
||||
ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
|
||||
ctx = sessions.NewContext(ctx, state, tt.ctxError)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
got := a.AuthenticateSession(fn)
|
||||
got := a.userDetailsLoggerMiddleware(a.AuthenticateSession(fn))
|
||||
got.ServeHTTP(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("AuthenticateSession() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
|
||||
|
@ -96,35 +89,45 @@ func TestProxy_AuthorizeSession(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
session sessions.SessionStore
|
||||
authzClient client.Authorizer
|
||||
name string
|
||||
refreshRespStatus int
|
||||
session sessions.SessionStore
|
||||
authzClient client.Authorizer
|
||||
|
||||
ctxError error
|
||||
provider identity.Authenticator
|
||||
|
||||
wantStatus int
|
||||
}{
|
||||
{"user is authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, nil, identity.MockProvider{}, http.StatusOK},
|
||||
{"user is not authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: false}, nil, identity.MockProvider{}, http.StatusForbidden},
|
||||
{"ctx error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"authz client error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"user is authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, nil, identity.MockProvider{}, http.StatusOK},
|
||||
{"user is not authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: false}}, nil, identity.MockProvider{}, http.StatusUnauthorized},
|
||||
{"ctx error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"authz client error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"expired, reauth failed", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{SessionExpired: true}}, nil, identity.MockProvider{}, http.StatusUnauthorized},
|
||||
//todo(bdd): it's a bit tricky to test the refresh flow
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.refreshRespStatus)
|
||||
fmt.Fprintln(w, "REFRESH GOOD")
|
||||
}))
|
||||
defer ts.Close()
|
||||
rURL := ts.URL
|
||||
a := Proxy{
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
|
||||
authenticateURL: uriParseHelper("https://authenticate.corp.example"),
|
||||
authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
|
||||
sessionStore: tt.session,
|
||||
AuthorizeClient: tt.authzClient,
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
|
||||
authenticateURL: uriParseHelper("https://authenticate.corp.example"),
|
||||
authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
|
||||
authenticateRefreshURL: uriParseHelper(rURL),
|
||||
sessionStore: tt.session,
|
||||
AuthorizeClient: tt.authzClient,
|
||||
encoder: &mock.Encoder{},
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
state, _, _ := tt.session.LoadSession(r)
|
||||
state, _ := tt.session.LoadSession(r)
|
||||
ctx := r.Context()
|
||||
ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
|
||||
ctx = sessions.NewContext(ctx, state, tt.ctxError)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -137,69 +140,6 @@ func TestProxy_AuthorizeSession(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type mockJWTSigner struct {
|
||||
SignError error
|
||||
}
|
||||
|
||||
// Sign implements the JWTSigner interface from the cryptutil package, but just
|
||||
// base64's the inputs instead for stesting.
|
||||
func (s *mockJWTSigner) Marshal(v interface{}) ([]byte, error) {
|
||||
|
||||
return []byte("ok"), s.SignError
|
||||
}
|
||||
|
||||
func TestProxy_SignRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprint(w, http.StatusText(http.StatusOK))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session sessions.SessionStore
|
||||
|
||||
signerError error
|
||||
ctxError error
|
||||
|
||||
wantStatus int
|
||||
wantHeaders string
|
||||
}{
|
||||
{"good", &mstore.Store{Session: &sessions.State{Email: "test"}}, nil, nil, http.StatusOK, "ok"},
|
||||
{"invalid session", &mstore.Store{Session: &sessions.State{Email: "test"}}, nil, errors.New("err"), http.StatusForbidden, ""},
|
||||
{"signature failure, warn but ok", &mstore.Store{Session: &sessions.State{Email: "test"}}, errors.New("err"), nil, http.StatusOK, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
a := Proxy{
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
|
||||
authenticateURL: uriParseHelper("https://authenticate.corp.example"),
|
||||
authenticateSigninURL: uriParseHelper("https://authenticate.corp.example/sign_in"),
|
||||
sessionStore: tt.session,
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
state, _, _ := tt.session.LoadSession(r)
|
||||
ctx := r.Context()
|
||||
ctx = sessions.NewContext(ctx, state, "", tt.ctxError)
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
signer := &mockJWTSigner{SignError: tt.signerError}
|
||||
got := a.SignRequest(signer)(fn)
|
||||
got.ServeHTTP(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("SignRequest() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
|
||||
}
|
||||
if headers := r.Header.Get(HeaderJWT); tt.wantHeaders != headers {
|
||||
t.Errorf("SignRequest() headers = %v, want %v", headers, tt.wantHeaders)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_SetResponseHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -230,7 +170,7 @@ func TestProxy_SetResponseHeaders(t *testing.T) {
|
|||
got := SetResponseHeaders(tt.setHeaders)(fn)
|
||||
got.ServeHTTP(w, r)
|
||||
if diff := cmp.Diff(w.Body.String(), tt.wantHeaders); diff != "" {
|
||||
t.Errorf("SignRequest() :\n %s", diff)
|
||||
t.Errorf("SetResponseHeaders() :\n %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue