mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-04 01:09:36 +02:00
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:
parent
ae3049baca
commit
0d9a372182
7 changed files with 284 additions and 27 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue