envoy: implement refresh session (#674)

* authorize: refresh session WIP

* remove upstream cookie with lua

* only refresh session on expired

* authorize: handle session expiration

* authorize: add refresh test, fix isExpired check

* proxy: implement preserve host header option

* authorize: allow CORS preflight requests

* proxy: add request headers

* authenticate: use id token expiry
This commit is contained in:
Caleb Doxsey 2020-05-11 07:29:23 -06:00 committed by Travis Groth
parent ae3049baca
commit 0d9a372182
7 changed files with 284 additions and 27 deletions

View file

@ -12,6 +12,8 @@ import (
"github.com/pomerium/pomerium/authorize/evaluator/opa"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/metrics"
"github.com/pomerium/pomerium/internal/telemetry/trace"
@ -31,11 +33,24 @@ func (a *atomicOptions) Store(options config.Options) {
a.value.Store(options)
}
type atomicMarshalUnmarshaler struct {
value atomic.Value
}
func (a *atomicMarshalUnmarshaler) Load() encoding.MarshalUnmarshaler {
return a.value.Load().(encoding.MarshalUnmarshaler)
}
func (a *atomicMarshalUnmarshaler) Store(encoder encoding.MarshalUnmarshaler) {
a.value.Store(encoder)
}
// Authorize struct holds
type Authorize struct {
pe evaluator.Evaluator
currentOptions atomicOptions
currentEncoder atomicMarshalUnmarshaler
}
// New validates and creates a new Authorize service from a set of config options.
@ -44,8 +59,19 @@ func New(opts config.Options) (*Authorize, error) {
return nil, fmt.Errorf("authorize: bad options: %w", err)
}
var a Authorize
var host string
if opts.AuthenticateURL != nil {
host = opts.AuthenticateURL.Host
}
encoder, err := jws.NewHS256Signer([]byte(opts.SharedKey), host)
if err != nil {
return nil, err
}
a.currentEncoder.Store(encoder)
a.currentOptions.Store(config.Options{})
err := a.UpdateOptions(opts)
err = a.UpdateOptions(opts)
if err != nil {
return nil, err
}

View file

@ -2,13 +2,17 @@ package authorize
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/sessions/cookie"
@ -35,10 +39,26 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
hattrs := in.GetAttributes().GetRequest().GetHttp()
hdrs := getCheckRequestHeaders(in)
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
sess, sesserr := a.loadSessionFromCheckRequest(in)
if a.isExpired(sess) {
log.Info().Msg("refreshing session")
if newSession, err := a.refreshSession(ctx, sess); err == nil {
sess = newSession
sesserr = nil
requestHeaders, err = a.getEnvoyRequestHeaders(sess)
if err != nil {
log.Warn().Err(err).Msg("authorize: error generating new request headers")
}
} else {
log.Warn().Err(err).Msg("authorize: error refreshing session")
}
}
requestURL := getCheckRequestURL(in)
req := &evaluator.Request{
User: sess,
User: string(sess),
Header: hdrs,
Host: hattrs.GetHost(),
Method: hattrs.GetMethod(),
@ -65,12 +85,17 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
evt = evt.Strs("deny-reasons", reply.GetDenyReasons())
evt = evt.Str("email", reply.GetEmail())
evt = evt.Strs("groups", reply.GetGroups())
evt = evt.Str("session", string(sess))
evt.Msg("authorize check")
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{}},
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
}
@ -136,15 +161,100 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
}, nil
}
func (a *Authorize) loadSessionFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) (string, error) {
opts := a.currentOptions.Load()
// used to load and verify JWT tokens signed by the authenticate service
encoder, err := jws.NewHS256Signer([]byte(opts.SharedKey), opts.AuthenticateURL.Host)
func (a *Authorize) getEnvoyRequestHeaders(rawSession []byte) ([]*envoy_api_v2_core.HeaderValueOption, error) {
cookieStore, err := a.getCookieStore()
if err != nil {
return "", err
return nil, err
}
recorder := httptest.NewRecorder()
err = cookieStore.SaveSession(recorder, nil /* unused by cookie store */, string(rawSession))
if err != nil {
return nil, fmt.Errorf("authorize: error saving cookie: %w", err)
}
var hvos []*envoy_api_v2_core.HeaderValueOption
for k, vs := range recorder.Header() {
for _, v := range vs {
hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{
Header: &envoy_api_v2_core.HeaderValue{
Key: "x-pomerium-" + k,
Value: v,
},
})
}
}
return hvos, nil
}
func (a *Authorize) refreshSession(ctx context.Context, rawSession []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"})
q := refreshURI.Query()
q.Set(urlutil.QueryAccessTokenID, state.AccessTokenID) // hash value points to parent token
q.Set(urlutil.QueryAudience, strings.Join(state.Audience, ",")) // request's audience, this route
refreshURI.RawQuery = q.Encode()
signedRefreshURL := urlutil.NewSignedURL(options.SharedKey, refreshURI).String()
// 2 - http call to authenticate service
req, err := http.NewRequestWithContext(ctx, http.MethodGet, signedRefreshURL, nil)
if err != nil {
return nil, fmt.Errorf("authorize: refresh request: %w", err)
}
req.Header.Set("X-Requested-With", "XmlHttpRequest")
req.Header.Set("Accept", "application/json")
res, err := httputil.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("authorize: client err %s: %w", signedRefreshURL, err)
}
defer res.Body.Close()
newJwt, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
if err != nil {
return nil, err
}
// auth couldn't refresh the session, delete the session and reload via 302
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("authorize: backend refresh failed: %s", newJwt)
}
return newJwt, nil
}
func (a *Authorize) loadSessionFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) ([]byte, error) {
cookieStore, err := a.getCookieStore()
if err != nil {
return nil, err
}
sess, err := cookieStore.LoadSession(&http.Request{
Header: getCheckRequestHeaders(req),
})
if err != nil {
return nil, err
}
return []byte(sess), nil
}
func (a *Authorize) isExpired(rawSession []byte) bool {
state := sessions.State{}
err := a.currentEncoder.Load().Unmarshal(rawSession, &state)
return err == nil && state.IsExpired()
}
func (a *Authorize) getCookieStore() (sessions.SessionStore, error) {
opts := a.currentOptions.Load()
encoder := a.currentEncoder.Load()
cookieOptions := &cookie.Options{
Name: opts.CookieName,
Domain: opts.CookieDomain,
@ -155,13 +265,9 @@ func (a *Authorize) loadSessionFromCheckRequest(req *envoy_service_auth_v2.Check
cookieStore, err := cookie.NewStore(cookieOptions, encoder)
if err != nil {
return "", err
return nil, err
}
sess, err := cookieStore.LoadSession(&http.Request{
Header: http.Header(getCheckRequestHeaders(req)),
})
return sess, err
return cookieStore, nil
}
func getFullURL(rawurl, host string) string {