mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-01 10:22:43 +02:00
authenticate: make service http only
- Rename SessionState to State to avoid stutter. - Simplified option validation to use a wrapper function for base64 secrets. - Removed authenticates grpc code. - Abstracted logic to load and validate a user's authenticate session. - Removed instances of url.Parse in favor of urlutil's version. - proxy: replaces grpc refresh logic with forced deadline advancement. - internal/sessions: remove rest store; parse authorize header as part of session store. - proxy: refactor request signer - sessions: remove extend deadline (fixes #294) - remove AuthenticateInternalAddr - remove AuthenticateInternalAddrString - omit type tag.Key from declaration of vars TagKey* it will be inferred from the right-hand side - remove compatibility package xerrors - use cloned http.DefaultTransport as base transport
This commit is contained in:
parent
bc72d08ad4
commit
380d314404
53 changed files with 718 additions and 2280 deletions
|
@ -1,117 +0,0 @@
|
|||
package clients // import "github.com/pomerium/pomerium/proxy/clients"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||
pb "github.com/pomerium/pomerium/proto/authenticate"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Authenticator provides the authenticate service interface
|
||||
type Authenticator interface {
|
||||
// Redeem takes a code and returns a validated session or an error
|
||||
Redeem(context.Context, string) (*sessions.SessionState, error)
|
||||
// Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
|
||||
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
|
||||
// Validate evaluates a given oidc id_token for validity. Returns validity and any error.
|
||||
Validate(context.Context, string) (bool, error)
|
||||
// Close closes the authenticator connection if any.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// NewAuthenticateClient returns a new authenticate service client. Presently,
|
||||
// only gRPC is supported and is always returned so name is ignored.
|
||||
func NewAuthenticateClient(name string, opts *Options) (a Authenticator, err error) {
|
||||
return NewGRPCAuthenticateClient(opts)
|
||||
}
|
||||
|
||||
// NewGRPCAuthenticateClient returns a new authenticate service client.
|
||||
func NewGRPCAuthenticateClient(opts *Options) (p *AuthenticateGRPC, err error) {
|
||||
conn, err := NewGRPCClientConn(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authClient := pb.NewAuthenticatorClient(conn)
|
||||
return &AuthenticateGRPC{Conn: conn, client: authClient}, nil
|
||||
}
|
||||
|
||||
// AuthenticateGRPC is a gRPC implementation of an authenticator (authenticate client)
|
||||
type AuthenticateGRPC struct {
|
||||
Conn *grpc.ClientConn
|
||||
client pb.AuthenticatorClient
|
||||
}
|
||||
|
||||
// Redeem makes an RPC call to the authenticate service to creates a session state
|
||||
// from an encrypted code provided as a result of an oauth2 callback process.
|
||||
func (a *AuthenticateGRPC) Redeem(ctx context.Context, code string) (*sessions.SessionState, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.Redeem")
|
||||
defer span.End()
|
||||
|
||||
if code == "" {
|
||||
return nil, errors.New("missing code")
|
||||
}
|
||||
protoSession, err := a.client.Authenticate(ctx, &pb.AuthenticateRequest{Code: code})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, err := pb.SessionFromProto(protoSession)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// Refresh makes an RPC call to the authenticate service to attempt to refresh the
|
||||
// user's session. Requires a valid refresh token. Will return an error if the identity provider
|
||||
// has revoked the session or if the refresh token is no longer valid in this context.
|
||||
func (a *AuthenticateGRPC) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.Refresh")
|
||||
defer span.End()
|
||||
|
||||
if s.RefreshToken == "" {
|
||||
return nil, errors.New("missing refresh token")
|
||||
}
|
||||
req, err := pb.ProtoFromSession(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// todo(bdd): add grpc specific timeouts to main options
|
||||
// todo(bdd): handle request id (metadata!?) in grpc receiver and add to ctx logger
|
||||
reply, err := a.client.Refresh(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newSession, err := pb.SessionFromProto(reply)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSession, nil
|
||||
}
|
||||
|
||||
// Validate makes an RPC call to the authenticate service to validate the JWT id token;
|
||||
// does NOT do nonce or revokation validation.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func (a *AuthenticateGRPC) Validate(ctx context.Context, idToken string) (bool, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.Validate")
|
||||
defer span.End()
|
||||
|
||||
if idToken == "" {
|
||||
return false, errors.New("missing id token")
|
||||
}
|
||||
|
||||
r, err := a.client.Validate(ctx, &pb.ValidateRequest{IdToken: idToken})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return r.IsValid, nil
|
||||
}
|
||||
|
||||
// Close tears down the ClientConn and all underlying connections.
|
||||
func (a *AuthenticateGRPC) Close() error {
|
||||
return a.Conn.Close()
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
package clients // import "github.com/pomerium/pomerium/proxy/clients"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
pb "github.com/pomerium/pomerium/proto/authenticate"
|
||||
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serviceName string
|
||||
opts *Options
|
||||
wantErr bool
|
||||
}{
|
||||
{"grpc good", "grpc", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "secret"}, false},
|
||||
{"grpc missing shared secret", "grpc", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: ""}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewAuthenticateClient(tt.serviceName, tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
|
||||
// rpcMsg implements the gomock.Matcher interface
|
||||
type rpcMsg struct {
|
||||
msg proto.Message
|
||||
}
|
||||
|
||||
func (r *rpcMsg) Matches(msg interface{}) bool {
|
||||
m, ok := msg.(proto.Message)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return proto.Equal(m, r.msg)
|
||||
}
|
||||
|
||||
func (r *rpcMsg) String() string {
|
||||
return fmt.Sprintf("is %s", r.msg)
|
||||
}
|
||||
|
||||
func TestProxy_Redeem(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
|
||||
req := &pb.AuthenticateRequest{Code: "unit_test"}
|
||||
mockExpire, err := ptypes.TimestampProto(fixedDate)
|
||||
if err != nil {
|
||||
t.Fatalf("%v failed converting timestamp", err)
|
||||
}
|
||||
|
||||
mockAuthenticateClient.EXPECT().Authenticate(
|
||||
gomock.Any(),
|
||||
&rpcMsg{msg: req},
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
RefreshDeadline: mockExpire,
|
||||
}, nil)
|
||||
tests := []struct {
|
||||
name string
|
||||
idToken string
|
||||
want *sessions.SessionState
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "unit_test", &sessions.SessionState{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IDToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
RefreshDeadline: (fixedDate),
|
||||
}, false},
|
||||
{"empty code", "", nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := AuthenticateGRPC{client: mockAuthenticateClient}
|
||||
got, err := a.Redeem(context.Background(), tt.idToken)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Proxy.AuthenticateValidate() error = %v,\n wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != nil {
|
||||
if got.AccessToken != "mocked access token" {
|
||||
t.Errorf("authenticate: invalid access token")
|
||||
}
|
||||
if got.RefreshToken != "mocked refresh token" {
|
||||
t.Errorf("authenticate: invalid refresh token")
|
||||
}
|
||||
if got.IDToken != "mocked id token" {
|
||||
t.Errorf("authenticate: invalid id token")
|
||||
}
|
||||
if got.User != "user1" {
|
||||
t.Errorf("authenticate: invalid user")
|
||||
}
|
||||
if got.Email != "test@email.com" {
|
||||
t.Errorf("authenticate: invalid email")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestProxy_AuthenticateValidate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
|
||||
req := &pb.ValidateRequest{IdToken: "unit_test"}
|
||||
|
||||
mockAuthenticateClient.EXPECT().Validate(
|
||||
gomock.Any(),
|
||||
&rpcMsg{msg: req},
|
||||
).Return(&pb.ValidateReply{IsValid: false}, nil)
|
||||
|
||||
ac := mockAuthenticateClient
|
||||
tests := []struct {
|
||||
name string
|
||||
idToken string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "unit_test", false, false},
|
||||
{"empty id token", "", false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := AuthenticateGRPC{client: ac}
|
||||
|
||||
got, err := a.Validate(context.Background(), tt.idToken)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Proxy.AuthenticateValidate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Proxy.AuthenticateValidate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_AuthenticateRefresh(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockRefreshClient := mock.NewMockAuthenticatorClient(ctrl)
|
||||
mockExpire, _ := ptypes.TimestampProto(fixedDate)
|
||||
|
||||
mockRefreshClient.EXPECT().Refresh(
|
||||
gomock.Any(),
|
||||
gomock.Not(sessions.SessionState{RefreshToken: "fail"}),
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: mockExpire,
|
||||
}, nil).AnyTimes()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *sessions.SessionState
|
||||
want *sessions.SessionState
|
||||
wantErr bool
|
||||
}{
|
||||
{"good",
|
||||
&sessions.SessionState{RefreshToken: "unit_test"},
|
||||
&sessions.SessionState{
|
||||
AccessToken: "new access token",
|
||||
RefreshDeadline: fixedDate,
|
||||
}, false},
|
||||
{"empty refresh token", &sessions.SessionState{RefreshToken: ""}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := AuthenticateGRPC{client: mockRefreshClient}
|
||||
|
||||
got, err := a.Refresh(context.Background(), tt.session)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Proxy.AuthenticateRefresh() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Proxy.AuthenticateRefresh() got = \n%#v\nwant \n%#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGRPC(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *Options
|
||||
wantErr bool
|
||||
wantErrStr string
|
||||
wantTarget string
|
||||
}{
|
||||
{"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""},
|
||||
{"empty connection", &Options{Addr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
|
||||
{"both internal and addr empty", &Options{Addr: nil, InternalAddr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
|
||||
{"addr with port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"},
|
||||
{"addr without port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"internal addr with port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"},
|
||||
{"internal addr without port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"cert override", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"custom ca", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "localhost.example:443"},
|
||||
{"bad ca encoding", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "localhost.example:443"},
|
||||
{"custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "localhost.example:443"},
|
||||
{"bad custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "localhost.example:443"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewGRPCAuthenticateClient(tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewGRPCAuthenticateClient() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if !strings.EqualFold(err.Error(), tt.wantErrStr) {
|
||||
t.Errorf("NewGRPCAuthenticateClient() error = %v did not contain wantErr %v", err, tt.wantErrStr)
|
||||
}
|
||||
}
|
||||
if got != nil && got.Conn.Target() != tt.wantTarget {
|
||||
t.Errorf("NewGRPCAuthenticateClient() target = %v expected %v", got.Conn.Target(), tt.wantTarget)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -15,9 +15,9 @@ import (
|
|||
type Authorizer interface {
|
||||
// Authorize takes a route and user session and returns whether the
|
||||
// request is valid per access policy
|
||||
Authorize(context.Context, string, *sessions.SessionState) (bool, error)
|
||||
Authorize(context.Context, string, *sessions.State) (bool, error)
|
||||
// IsAdmin takes a session and returns whether the user is an administrator
|
||||
IsAdmin(context.Context, *sessions.SessionState) (bool, error)
|
||||
IsAdmin(context.Context, *sessions.State) (bool, error)
|
||||
// Close closes the auth connection if any.
|
||||
Close() error
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ type AuthorizeGRPC struct {
|
|||
|
||||
// Authorize takes a route and user session and returns whether the
|
||||
// request is valid per access policy
|
||||
func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
|
||||
func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.Authorize")
|
||||
defer span.End()
|
||||
|
||||
|
@ -65,7 +65,7 @@ func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions
|
|||
}
|
||||
|
||||
// IsAdmin takes a session and returns whether the user is an administrator
|
||||
func (a *AuthorizeGRPC) IsAdmin(ctx context.Context, s *sessions.SessionState) (bool, error) {
|
||||
func (a *AuthorizeGRPC) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.IsAdmin")
|
||||
defer span.End()
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ package clients
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
@ -23,12 +25,12 @@ func TestAuthorizeGRPC_Authorize(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
route string
|
||||
s *sessions.SessionState
|
||||
s *sessions.State
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "hello.pomerium.io", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
|
||||
{"impersonate request", "hello.pomerium.io", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false},
|
||||
{"good", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
|
||||
{"impersonate request", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false},
|
||||
{"session cannot be nil", "hello.pomerium.io", nil, false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -56,11 +58,11 @@ func TestAuthorizeGRPC_IsAdmin(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
s *sessions.SessionState
|
||||
s *sessions.State
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
|
||||
{"good", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
|
||||
{"session cannot be nil", nil, false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -77,3 +79,41 @@ func TestAuthorizeGRPC_IsAdmin(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGRPC(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *Options
|
||||
wantErr bool
|
||||
wantErrStr string
|
||||
wantTarget string
|
||||
}{
|
||||
{"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""},
|
||||
{"empty connection", &Options{Addr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
|
||||
{"both internal and addr empty", &Options{Addr: nil, InternalAddr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
|
||||
{"addr with port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"},
|
||||
{"addr without port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"internal addr with port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"},
|
||||
{"internal addr without port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"cert override", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "localhost.example:443"},
|
||||
{"custom ca", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "localhost.example:443"},
|
||||
{"bad ca encoding", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "localhost.example:443"},
|
||||
{"custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "localhost.example:443"},
|
||||
{"bad custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "localhost.example:443"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewGRPCAuthorizeClient(tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewGRPCAuthorizeClient() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if !strings.EqualFold(err.Error(), tt.wantErrStr) {
|
||||
t.Errorf("NewGRPCAuthorizeClient() error = %v did not contain wantErr %v", err, tt.wantErrStr)
|
||||
}
|
||||
}
|
||||
if got != nil && got.Conn.Target() != tt.wantTarget {
|
||||
t.Errorf("NewGRPCAuthorizeClient() target = %v expected %v", got.Conn.Target(), tt.wantTarget)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/telemetry/metrics"
|
||||
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/balancer/roundrobin"
|
||||
|
@ -25,7 +26,7 @@ const defaultGRPCPort = 443
|
|||
|
||||
// Options contains options for connecting to a pomerium rpc service.
|
||||
type Options struct {
|
||||
// Addr is the location of the authenticate service. e.g. "service.corp.example:8443"
|
||||
// Addr is the location of the service. e.g. "service.corp.example:8443"
|
||||
Addr *url.URL
|
||||
// InternalAddr is the internal (behind the ingress) address to use when
|
||||
// making a connection. If empty, Addr is used.
|
||||
|
@ -34,7 +35,7 @@ type Options struct {
|
|||
// returned certificates from the server. gRPC internals also use it to override the virtual
|
||||
// hosting name if it is set.
|
||||
OverrideCertificateName string
|
||||
// Shared secret is used to authenticate a authenticate-client with a authenticate-server.
|
||||
// Shared secret is used to mutually authenticate a client and server.
|
||||
SharedSecret string
|
||||
// CA specifies the base64 encoded TLS certificate authority to use.
|
||||
CA string
|
||||
|
|
|
@ -6,35 +6,6 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
)
|
||||
|
||||
// MockAuthenticate provides a mocked implementation of the authenticator interface.
|
||||
type MockAuthenticate struct {
|
||||
RedeemError error
|
||||
RedeemResponse *sessions.SessionState
|
||||
RefreshResponse *sessions.SessionState
|
||||
RefreshError error
|
||||
ValidateResponse bool
|
||||
ValidateError error
|
||||
CloseError error
|
||||
}
|
||||
|
||||
// Redeem is a mocked authenticator client function.
|
||||
func (a MockAuthenticate) Redeem(ctx context.Context, code string) (*sessions.SessionState, error) {
|
||||
return a.RedeemResponse, a.RedeemError
|
||||
}
|
||||
|
||||
// Refresh is a mocked authenticator client function.
|
||||
func (a MockAuthenticate) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
|
||||
return a.RefreshResponse, a.RefreshError
|
||||
}
|
||||
|
||||
// Validate is a mocked authenticator client function.
|
||||
func (a MockAuthenticate) Validate(ctx context.Context, idToken string) (bool, error) {
|
||||
return a.ValidateResponse, a.ValidateError
|
||||
}
|
||||
|
||||
// Close is a mocked authenticator client function.
|
||||
func (a MockAuthenticate) Close() error { return a.CloseError }
|
||||
|
||||
// MockAuthorize provides a mocked implementation of the authorizer interface.
|
||||
type MockAuthorize struct {
|
||||
AuthorizeResponse bool
|
||||
|
@ -48,11 +19,11 @@ type MockAuthorize struct {
|
|||
func (a MockAuthorize) Close() error { return a.CloseError }
|
||||
|
||||
// Authorize is a mocked authorizer client function.
|
||||
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
|
||||
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) {
|
||||
return a.AuthorizeResponse, a.AuthorizeError
|
||||
}
|
||||
|
||||
// IsAdmin is a mocked IsAdmin function.
|
||||
func (a MockAuthorize) IsAdmin(ctx context.Context, s *sessions.SessionState) (bool, error) {
|
||||
func (a MockAuthorize) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) {
|
||||
return a.IsAdminResponse, a.IsAdminError
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
)
|
||||
|
||||
func TestMockAuthenticate(t *testing.T) {
|
||||
// Absurd, but I caught a typo this way.
|
||||
redeemResponse := &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
}
|
||||
ma := &MockAuthenticate{
|
||||
RedeemError: errors.New("redeem error"),
|
||||
RedeemResponse: redeemResponse,
|
||||
RefreshResponse: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
},
|
||||
RefreshError: errors.New("refresh error"),
|
||||
ValidateResponse: true,
|
||||
ValidateError: errors.New("validate error"),
|
||||
CloseError: errors.New("close error"),
|
||||
}
|
||||
got, gotErr := ma.Redeem(context.Background(), "a")
|
||||
if gotErr.Error() != "redeem error" {
|
||||
t.Errorf("unexpected value for gotErr %s", gotErr)
|
||||
}
|
||||
if !reflect.DeepEqual(redeemResponse, got) {
|
||||
t.Errorf("unexpected value for redeemResponse %s", got)
|
||||
}
|
||||
newSession, gotErr := ma.Refresh(context.Background(), nil)
|
||||
if gotErr.Error() != "refresh error" {
|
||||
t.Errorf("unexpected value for gotErr %s", gotErr)
|
||||
}
|
||||
if !reflect.DeepEqual(newSession, redeemResponse) {
|
||||
t.Errorf("unexpected value for newSession %s", newSession)
|
||||
}
|
||||
|
||||
ok, gotErr := ma.Validate(context.Background(), "a")
|
||||
if !ok {
|
||||
t.Errorf("unexpected value for ok : %t", ok)
|
||||
}
|
||||
if gotErr.Error() != "validate error" {
|
||||
t.Errorf("unexpected value for gotErr %s", gotErr)
|
||||
}
|
||||
gotErr = ma.Close()
|
||||
if gotErr.Error() != "close error" {
|
||||
t.Errorf("unexpected value for ma.CloseError %s", gotErr)
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/templates"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
// StateParameter holds the redirect id along with the session id.
|
||||
|
@ -36,9 +37,9 @@ func (p *Proxy) Handler() http.Handler {
|
|||
mux.HandleFunc("/.pomerium", p.UserDashboard)
|
||||
mux.HandleFunc("/.pomerium/impersonate", p.Impersonate) // POST
|
||||
mux.HandleFunc("/.pomerium/sign_out", p.SignOut)
|
||||
// handlers handlers with validation
|
||||
mux.Handle("/.pomerium/callback", validate.ThenFunc(p.OAuthCallback))
|
||||
mux.Handle("/.pomerium/refresh", validate.ThenFunc(p.Refresh))
|
||||
// handlers with validation
|
||||
mux.Handle("/.pomerium/callback", validate.ThenFunc(p.AuthenticateCallback))
|
||||
mux.Handle("/.pomerium/refresh", validate.ThenFunc(p.ForceRefresh))
|
||||
mux.Handle("/", validate.ThenFunc(p.Proxy))
|
||||
return mux
|
||||
}
|
||||
|
@ -60,12 +61,12 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
uri, err := url.Parse(r.Form.Get("redirect_uri"))
|
||||
uri, err := urlutil.ParseAndValidateURL(r.Form.Get("redirect_uri"))
|
||||
if err == nil && uri.String() != "" {
|
||||
redirectURL = uri
|
||||
}
|
||||
default:
|
||||
uri, err := url.Parse(r.URL.Query().Get("redirect_uri"))
|
||||
uri, err := urlutil.ParseAndValidateURL(r.URL.Query().Get("redirect_uri"))
|
||||
if err == nil && uri.String() != "" {
|
||||
redirectURL = uri
|
||||
}
|
||||
|
@ -76,24 +77,20 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
// OAuthStart begins the authenticate flow, encrypting the redirect url
|
||||
// in a request to the provider's sign in endpoint.
|
||||
func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// create a CSRF value used to mitigate replay attacks.
|
||||
state := &StateParameter{
|
||||
SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()),
|
||||
RedirectURI: r.URL.String(),
|
||||
}
|
||||
|
||||
// Encrypt, and save CSRF state. Will be checked on callback.
|
||||
localState, err := p.cipher.Marshal(state)
|
||||
// Encrypt CSRF + redirect_uri and store in csrf session. Validated on callback.
|
||||
csrfState, err := p.cipher.Marshal(state)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
p.csrfStore.SetCSRF(w, r, localState)
|
||||
p.csrfStore.SetCSRF(w, r, csrfState)
|
||||
|
||||
// Though the plaintext payload is identical, we re-encrypt which will
|
||||
// create a different cipher text using another nonce
|
||||
remoteState, err := p.cipher.Marshal(state)
|
||||
paramState, err := p.cipher.Marshal(state)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
|
@ -101,68 +98,55 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Sanity check. The encrypted payload of local and remote state should
|
||||
// never match as each encryption round uses a cryptographic nonce.
|
||||
//
|
||||
// todo(bdd): since this should nearly (1/(2^32*2^32)) never happen should
|
||||
// we panic as a failure most likely means the rands entropy source is failing?
|
||||
if remoteState == localState {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
httputil.ErrorResponse(w, r, httputil.Error("encrypted state should not match", http.StatusBadRequest, nil))
|
||||
return
|
||||
}
|
||||
// if paramState == csrfState {
|
||||
// httputil.ErrorResponse(w, r, httputil.Error("encrypted state should not match", http.StatusBadRequest, nil))
|
||||
// return
|
||||
// }
|
||||
|
||||
signinURL := p.GetSignInURL(p.authenticateURL, p.GetRedirectURL(r.Host), remoteState)
|
||||
log.FromRequest(r).Debug().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start")
|
||||
signinURL := p.GetSignInURL(p.authenticateURL, p.GetRedirectURL(r.Host), paramState)
|
||||
|
||||
// Redirect the user to the authenticate service along with the encrypted
|
||||
// state which contains a redirect uri back to the proxy and a nonce
|
||||
http.Redirect(w, r, signinURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// OAuthCallback validates the cookie sent back from the authenticate service. This function will
|
||||
// contain an error, or it will contain a `code`; the code can be used to fetch an access token, and
|
||||
// other metadata, from the authenticator.
|
||||
// finish the oauth cycle
|
||||
func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// AuthenticateCallback checks the state parameter to make sure it matches the
|
||||
// local csrf state then redirects the user back to the original intended route.
|
||||
func (p *Proxy) AuthenticateCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if callbackError := r.Form.Get("error"); callbackError != "" {
|
||||
httputil.ErrorResponse(w, r, httputil.Error(callbackError, http.StatusBadRequest, nil))
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypted CSRF passed from authenticate service
|
||||
remoteStateEncrypted := r.Form.Get("state")
|
||||
remoteStatePlain := new(StateParameter)
|
||||
if err := p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain); err != nil {
|
||||
var remoteStatePlain StateParameter
|
||||
if err := p.cipher.Unmarshal(remoteStateEncrypted, &remoteStatePlain); err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypted CSRF from session storage
|
||||
c, err := p.csrfStore.GetCSRF(r)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
p.csrfStore.ClearCSRF(w, r)
|
||||
|
||||
localStateEncrypted := c.Value
|
||||
localStatePlain := new(StateParameter)
|
||||
err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain)
|
||||
var localStatePlain StateParameter
|
||||
err = p.cipher.Unmarshal(localStateEncrypted, &localStatePlain)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If the encrypted value of local and remote state match, reject.
|
||||
// Likely a replay attack or nonce-reuse.
|
||||
// assert no nonce reuse
|
||||
if remoteStateEncrypted == localStateEncrypted {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
|
||||
httputil.ErrorResponse(w, r, httputil.Error("local and remote state should not match!", http.StatusBadRequest, nil))
|
||||
|
||||
httputil.ErrorResponse(w, r,
|
||||
httputil.Error("local and remote state", http.StatusBadRequest,
|
||||
fmt.Errorf("possible nonce-reuse / replay attack")))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -205,13 +189,23 @@ func isCORSPreflight(r *http.Request) bool {
|
|||
r.Header.Get("Origin") != ""
|
||||
}
|
||||
|
||||
func (p *Proxy) loadExistingSession(r *http.Request) (*sessions.State, error) {
|
||||
s, err := p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy: invalid session: %w", err)
|
||||
}
|
||||
if err := s.Valid(); err != nil {
|
||||
return nil, fmt.Errorf("proxy: invalid state: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Proxy authenticates a request, either proxying the request if it is authenticated,
|
||||
// or starting the authenticate service for validation if not.
|
||||
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
||||
// does a route exist for this request?
|
||||
route, ok := p.router(r)
|
||||
if !ok {
|
||||
httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not a managed route.", r.Host), http.StatusNotFound, nil))
|
||||
httputil.ErrorResponse(w, r, httputil.Error("", http.StatusNotFound, nil))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -221,30 +215,17 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
s, err := p.restStore.LoadSession(r)
|
||||
// if authorization bearer token does not exist or fails, use cookie store
|
||||
if err != nil || s == nil {
|
||||
s, err = p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: invalid session, re-authenticating")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
p.OAuthStart(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = p.authenticate(w, r, s); err != nil {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
httputil.ErrorResponse(w, r, httputil.Error("User unauthenticated", http.StatusUnauthorized, err))
|
||||
s, err := p.loadExistingSession(r)
|
||||
if err != nil {
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: bad authN session, redirecting")
|
||||
p.OAuthStart(w, r)
|
||||
return
|
||||
}
|
||||
authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, s)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !authorized {
|
||||
} else if !authorized {
|
||||
httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not authorized for this route", s.Email), http.StatusForbidden, nil))
|
||||
return
|
||||
}
|
||||
|
@ -259,20 +240,13 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
// It also contains certain administrative actions like user impersonation.
|
||||
// Nota bene: This endpoint does authentication, not authorization.
|
||||
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
session, err := p.loadExistingSession(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Debug().Str("cause", err.Error()).Msg("proxy: no session, redirecting to auth")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: bad authN session, redirecting")
|
||||
p.OAuthStart(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.authenticate(w, r, session); err != nil {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
httputil.ErrorResponse(w, r, httputil.Error("User unauthenticated", http.StatusUnauthorized, err))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"}
|
||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||
if err != nil {
|
||||
|
@ -314,13 +288,14 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
templates.New().ExecuteTemplate(w, "dashboard.html", t)
|
||||
}
|
||||
|
||||
// Refresh redeems and extends an existing authenticated oidc session with
|
||||
// ForceRefresh redeems and extends an existing authenticated oidc session with
|
||||
// the underlying identity provider. All session details including groups,
|
||||
// timeouts, will be renewed.
|
||||
func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
func (p *Proxy) ForceRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := p.loadExistingSession(r)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: bad authN session, redirecting")
|
||||
p.OAuthStart(w, r)
|
||||
return
|
||||
}
|
||||
iss, err := session.IssuedAt()
|
||||
|
@ -332,16 +307,13 @@ func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
|
|||
// reject a refresh if it's been less than the refresh cooldown to prevent abuse
|
||||
if time.Since(iss) < p.refreshCooldown {
|
||||
httputil.ErrorResponse(w, r,
|
||||
httputil.Error(fmt.Sprintf("Session must be %s old before refreshing", p.refreshCooldown), http.StatusBadRequest, nil))
|
||||
httputil.Error(
|
||||
fmt.Sprintf("Session must be %s old before refreshing", p.refreshCooldown),
|
||||
http.StatusBadRequest, nil))
|
||||
return
|
||||
}
|
||||
|
||||
newSession, err := p.AuthenticateClient.Refresh(r.Context(), session)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = p.sessionStore.SaveSession(w, r, newSession); err != nil {
|
||||
session.ForceRefresh()
|
||||
if err = p.sessionStore.SaveSession(w, r, session); err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
@ -357,12 +329,12 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
session, err := p.loadExistingSession(r)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
log.Debug().Str("cause", err.Error()).Msg("proxy: bad authN session, redirecting")
|
||||
p.OAuthStart(w, r)
|
||||
return
|
||||
}
|
||||
// authorization check -- is this user an admin?
|
||||
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
|
||||
if err != nil || !isAdmin {
|
||||
httputil.ErrorResponse(w, r, httputil.Error(fmt.Sprintf("%s is not an administrator", session.Email), http.StatusForbidden, err))
|
||||
|
@ -376,7 +348,7 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
p.csrfStore.ClearCSRF(w, r)
|
||||
encryptedCSRF := c.Value
|
||||
decryptedCSRF := new(StateParameter)
|
||||
var decryptedCSRF StateParameter
|
||||
if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil {
|
||||
httputil.ErrorResponse(w, r, err)
|
||||
return
|
||||
|
@ -398,26 +370,6 @@ func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/.pomerium", http.StatusFound)
|
||||
}
|
||||
|
||||
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration,
|
||||
// clearing the session cookie if it's invalid and returning an error if necessary..
|
||||
func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, s *sessions.SessionState) error {
|
||||
if s.RefreshPeriodExpired() {
|
||||
s, err := p.AuthenticateClient.Refresh(r.Context(), s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxy: session refresh failed : %v", err)
|
||||
}
|
||||
if err := p.sessionStore.SaveSession(w, r, s); err != nil {
|
||||
return fmt.Errorf("proxy: refresh failed : %v", err)
|
||||
}
|
||||
} else {
|
||||
valid, err := p.AuthenticateClient.Validate(r.Context(), s.IDToken)
|
||||
if err != nil || !valid {
|
||||
return fmt.Errorf("proxy: session validate failed: %v : %v", valid, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// router attempts to find a route for a request. If a route is successfully matched,
|
||||
// it returns the route information and a bool value of `true`. If a route can
|
||||
// not be matched, a nil value for the route and false bool value is returned.
|
||||
|
@ -461,7 +413,7 @@ func (p *Proxy) GetSignInURL(authenticateURL, redirectURL *url.URL, state string
|
|||
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_in"})
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params, _ := url.ParseQuery(a.RawQuery) // handled by ServeMux
|
||||
params.Set("redirect_uri", rawRedirect)
|
||||
params.Set("shared_secret", p.SharedKey)
|
||||
params.Set("response_type", "code")
|
||||
|
@ -477,7 +429,7 @@ func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL {
|
|||
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_out"})
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params, _ := url.ParseQuery(a.RawQuery) // handled by ServeMux
|
||||
params.Add("redirect_uri", rawRedirect)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
params.Set("sig", p.signRedirectURL(rawRedirect, now))
|
||||
|
|
|
@ -72,7 +72,6 @@ func TestProxy_GetRedirectURL(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Proxy{redirectURL: &url.URL{Path: "/.pomerium/callback"}}
|
||||
|
||||
if got := p.GetRedirectURL(tt.host); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Proxy.GetRedirectURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
@ -240,8 +239,7 @@ func TestProxy_router(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.AuthenticateClient = clients.MockAuthenticate{}
|
||||
p.cipher = mockCipher{}
|
||||
p.cipher = &cryptutil.MockCipher{MarshalResponse: "foo"}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, tt.host, nil)
|
||||
_, ok := p.router(req)
|
||||
|
@ -253,7 +251,7 @@ func TestProxy_router(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProxy_Proxy(t *testing.T) {
|
||||
goodSession := &sessions.SessionState{
|
||||
goodSession := &sessions.State{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
RefreshDeadline: time.Now().Add(10 * time.Second),
|
||||
|
@ -278,39 +276,34 @@ func TestProxy_Proxy(t *testing.T) {
|
|||
headersWs.Set("Upgrade", "websocket")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
header http.Header
|
||||
host string
|
||||
session sessions.SessionStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
header http.Header
|
||||
host string
|
||||
session sessions.SessionStore
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
|
||||
{"good email impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateEmail: "test@user.example"}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
{"good group impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateGroups: []string{"group1", "group2"}}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
{"good", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
|
||||
{"good email impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateEmail: "test@user.example"}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
{"good group impersonation", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second), ImpersonateGroups: []string{"group1", "group2"}}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
|
||||
// same request as above, but with cors_allow_preflight=false in the policy
|
||||
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
// cors allowed, but the request is missing proper headers
|
||||
{"invalid cors headers", optsCORS, http.MethodOptions, badCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
{"unexpected error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
|
||||
{"invalid cors headers", optsCORS, http.MethodOptions, badCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
// redirect to start auth process
|
||||
{"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
|
||||
{"user not authorized", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
{"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError},
|
||||
{"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
|
||||
{"user not authorized", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||
{"authorization call failed", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeError: errors.New("error")}, http.StatusInternalServerError},
|
||||
// authenticate errors
|
||||
{"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
|
||||
{"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized},
|
||||
{"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized},
|
||||
{"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized},
|
||||
{"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
|
||||
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusNotFound},
|
||||
// no session, redirect to login
|
||||
{"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
|
||||
{"No policies", optsNoPolicies, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
|
||||
{"session error, redirect to authn", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
|
||||
{"session expired,redirect to authn", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: sessions.ErrExpired}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
|
||||
{"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
|
||||
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusNotFound},
|
||||
{"no http found (no session),redirect to authn", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
|
||||
{"No policies", optsNoPolicies, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -323,13 +316,13 @@ func TestProxy_Proxy(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.cipher = mockCipher{}
|
||||
p.cipher = &cryptutil.MockCipher{MarshalResponse: "foo"}
|
||||
p.sessionStore = tt.session
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
|
||||
r := httptest.NewRequest(tt.method, tt.host, nil)
|
||||
r.Header = tt.header
|
||||
r.Header.Set("Accept", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
p.Proxy(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
|
@ -348,23 +341,21 @@ func TestProxy_Proxy(t *testing.T) {
|
|||
func TestProxy_UserDashboard(t *testing.T) {
|
||||
opts := testOptions(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authorizer clients.Authorizer
|
||||
|
||||
wantAdminForm bool
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusOK},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusBadRequest},
|
||||
{"auth failure, validation error", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthenticate{ValidateError: errors.New("not valid anymore"), ValidateResponse: false}, clients.MockAuthorize{}, false, http.StatusUnauthorized},
|
||||
{"can't save csrf", opts, http.MethodGet, &cryptutil.MockCipher{MarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusInternalServerError},
|
||||
{"want admin form good admin authorization", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK},
|
||||
{"is admin but authorization fails", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError},
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{}, false, http.StatusOK},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthorize{}, false, http.StatusFound},
|
||||
{"can't save csrf", opts, http.MethodGet, &cryptutil.MockCipher{MarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example"}}, clients.MockAuthorize{}, false, http.StatusInternalServerError},
|
||||
{"want admin form good admin authorization", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK},
|
||||
{"is admin but authorization fails", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -375,15 +366,18 @@ func TestProxy_UserDashboard(t *testing.T) {
|
|||
}
|
||||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.session
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
|
||||
r := httptest.NewRequest(tt.method, "/", nil)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
p.UserDashboard(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
|
||||
t.Errorf("\n%+v", opts)
|
||||
t.Errorf("\n%+v", w.Body.String())
|
||||
|
||||
}
|
||||
if adminForm := strings.Contains(w.Body.String(), "impersonate"); adminForm != tt.wantAdminForm {
|
||||
t.Errorf("wanted admin form got %v want %v", adminForm, tt.wantAdminForm)
|
||||
|
@ -393,28 +387,27 @@ func TestProxy_UserDashboard(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProxy_Refresh(t *testing.T) {
|
||||
func TestProxy_ForceRefresh(t *testing.T) {
|
||||
opts := testOptions(t)
|
||||
opts.RefreshCooldown = 0
|
||||
timeSinceError := testOptions(t)
|
||||
timeSinceError.RefreshCooldown = time.Duration(int(^uint(0) >> 1))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
name string
|
||||
options config.Options
|
||||
method string
|
||||
cipher cryptutil.Cipher
|
||||
session sessions.SessionStore
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusFound},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"bad id token", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "bad"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"issue date too soon", timeSinceError, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest},
|
||||
{"refresh failure", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{RefreshError: errors.New("err")}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"can't save refreshed session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthorize{}, http.StatusFound},
|
||||
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthorize{}, http.StatusFound},
|
||||
{"bad id token", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: "bad"}}, clients.MockAuthorize{}, http.StatusInternalServerError},
|
||||
{"issue date too soon", timeSinceError, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthorize{}, http.StatusBadRequest},
|
||||
{"refresh failure", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthorize{}, http.StatusFound},
|
||||
{"can't save refreshed session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthorize{}, http.StatusFound},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -424,12 +417,11 @@ func TestProxy_Refresh(t *testing.T) {
|
|||
}
|
||||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.session
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
|
||||
r := httptest.NewRequest(tt.method, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
p.Refresh(w, r)
|
||||
p.ForceRefresh(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
|
||||
t.Errorf("\n%+v", opts)
|
||||
|
@ -442,30 +434,29 @@ func TestProxy_Impersonate(t *testing.T) {
|
|||
opts := testOptions(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
malformed bool
|
||||
options config.Options
|
||||
method string
|
||||
email string
|
||||
groups string
|
||||
csrf string
|
||||
cipher cryptutil.Cipher
|
||||
sessionStore sessions.SessionStore
|
||||
csrfStore sessions.CSRFStore
|
||||
authenticator clients.Authenticator
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
name string
|
||||
malformed bool
|
||||
options config.Options
|
||||
method string
|
||||
email string
|
||||
groups string
|
||||
csrf string
|
||||
cipher cryptutil.Cipher
|
||||
sessionStore sessions.SessionStore
|
||||
csrfStore sessions.CSRFStore
|
||||
authorizer clients.Authorizer
|
||||
wantStatus int
|
||||
}{
|
||||
{"good", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
|
||||
{"session load error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"non admin users rejected", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden},
|
||||
{"non admin users rejected on error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusForbidden},
|
||||
{"csrf from store retrieve failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}, GetError: errors.New("err")}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"can't decrypt csrf value", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"decrypted csrf mismatch", false, opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest},
|
||||
{"save session failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"malformed", true, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"groups", false, opts, http.MethodPost, "user@blah.com", "group1,group2", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
|
||||
{"good", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
|
||||
{"session load error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
|
||||
// {"non admin users rejected", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden},
|
||||
{"non admin users rejected on error", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusForbidden},
|
||||
{"csrf from store retrieve failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}, GetError: errors.New("err")}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"can't decrypt csrf value", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"decrypted csrf mismatch", false, opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest},
|
||||
{"save session failure", false, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"malformed", true, opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
|
||||
{"groups", false, opts, http.MethodPost, "user@blah.com", "group1,group2", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.State{RefreshDeadline: time.Now().Add(10 * time.Second), Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -476,7 +467,6 @@ func TestProxy_Impersonate(t *testing.T) {
|
|||
p.cipher = tt.cipher
|
||||
p.sessionStore = tt.sessionStore
|
||||
p.csrfStore = tt.csrfStore
|
||||
p.AuthenticateClient = tt.authenticator
|
||||
p.AuthorizeClient = tt.authorizer
|
||||
postForm := url.Values{}
|
||||
postForm.Add("email", tt.email)
|
||||
|
@ -501,19 +491,17 @@ func TestProxy_Impersonate(t *testing.T) {
|
|||
|
||||
func TestProxy_OAuthCallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
csrf sessions.MockCSRFStore
|
||||
session sessions.MockSessionStore
|
||||
authenticator clients.MockAuthenticate
|
||||
params map[string]string
|
||||
wantCode int
|
||||
name string
|
||||
csrf sessions.MockCSRFStore
|
||||
session sessions.MockSessionStore
|
||||
params map[string]string
|
||||
wantCode int
|
||||
}{
|
||||
{"good", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
|
||||
{"error", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"error": "some error"}, http.StatusBadRequest},
|
||||
{"state err", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
|
||||
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"unmarshal err", sessions.MockCSRFStore{Cookie: &http.Cookie{Name: "something_csrf", Value: "unmarshal error"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"malformed", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"good", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
|
||||
{"state err", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
|
||||
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"unmarshal err", sessions.MockCSRFStore{Cookie: &http.Cookie{Name: "something_csrf", Value: "unmarshal error"}}, sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
{"malformed", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.State{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -524,7 +512,6 @@ func TestProxy_OAuthCallback(t *testing.T) {
|
|||
}
|
||||
proxy.sessionStore = &tt.session
|
||||
proxy.csrfStore = tt.csrf
|
||||
proxy.AuthenticateClient = tt.authenticator
|
||||
proxy.cipher = mockCipher{}
|
||||
// proxy.Csrf
|
||||
req := httptest.NewRequest(http.MethodPost, "/.pomerium/callback", nil)
|
||||
|
@ -537,7 +524,7 @@ func TestProxy_OAuthCallback(t *testing.T) {
|
|||
req.URL.RawQuery = "email=%zzzzz"
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
proxy.OAuthCallback(w, req)
|
||||
proxy.AuthenticateCallback(w, req)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
|
|
111
proxy/proxy.go
111
proxy/proxy.go
|
@ -2,11 +2,9 @@ package proxy // import "github.com/pomerium/pomerium/proxy"
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
@ -39,51 +37,27 @@ const (
|
|||
// ValidateOptions checks that proper configuration settings are set to create
|
||||
// a proper Proxy instance
|
||||
func ValidateOptions(o config.Options) error {
|
||||
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("`SHARED_SECRET` setting is invalid base64: %v", err)
|
||||
if _, err := cryptutil.NewCipherFromBase64(o.SharedKey); err != nil {
|
||||
return fmt.Errorf("proxy: invalid 'SHARED_SECRET': %v", err)
|
||||
}
|
||||
if len(decoded) != 32 {
|
||||
return fmt.Errorf("`SHARED_SECRET` want 32 but got %d bytes", len(decoded))
|
||||
if _, err := cryptutil.NewCipherFromBase64(o.CookieSecret); err != nil {
|
||||
return fmt.Errorf("proxy: invalid 'COOKIE_SECRET': %v", err)
|
||||
}
|
||||
|
||||
if o.AuthenticateURL == nil {
|
||||
return fmt.Errorf("proxy: missing setting: authenticate-service-url")
|
||||
return fmt.Errorf("proxy: missing 'AUTHENTICATE_SERVICE_URL'")
|
||||
}
|
||||
if _, err := urlutil.ParseAndValidateURL(o.AuthenticateURL.String()); err != nil {
|
||||
return fmt.Errorf("proxy: error parsing authenticate url: %v", err)
|
||||
return fmt.Errorf("proxy: invalid 'AUTHENTICATE_SERVICE_URL': %v", err)
|
||||
}
|
||||
|
||||
if o.AuthorizeURL == nil {
|
||||
return fmt.Errorf("proxy: missing setting: authenticate-service-url")
|
||||
return fmt.Errorf("proxy: missing 'AUTHORIZE_SERVICE_URL'")
|
||||
}
|
||||
if _, err := urlutil.ParseAndValidateURL(o.AuthorizeURL.String()); err != nil {
|
||||
return fmt.Errorf("proxy: error parsing authorize url: %v", err)
|
||||
}
|
||||
if o.AuthenticateInternalAddr != nil {
|
||||
if _, err := urlutil.ParseAndValidateURL(o.AuthenticateInternalAddr.String()); err != nil {
|
||||
return fmt.Errorf("proxy: error parsing authorize url: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if o.CookieSecret == "" {
|
||||
return fmt.Errorf("proxy: missing setting: cookie-secret")
|
||||
}
|
||||
decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxy: cookie secret is invalid base64: %v", err)
|
||||
}
|
||||
if len(decodedCookieSecret) != 32 {
|
||||
return fmt.Errorf("proxy: cookie secret expects 32 bytes but got %d", len(decodedCookieSecret))
|
||||
return fmt.Errorf("proxy: invalid 'AUTHORIZE_SERVICE_URL': %v", err)
|
||||
}
|
||||
if len(o.SigningKey) != 0 {
|
||||
decodedSigningKey, err := base64.StdEncoding.DecodeString(o.SigningKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxy: signing key is invalid base64: %v", err)
|
||||
}
|
||||
_, err = cryptutil.NewES256Signer(decodedSigningKey, "localhost")
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxy: invalid signing key is : %v", err)
|
||||
if _, err := cryptutil.NewES256Signer(o.SigningKey, "localhost"); err != nil {
|
||||
return fmt.Errorf("proxy: invalid 'SIGNING_KEY': %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -92,12 +66,11 @@ func ValidateOptions(o config.Options) error {
|
|||
// Proxy stores all the information associated with proxying a request.
|
||||
type Proxy struct {
|
||||
// SharedKey used to mutually authenticate service communication
|
||||
SharedKey string
|
||||
authenticateURL *url.URL
|
||||
authenticateInternalAddr *url.URL
|
||||
authorizeURL *url.URL
|
||||
AuthenticateClient clients.Authenticator
|
||||
AuthorizeClient clients.Authorizer
|
||||
SharedKey string
|
||||
authenticateURL *url.URL
|
||||
authorizeURL *url.URL
|
||||
|
||||
AuthorizeClient clients.Authorizer
|
||||
|
||||
cipher cryptutil.Cipher
|
||||
cookieName string
|
||||
|
@ -105,7 +78,6 @@ type Proxy struct {
|
|||
defaultUpstreamTimeout time.Duration
|
||||
redirectURL *url.URL
|
||||
refreshCooldown time.Duration
|
||||
restStore sessions.SessionStore
|
||||
routeConfigs map[string]*routeConfig
|
||||
sessionStore sessions.SessionStore
|
||||
signingKey string
|
||||
|
@ -123,11 +95,9 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
if err := ValidateOptions(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// error explicitly handled by validate
|
||||
decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
cipher, err := cryptutil.NewCipher(decodedSecret)
|
||||
cipher, err := cryptutil.NewCipherFromBase64(opts.CookieSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cookie-secret error: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookieStore, err := sessions.NewCookieStore(
|
||||
|
@ -140,10 +110,6 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
CookieCipher: cipher,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
restStore, err := sessions.NewRestStore(&sessions.RestStoreOptions{Cipher: cipher})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -158,7 +124,6 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
defaultUpstreamTimeout: opts.DefaultUpstreamTimeout,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
refreshCooldown: opts.RefreshCooldown,
|
||||
restStore: restStore,
|
||||
sessionStore: cookieStore,
|
||||
signingKey: opts.SigningKey,
|
||||
templates: templates.New(),
|
||||
|
@ -166,7 +131,6 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
// DeepCopy urls to avoid accidental mutation, err checked in validate func
|
||||
p.authenticateURL, _ = urlutil.DeepCopy(opts.AuthenticateURL)
|
||||
p.authorizeURL, _ = urlutil.DeepCopy(opts.AuthorizeURL)
|
||||
p.authenticateInternalAddr, _ = urlutil.DeepCopy(opts.AuthenticateInternalAddr)
|
||||
|
||||
if err := p.UpdatePolicies(&opts); err != nil {
|
||||
return nil, err
|
||||
|
@ -174,20 +138,6 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
metrics.AddPolicyCountCallback("proxy", func() int64 {
|
||||
return int64(len(p.routeConfigs))
|
||||
})
|
||||
p.AuthenticateClient, err = clients.NewAuthenticateClient("grpc",
|
||||
&clients.Options{
|
||||
Addr: p.authenticateURL,
|
||||
InternalAddr: p.authenticateInternalAddr,
|
||||
OverrideCertificateName: opts.OverrideCertificateName,
|
||||
SharedSecret: opts.SharedKey,
|
||||
CA: opts.CA,
|
||||
CAFile: opts.CAFile,
|
||||
RequestTimeout: opts.GRPCClientTimeout,
|
||||
ClientDNSRoundRobin: opts.GRPCClientDNSRoundRobin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc",
|
||||
&clients.Options{
|
||||
Addr: p.authorizeURL,
|
||||
|
@ -213,19 +163,7 @@ func (p *Proxy) UpdatePolicies(opts *config.Options) error {
|
|||
}
|
||||
proxy := NewReverseProxy(policy.Destination)
|
||||
// build http transport (roundtripper) middleware chain
|
||||
// todo(bdd): replace with transport.Clone() in go 1.13
|
||||
transport := http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
c := tripper.NewChain()
|
||||
c = c.Append(metrics.HTTPMetricsRoundTripper("proxy", policy.Destination.Host))
|
||||
|
||||
|
@ -253,7 +191,7 @@ func (p *Proxy) UpdatePolicies(opts *config.Options) error {
|
|||
if isCustomClientConfig {
|
||||
transport.TLSClientConfig = &tlsClientConfig
|
||||
}
|
||||
proxy.Transport = c.Then(&transport)
|
||||
proxy.Transport = c.Then(transport)
|
||||
|
||||
handler, err := p.newReverseProxyHandler(proxy, &policy)
|
||||
if err != nil {
|
||||
|
@ -298,15 +236,6 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
|
|||
return proxy
|
||||
}
|
||||
|
||||
// newRouteSigner creates a route specific signer.
|
||||
func (p *Proxy) newRouteSigner(audience string) (cryptutil.JWTSigner, error) {
|
||||
decodedSigningKey, err := base64.StdEncoding.DecodeString(p.signingKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cryptutil.NewES256Signer(decodedSigningKey, audience)
|
||||
}
|
||||
|
||||
// newReverseProxyHandler applies handler specific options to a given route.
|
||||
func (p *Proxy) newReverseProxyHandler(rp *httputil.ReverseProxy, route *config.Policy) (handler http.Handler, err error) {
|
||||
handler = &UpstreamProxy{
|
||||
|
@ -318,7 +247,7 @@ func (p *Proxy) newReverseProxyHandler(rp *httputil.ReverseProxy, route *config.
|
|||
|
||||
// if signing key is set, add signer to middleware
|
||||
if len(p.signingKey) != 0 {
|
||||
signer, err := p.newRouteSigner(route.Source.Host)
|
||||
signer, err := cryptutil.NewES256Signer(p.signingKey, route.Source.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -169,9 +169,6 @@ func TestOptions_Validate(t *testing.T) {
|
|||
authurl, _ := url.Parse("authenticate.corp.beyondperimeter.com")
|
||||
authenticateBadScheme := testOptions(t)
|
||||
authenticateBadScheme.AuthenticateURL = authurl
|
||||
authenticateInternalBadScheme := testOptions(t)
|
||||
authenticateInternalBadScheme.AuthenticateInternalAddr = authurl
|
||||
|
||||
authorizeBadSCheme := testOptions(t)
|
||||
authorizeBadSCheme.AuthorizeURL = authurl
|
||||
authorizeNil := testOptions(t)
|
||||
|
@ -200,7 +197,6 @@ func TestOptions_Validate(t *testing.T) {
|
|||
{"nil options", config.Options{}, true},
|
||||
{"authenticate service url", badAuthURL, true},
|
||||
{"authenticate service url no scheme", authenticateBadScheme, true},
|
||||
{"internal authenticate service url no scheme", authenticateInternalBadScheme, true},
|
||||
{"authorize service url no scheme", authorizeBadSCheme, true},
|
||||
{"authorize service cannot be nil", authorizeNil, true},
|
||||
{"no cookie secret", emptyCookieSecret, true},
|
||||
|
@ -221,7 +217,6 @@ func TestOptions_Validate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
|
||||
good := testOptions(t)
|
||||
shortCookieLength := testOptions(t)
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue