From a969f33d88c6f612c5f59202b471ac70ead8ef0e Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Fri, 22 May 2020 13:25:59 -0600 Subject: [PATCH] authorize: refactor and add additional unit tests (#757) * authorize: clean up code, add test * authorize: additional test * authorize: additional test --- authorize/{errors.go => check_response.go} | 46 +++- authorize/evaluator/evaluator.go | 4 - authorize/grpc.go | 271 +++++++++------------ authorize/grpc_test.go | 150 ++++++++++++ 4 files changed, 314 insertions(+), 157 deletions(-) rename authorize/{errors.go => check_response.go} (66%) create mode 100644 authorize/grpc_test.go diff --git a/authorize/errors.go b/authorize/check_response.go similarity index 66% rename from authorize/errors.go rename to authorize/check_response.go index 61a55d0fa..f349468ab 100644 --- a/authorize/errors.go +++ b/authorize/check_response.go @@ -3,6 +3,7 @@ package authorize import ( "bytes" "net/http" + "net/url" "strings" envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" @@ -11,12 +12,39 @@ import ( "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" + "github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" + "github.com/pomerium/pomerium/internal/urlutil" ) -func (a *Authorize) deniedResponse(in *envoy_service_auth_v2.CheckRequest, - code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse { +func (a *Authorize) okResponse( + reply *authorize.IsAuthorizedReply, + rawSession []byte, + isNewSession bool, +) *envoy_service_auth_v2.CheckResponse { + + requestHeaders, err := a.getEnvoyRequestHeaders(rawSession, isNewSession) + if err != nil { + log.Warn().Err(err).Msg("authorize: error generating new request headers") + } + requestHeaders = append(requestHeaders, + mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJwt)) + + return &envoy_service_auth_v2.CheckResponse{ + Status: &status.Status{Code: int32(codes.OK), Message: "OK"}, + HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{ + OkResponse: &envoy_service_auth_v2.OkHttpResponse{ + Headers: requestHeaders, + }, + }, + } +} + +func (a *Authorize) deniedResponse( + in *envoy_service_auth_v2.CheckRequest, + code int32, reason string, headers map[string]string, +) *envoy_service_auth_v2.CheckResponse { returnHTMLError := true inHeaders := in.GetAttributes().GetRequest().GetHttp().GetHeaders() @@ -96,6 +124,20 @@ func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers m } } +func (a *Authorize) redirectResponse(in *envoy_service_auth_v2.CheckRequest) *envoy_service_auth_v2.CheckResponse { + opts := a.currentOptions.Load() + + signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"}) + q := signinURL.Query() + q.Set(urlutil.QueryRedirectURI, getCheckRequestURL(in).String()) + signinURL.RawQuery = q.Encode() + redirectTo := urlutil.NewSignedURL(opts.SharedKey, signinURL).String() + + return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{ + "Location": redirectTo, + }) +} + func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption { return &envoy_api_v2_core.HeaderValueOption{ Header: &envoy_api_v2_core.HeaderValue{ diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index acfb5c153..afeebbe42 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -29,15 +29,11 @@ type Request struct { Method string `json:"method,omitempty"` // URL specifies either the URI being requested. URL string `json:"url,omitempty"` - // The protocol version for incoming server requests. - Proto string `json:"proto,omitempty"` // "HTTP/1.0" // Header contains the request header fields either received // by the server or to be sent by the client. Header map[string][]string `json:"headers,omitempty"` // Host specifies the host on which the URL is sought. Host string `json:"host,omitempty"` - // RemoteAddr is the network address that sent the request. - RemoteAddr string `json:"remote_addr,omitempty"` // RequestURI is the unmodified request-target of the // Request-Line (RFC 7230, Section 3.1.1) as sent by the client // to a server. Usually the URL field should be used instead. diff --git a/authorize/grpc.go b/authorize/grpc.go index 28defd522..61b0d6508 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -2,6 +2,7 @@ package authorize import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -10,7 +11,7 @@ import ( "strings" "github.com/pomerium/pomerium/authorize/evaluator" - "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/sessions" @@ -20,8 +21,6 @@ import ( envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2" - "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc/codes" ) // Check implements the envoy auth server gRPC endpoint. @@ -29,133 +28,71 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check") defer span.End() - opts := a.currentOptions.Load() - // maybe rewrite http request for forward auth - isForwardAuth := handleForwardAuth(opts, in) - - hattrs := in.GetAttributes().GetRequest().GetHttp() + isForwardAuth := a.handleForwardAuth(in) hreq := getHTTPRequestFromCheckRequest(in) - hdrs := getCheckRequestHeaders(in) - isNewSession := false - sess, sesserr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load()) - if a.isExpired(sess) { + rawJWT, sessionErr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load()) + if a.isExpired(rawJWT) { log.Info().Msg("refreshing session") - if newSession, err := a.refreshSession(ctx, sess); err == nil { - sess = newSession - sesserr = nil + if newRawJWT, err := a.refreshSession(ctx, rawJWT); err == nil { + rawJWT = newRawJWT + sessionErr = nil isNewSession = true } else { log.Warn().Err(err).Msg("authorize: error refreshing session") + // set the error to expired so that we can force a new login + sessionErr = sessions.ErrExpired } } - requestHeaders, err := a.getEnvoyRequestHeaders(sess, isNewSession) - if err != nil { - log.Warn().Err(err).Msg("authorize: error generating new request headers") - } - - requestURL := getCheckRequestURL(in) - req := &evaluator.Request{ - User: string(sess), - Header: hdrs, - Host: hattrs.GetHost(), - Method: hattrs.GetMethod(), - RequestURI: requestURL.String(), - RemoteAddr: in.GetAttributes().GetSource().GetAddress().String(), - URL: requestURL.String(), - ClientCertificate: getPeerCertificate(in), - } + req := getEvaluatorRequestFromCheckRequest(in, rawJWT) reply, err := a.pe.IsAuthorized(ctx, req) if err != nil { return nil, err } + logAuthorizeCheck(ctx, in, reply, rawJWT) - evt := log.Info().Str("service", "authorize") - // request - evt = evt.Str("request-id", requestid.FromContext(ctx)) - evt = evt.Strs("check-request-id", hdrs["X-Request-Id"]) - evt = evt.Str("method", hattrs.GetMethod()) - evt = evt.Interface("headers", hdrs) - evt = evt.Str("path", hattrs.GetPath()) - evt = evt.Str("host", hattrs.GetHost()) - evt = evt.Str("query", hattrs.GetQuery()) - // reply - evt = evt.Bool("allow", reply.GetAllow()) - evt = evt.Bool("session-expired", reply.GetSessionExpired()) - evt = evt.Strs("deny-reasons", reply.GetDenyReasons()) - evt = evt.Str("email", reply.GetEmail()) - evt = evt.Strs("groups", reply.GetGroups()) - if sess != nil { - evt = evt.Str("session", string(sess)) - } - if reply.GetHttpStatus() != nil { - evt = evt.Interface("http_status", reply.GetHttpStatus()) - } - evt.Msg("authorize check") - - requestHeaders = append(requestHeaders, - &envoy_api_v2_core.HeaderValueOption{ - Header: &envoy_api_v2_core.HeaderValue{ - Key: "x-pomerium-jwt-assertion", - Value: reply.SignedJwt, - }, - }) - - if reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK { + switch { + case reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK: + // custom error from the IsAuthorized call return a.deniedResponse(in, reply.GetHttpStatus().GetCode(), reply.GetHttpStatus().GetMessage(), reply.GetHttpStatus().GetHeaders(), ), nil - } - if reply.Allow { - return &envoy_service_auth_v2.CheckResponse{ - Status: &status.Status{Code: int32(codes.OK), Message: "OK"}, - HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{ - OkResponse: &envoy_service_auth_v2.OkHttpResponse{ - Headers: requestHeaders, - }, - }, - }, nil - } + case reply.Allow: + // ok! + return a.okResponse(reply, rawJWT, isNewSession), nil - if reply.SessionExpired { - sesserr = sessions.ErrExpired - } - - switch sesserr { - case sessions.ErrExpired, sessions.ErrIssuedInTheFuture, sessions.ErrMalformed, sessions.ErrNoSessionFound, sessions.ErrNotValidYet: + case reply.SessionExpired, + errors.Is(sessionErr, sessions.ErrExpired), + errors.Is(sessionErr, sessions.ErrIssuedInTheFuture), + errors.Is(sessionErr, sessions.ErrMalformed), + errors.Is(sessionErr, sessions.ErrNoSessionFound), + errors.Is(sessionErr, sessions.ErrNotValidYet): // redirect to login - default: - var msg string - if sesserr != nil { - msg = sesserr.Error() + + // no redirect for forward auth, that's handled by a separate config setting + if isForwardAuth { + return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil } + + return a.redirectResponse(in), nil + + default: // all other errors + var msg string + if sessionErr != nil { + msg = sessionErr.Error() + } return a.deniedResponse(in, http.StatusForbidden, msg, nil), nil } - - // no redirect for forward auth, that's handled by a separate config setting - if isForwardAuth { - return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil - } - - signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"}) - q := signinURL.Query() - q.Set(urlutil.QueryRedirectURI, requestURL.String()) - signinURL.RawQuery = q.Encode() - redirectTo := urlutil.NewSignedURL(opts.SharedKey, signinURL).String() - - return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{ - "Location": redirectTo, - }), nil } -func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([]*envoy_api_v2_core.HeaderValueOption, error) { +func (a *Authorize) getEnvoyRequestHeaders(rawJWT []byte, isNewSession bool) ([]*envoy_api_v2_core.HeaderValueOption, error) { var hvos []*envoy_api_v2_core.HeaderValueOption if isNewSession { @@ -164,44 +101,28 @@ func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([] return nil, err } - hdrs, err := getJWTSetCookieHeaders(cookieStore, rawjwt) + hdrs, err := getJWTSetCookieHeaders(cookieStore, rawJWT) if err != nil { return nil, err } for k, v := range hdrs { - hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{ - Header: &envoy_api_v2_core.HeaderValue{ - Key: "x-pomerium-" + k, - Value: v, - }, - }) + hvos = append(hvos, mkHeader("x-pomerium-"+k, v)) } } - hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawjwt) + hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawJWT) if err != nil { return nil, err } for k, v := range hdrs { - hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{ - Header: &envoy_api_v2_core.HeaderValue{ - Key: k, - Value: v, - }, - }) + hvos = append(hvos, mkHeader(k, v)) } return hvos, nil } -func (a *Authorize) refreshSession(ctx context.Context, rawSession []byte) (newSession []byte, err error) { +func (a *Authorize) refreshSession(ctx context.Context, rawJWT []byte) (newSession []byte, err error) { options := a.currentOptions.Load() - encoder := a.currentEncoder.Load() - - var state sessions.State - if err := encoder.Unmarshal(rawSession, &state); err != nil { - return nil, fmt.Errorf("error unmarshaling raw session: %w", err) - } // 1 - build a signed url to call refresh on authenticate service refreshURI := options.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/refresh"}) @@ -212,7 +133,7 @@ func (a *Authorize) refreshSession(ctx context.Context, rawSession []byte) (newS if err != nil { return nil, fmt.Errorf("authorize: refresh request: %w", err) } - req.Header.Set("Authorization", fmt.Sprintf("Pomerium %s", rawSession)) + req.Header.Set("Authorization", fmt.Sprintf("Pomerium %s", rawJWT)) req.Header.Set("X-Requested-With", "XmlHttpRequest") req.Header.Set("Accept", "application/json") @@ -238,6 +159,49 @@ func (a *Authorize) isExpired(rawSession []byte) bool { return err == nil && state.IsExpired() } +func (a *Authorize) handleForwardAuth(req *envoy_service_auth_v2.CheckRequest) bool { + opts := a.currentOptions.Load() + + if opts.ForwardAuthURL == nil { + return false + } + + checkURL := getCheckRequestURL(req) + if urlutil.StripPort(checkURL.Host) == urlutil.StripPort(opts.ForwardAuthURL.Host) { + if (checkURL.Path == "/" || checkURL.Path == "/verify") && checkURL.Query().Get("uri") != "" { + verifyURL, err := url.Parse(checkURL.Query().Get("uri")) + if err != nil { + log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication") + return false + } + req.Attributes.Request.Http.Scheme = verifyURL.Scheme + req.Attributes.Request.Http.Host = verifyURL.Host + req.Attributes.Request.Http.Path = verifyURL.Path + // envoy sends the query string as part of the path + if verifyURL.RawQuery != "" { + req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery + } + return true + } + } + + return false +} + +func getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, rawJWT []byte) *evaluator.Request { + requestURL := getCheckRequestURL(in) + req := &evaluator.Request{ + User: string(rawJWT), + Header: getCheckRequestHeaders(in), + Host: in.GetAttributes().GetRequest().GetHttp().GetHost(), + Method: in.GetAttributes().GetRequest().GetHttp().GetMethod(), + RequestURI: requestURL.String(), + URL: requestURL.String(), + ClientCertificate: getPeerCertificate(in), + } + return req +} + func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) *http.Request { hattrs := req.GetAttributes().GetRequest().GetHttp() return &http.Request{ @@ -274,44 +238,49 @@ func getCheckRequestURL(req *envoy_service_auth_v2.CheckRequest) *url.URL { u.Path = path } - if h.Headers != nil { - if fwdProto, ok := h.Headers["x-forwarded-proto"]; ok { + if h.GetHeaders() != nil { + if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok { u.Scheme = fwdProto } } return u } -func handleForwardAuth(opts config.Options, req *envoy_service_auth_v2.CheckRequest) bool { - if opts.ForwardAuthURL == nil { - return false - } - - checkURL := getCheckRequestURL(req) - if urlutil.StripPort(checkURL.Host) == urlutil.StripPort(opts.ForwardAuthURL.Host) { - if (checkURL.Path == "/" || checkURL.Path == "/verify") && checkURL.Query().Get("uri") != "" { - verifyURL, err := url.Parse(checkURL.Query().Get("uri")) - if err != nil { - log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication") - return false - } - req.Attributes.Request.Http.Scheme = verifyURL.Scheme - req.Attributes.Request.Http.Host = verifyURL.Host - req.Attributes.Request.Http.Path = verifyURL.Path - // envoy sends the query string as part of the path - if verifyURL.RawQuery != "" { - req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery - } - return true - } - } - - return false -} - // getPeerCertificate gets the PEM-encoded peer certificate from the check request func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string { // ignore the error as we will just return the empty string in that case cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate()) return cert } + +func logAuthorizeCheck( + ctx context.Context, + in *envoy_service_auth_v2.CheckRequest, + reply *authorize.IsAuthorizedReply, + rawJWT []byte, +) { + hdrs := getCheckRequestHeaders(in) + hattrs := in.GetAttributes().GetRequest().GetHttp() + evt := log.Info().Str("service", "authorize") + // request + evt = evt.Str("request-id", requestid.FromContext(ctx)) + evt = evt.Strs("check-request-id", hdrs["X-Request-Id"]) + evt = evt.Str("method", hattrs.GetMethod()) + evt = evt.Interface("headers", hdrs) + evt = evt.Str("path", hattrs.GetPath()) + evt = evt.Str("host", hattrs.GetHost()) + evt = evt.Str("query", hattrs.GetQuery()) + // reply + evt = evt.Bool("allow", reply.GetAllow()) + evt = evt.Bool("session-expired", reply.GetSessionExpired()) + evt = evt.Strs("deny-reasons", reply.GetDenyReasons()) + evt = evt.Str("email", reply.GetEmail()) + evt = evt.Strs("groups", reply.GetGroups()) + if rawJWT != nil { + evt = evt.Str("session", string(rawJWT)) + } + if reply.GetHttpStatus() != nil { + evt = evt.Interface("http_status", reply.GetHttpStatus()) + } + evt.Msg("authorize check") +} diff --git a/authorize/grpc_test.go b/authorize/grpc_test.go new file mode 100644 index 000000000..7d6d25424 --- /dev/null +++ b/authorize/grpc_test.go @@ -0,0 +1,150 @@ +package authorize + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2" + "github.com/stretchr/testify/assert" + + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/config" +) + +const certPEM = ` +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE +BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl +cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw +WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN +TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp +bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q +5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC +7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa +BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF +BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy +LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz +cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf +BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG +AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t +L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+ +gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283 +TZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq +0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW +RvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh +yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA== +-----END CERTIFICATE-----` + +func Test_getEvaluatorRequest(t *testing.T) { + actual := getEvaluatorRequestFromCheckRequest(&envoy_service_auth_v2.CheckRequest{ + Attributes: &envoy_service_auth_v2.AttributeContext{ + Source: &envoy_service_auth_v2.AttributeContext_Peer{ + Certificate: url.QueryEscape(certPEM), + }, + Request: &envoy_service_auth_v2.AttributeContext_Request{ + Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{ + Id: "id-1234", + Method: "GET", + Headers: map[string]string{ + "accept": "text/html", + "x-forwarded-proto": "https", + }, + Path: "/some/path?qs=1", + Host: "example.com", + Scheme: "http", + Body: "BODY", + }, + }, + }, + }, []byte("HELLO WORLD")) + expect := &evaluator.Request{ + User: "HELLO WORLD", + Method: "GET", + URL: "https://example.com/some/path?qs=1", + Header: map[string][]string{ + "Accept": {"text/html"}, + "X-Forwarded-Proto": {"https"}, + }, + Host: "example.com", + RequestURI: "https://example.com/some/path?qs=1", + ClientCertificate: certPEM, + } + assert.Equal(t, expect, actual) +} + +func Test_handleForwardAuth(t *testing.T) { + checkReq := &envoy_service_auth_v2.CheckRequest{ + Attributes: &envoy_service_auth_v2.AttributeContext{ + Source: &envoy_service_auth_v2.AttributeContext_Peer{ + Certificate: url.QueryEscape(certPEM), + }, + Request: &envoy_service_auth_v2.AttributeContext_Request{ + Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{ + Method: "GET", + Path: "/verify?uri=" + url.QueryEscape("https://example.com/some/path?qs=1"), + Host: "forward-auth.example.com", + Scheme: "https", + }, + }, + }, + } + + t.Run("enabled", func(t *testing.T) { + a := new(Authorize) + a.currentOptions.Store(config.Options{ + ForwardAuthURL: mustParseURL("https://forward-auth.example.com"), + }) + isForwardAuth := a.handleForwardAuth(checkReq) + assert.True(t, isForwardAuth) + assert.Equal(t, &envoy_service_auth_v2.AttributeContext_HttpRequest{ + Method: "GET", + Path: "/some/path?qs=1", + Host: "example.com", + Scheme: "https", + }, checkReq.Attributes.Request.Http) + }) + t.Run("disabled", func(t *testing.T) { + a := new(Authorize) + a.currentOptions.Store(config.Options{ + ForwardAuthURL: nil, + }) + isForwardAuth := a.handleForwardAuth(checkReq) + assert.False(t, isForwardAuth) + }) +} + +func Test_refreshSession(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(struct { + Authorization string + }{ + Authorization: r.Header.Get("Authorization"), + }) + })) + defer srv.Close() + + sharedKey := make([]byte, 32) + a := new(Authorize) + a.currentOptions.Store(config.Options{ + AuthenticateURL: mustParseURL(srv.URL), + SharedKey: base64.StdEncoding.EncodeToString(sharedKey), + }) + + newSession, err := a.refreshSession(context.Background(), []byte("ABCD")) + assert.NoError(t, err) + assert.Equal(t, `{"Authorization":"Pomerium ABCD"}`, strings.TrimSpace(string(newSession))) +} + +func mustParseURL(str string) *url.URL { + u, err := url.Parse(str) + if err != nil { + panic(err) + } + return u +}