mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 10:56:28 +02:00
authorize: refactor and add additional unit tests (#757)
* authorize: clean up code, add test * authorize: additional test * authorize: additional test
This commit is contained in:
parent
5c3c020508
commit
a969f33d88
4 changed files with 314 additions and 157 deletions
|
@ -3,6 +3,7 @@ package authorize
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
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/genproto/googleapis/rpc/status"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Authorize) deniedResponse(in *envoy_service_auth_v2.CheckRequest,
|
func (a *Authorize) okResponse(
|
||||||
code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse {
|
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
|
returnHTMLError := true
|
||||||
inHeaders := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
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 {
|
func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption {
|
||||||
return &envoy_api_v2_core.HeaderValueOption{
|
return &envoy_api_v2_core.HeaderValueOption{
|
||||||
Header: &envoy_api_v2_core.HeaderValue{
|
Header: &envoy_api_v2_core.HeaderValue{
|
|
@ -29,15 +29,11 @@ type Request struct {
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
// URL specifies either the URI being requested.
|
// URL specifies either the URI being requested.
|
||||||
URL string `json:"url,omitempty"`
|
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
|
// Header contains the request header fields either received
|
||||||
// by the server or to be sent by the client.
|
// by the server or to be sent by the client.
|
||||||
Header map[string][]string `json:"headers,omitempty"`
|
Header map[string][]string `json:"headers,omitempty"`
|
||||||
// Host specifies the host on which the URL is sought.
|
// Host specifies the host on which the URL is sought.
|
||||||
Host string `json:"host,omitempty"`
|
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
|
// RequestURI is the unmodified request-target of the
|
||||||
// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
|
// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
|
||||||
// to a server. Usually the URL field should be used instead.
|
// to a server. Usually the URL field should be used instead.
|
||||||
|
|
|
@ -2,6 +2,7 @@ package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -10,7 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
"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/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
|
@ -20,8 +21,6 @@ import (
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
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"
|
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.
|
// 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")
|
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
opts := a.currentOptions.Load()
|
|
||||||
|
|
||||||
// maybe rewrite http request for forward auth
|
// maybe rewrite http request for forward auth
|
||||||
isForwardAuth := handleForwardAuth(opts, in)
|
isForwardAuth := a.handleForwardAuth(in)
|
||||||
|
|
||||||
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
|
||||||
hreq := getHTTPRequestFromCheckRequest(in)
|
hreq := getHTTPRequestFromCheckRequest(in)
|
||||||
|
|
||||||
hdrs := getCheckRequestHeaders(in)
|
|
||||||
|
|
||||||
isNewSession := false
|
isNewSession := false
|
||||||
sess, sesserr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
|
rawJWT, sessionErr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
|
||||||
if a.isExpired(sess) {
|
if a.isExpired(rawJWT) {
|
||||||
log.Info().Msg("refreshing session")
|
log.Info().Msg("refreshing session")
|
||||||
if newSession, err := a.refreshSession(ctx, sess); err == nil {
|
if newRawJWT, err := a.refreshSession(ctx, rawJWT); err == nil {
|
||||||
sess = newSession
|
rawJWT = newRawJWT
|
||||||
sesserr = nil
|
sessionErr = nil
|
||||||
isNewSession = true
|
isNewSession = true
|
||||||
} else {
|
} else {
|
||||||
log.Warn().Err(err).Msg("authorize: error refreshing session")
|
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)
|
req := getEvaluatorRequestFromCheckRequest(in, rawJWT)
|
||||||
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),
|
|
||||||
}
|
|
||||||
reply, err := a.pe.IsAuthorized(ctx, req)
|
reply, err := a.pe.IsAuthorized(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logAuthorizeCheck(ctx, in, reply, rawJWT)
|
||||||
|
|
||||||
evt := log.Info().Str("service", "authorize")
|
switch {
|
||||||
// request
|
case reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK:
|
||||||
evt = evt.Str("request-id", requestid.FromContext(ctx))
|
// custom error from the IsAuthorized call
|
||||||
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 {
|
|
||||||
return a.deniedResponse(in,
|
return a.deniedResponse(in,
|
||||||
reply.GetHttpStatus().GetCode(),
|
reply.GetHttpStatus().GetCode(),
|
||||||
reply.GetHttpStatus().GetMessage(),
|
reply.GetHttpStatus().GetMessage(),
|
||||||
reply.GetHttpStatus().GetHeaders(),
|
reply.GetHttpStatus().GetHeaders(),
|
||||||
), nil
|
), nil
|
||||||
}
|
|
||||||
|
|
||||||
if reply.Allow {
|
case reply.Allow:
|
||||||
return &envoy_service_auth_v2.CheckResponse{
|
// ok!
|
||||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
return a.okResponse(reply, rawJWT, isNewSession), nil
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: requestHeaders,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if reply.SessionExpired {
|
case reply.SessionExpired,
|
||||||
sesserr = sessions.ErrExpired
|
errors.Is(sessionErr, sessions.ErrExpired),
|
||||||
}
|
errors.Is(sessionErr, sessions.ErrIssuedInTheFuture),
|
||||||
|
errors.Is(sessionErr, sessions.ErrMalformed),
|
||||||
switch sesserr {
|
errors.Is(sessionErr, sessions.ErrNoSessionFound),
|
||||||
case sessions.ErrExpired, sessions.ErrIssuedInTheFuture, sessions.ErrMalformed, sessions.ErrNoSessionFound, sessions.ErrNotValidYet:
|
errors.Is(sessionErr, sessions.ErrNotValidYet):
|
||||||
// redirect to login
|
// redirect to login
|
||||||
default:
|
|
||||||
var msg string
|
|
||||||
if sesserr != nil {
|
|
||||||
msg = sesserr.Error()
|
|
||||||
}
|
|
||||||
// all other errors
|
|
||||||
return a.deniedResponse(in, http.StatusForbidden, msg, nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// no redirect for forward auth, that's handled by a separate config setting
|
// no redirect for forward auth, that's handled by a separate config setting
|
||||||
if isForwardAuth {
|
if isForwardAuth {
|
||||||
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
|
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"})
|
return a.redirectResponse(in), nil
|
||||||
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{
|
default:
|
||||||
"Location": redirectTo,
|
// all other errors
|
||||||
}), nil
|
var msg string
|
||||||
|
if sessionErr != nil {
|
||||||
|
msg = sessionErr.Error()
|
||||||
|
}
|
||||||
|
return a.deniedResponse(in, http.StatusForbidden, msg, nil), 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
|
var hvos []*envoy_api_v2_core.HeaderValueOption
|
||||||
|
|
||||||
if isNewSession {
|
if isNewSession {
|
||||||
|
@ -164,44 +101,28 @@ func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([]
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrs, err := getJWTSetCookieHeaders(cookieStore, rawjwt)
|
hdrs, err := getJWTSetCookieHeaders(cookieStore, rawJWT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for k, v := range hdrs {
|
for k, v := range hdrs {
|
||||||
hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{
|
hvos = append(hvos, mkHeader("x-pomerium-"+k, v))
|
||||||
Header: &envoy_api_v2_core.HeaderValue{
|
|
||||||
Key: "x-pomerium-" + k,
|
|
||||||
Value: v,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawjwt)
|
hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawJWT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for k, v := range hdrs {
|
for k, v := range hdrs {
|
||||||
hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{
|
hvos = append(hvos, mkHeader(k, v))
|
||||||
Header: &envoy_api_v2_core.HeaderValue{
|
|
||||||
Key: k,
|
|
||||||
Value: v,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hvos, nil
|
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()
|
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
|
// 1 - build a signed url to call refresh on authenticate service
|
||||||
refreshURI := options.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/refresh"})
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("authorize: refresh request: %w", err)
|
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("X-Requested-With", "XmlHttpRequest")
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
@ -238,6 +159,49 @@ func (a *Authorize) isExpired(rawSession []byte) bool {
|
||||||
return err == nil && state.IsExpired()
|
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 {
|
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) *http.Request {
|
||||||
hattrs := req.GetAttributes().GetRequest().GetHttp()
|
hattrs := req.GetAttributes().GetRequest().GetHttp()
|
||||||
return &http.Request{
|
return &http.Request{
|
||||||
|
@ -274,44 +238,49 @@ func getCheckRequestURL(req *envoy_service_auth_v2.CheckRequest) *url.URL {
|
||||||
u.Path = path
|
u.Path = path
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.Headers != nil {
|
if h.GetHeaders() != nil {
|
||||||
if fwdProto, ok := h.Headers["x-forwarded-proto"]; ok {
|
if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok {
|
||||||
u.Scheme = fwdProto
|
u.Scheme = fwdProto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return u
|
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
|
// getPeerCertificate gets the PEM-encoded peer certificate from the check request
|
||||||
func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string {
|
func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string {
|
||||||
// ignore the error as we will just return the empty string in that case
|
// ignore the error as we will just return the empty string in that case
|
||||||
cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate())
|
cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate())
|
||||||
return cert
|
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")
|
||||||
|
}
|
||||||
|
|
150
authorize/grpc_test.go
Normal file
150
authorize/grpc_test.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue