mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 23:57:34 +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/authorize/evaluator/opa"
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
"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/log"
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/metrics"
|
"github.com/pomerium/pomerium/internal/telemetry/metrics"
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||||
|
@ -31,11 +33,24 @@ func (a *atomicOptions) Store(options config.Options) {
|
||||||
a.value.Store(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
|
// Authorize struct holds
|
||||||
type Authorize struct {
|
type Authorize struct {
|
||||||
pe evaluator.Evaluator
|
pe evaluator.Evaluator
|
||||||
|
|
||||||
currentOptions atomicOptions
|
currentOptions atomicOptions
|
||||||
|
currentEncoder atomicMarshalUnmarshaler
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates and creates a new Authorize service from a set of config options.
|
// 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)
|
return nil, fmt.Errorf("authorize: bad options: %w", err)
|
||||||
}
|
}
|
||||||
var a Authorize
|
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{})
|
a.currentOptions.Store(config.Options{})
|
||||||
err := a.UpdateOptions(opts)
|
err = a.UpdateOptions(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,17 @@ package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||||
"github.com/pomerium/pomerium/config"
|
"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/log"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
"github.com/pomerium/pomerium/internal/sessions/cookie"
|
"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()
|
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
||||||
|
|
||||||
hdrs := getCheckRequestHeaders(in)
|
hdrs := getCheckRequestHeaders(in)
|
||||||
|
|
||||||
|
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
|
||||||
sess, sesserr := a.loadSessionFromCheckRequest(in)
|
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)
|
requestURL := getCheckRequestURL(in)
|
||||||
req := &evaluator.Request{
|
req := &evaluator.Request{
|
||||||
User: sess,
|
User: string(sess),
|
||||||
Header: hdrs,
|
Header: hdrs,
|
||||||
Host: hattrs.GetHost(),
|
Host: hattrs.GetHost(),
|
||||||
Method: hattrs.GetMethod(),
|
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.Strs("deny-reasons", reply.GetDenyReasons())
|
||||||
evt = evt.Str("email", reply.GetEmail())
|
evt = evt.Str("email", reply.GetEmail())
|
||||||
evt = evt.Strs("groups", reply.GetGroups())
|
evt = evt.Strs("groups", reply.GetGroups())
|
||||||
|
evt = evt.Str("session", string(sess))
|
||||||
evt.Msg("authorize check")
|
evt.Msg("authorize check")
|
||||||
|
|
||||||
if reply.Allow {
|
if reply.Allow {
|
||||||
return &envoy_service_auth_v2.CheckResponse{
|
return &envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{OkResponse: &envoy_service_auth_v2.OkHttpResponse{}},
|
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
||||||
|
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
||||||
|
Headers: requestHeaders,
|
||||||
|
},
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,15 +161,100 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) loadSessionFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) (string, error) {
|
func (a *Authorize) getEnvoyRequestHeaders(rawSession []byte) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
||||||
opts := a.currentOptions.Load()
|
cookieStore, err := a.getCookieStore()
|
||||||
|
|
||||||
// used to load and verify JWT tokens signed by the authenticate service
|
|
||||||
encoder, err := jws.NewHS256Signer([]byte(opts.SharedKey), opts.AuthenticateURL.Host)
|
|
||||||
if err != nil {
|
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{
|
cookieOptions := &cookie.Options{
|
||||||
Name: opts.CookieName,
|
Name: opts.CookieName,
|
||||||
Domain: opts.CookieDomain,
|
Domain: opts.CookieDomain,
|
||||||
|
@ -155,13 +265,9 @@ func (a *Authorize) loadSessionFromCheckRequest(req *envoy_service_auth_v2.Check
|
||||||
|
|
||||||
cookieStore, err := cookie.NewStore(cookieOptions, encoder)
|
cookieStore, err := cookie.NewStore(cookieOptions, encoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return cookieStore, nil
|
||||||
sess, err := cookieStore.LoadSession(&http.Request{
|
|
||||||
Header: http.Header(getCheckRequestHeaders(req)),
|
|
||||||
})
|
|
||||||
return sess, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFullURL(rawurl, host string) string {
|
func getFullURL(rawurl, host string) string {
|
||||||
|
|
|
@ -7,8 +7,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/integration/internal/flows"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/integration/internal/flows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthorization(t *testing.T) {
|
func TestAuthorization(t *testing.T) {
|
||||||
|
@ -34,14 +35,16 @@ func TestAuthorization(t *testing.T) {
|
||||||
t.Run("domains", func(t *testing.T) {
|
t.Run("domains", func(t *testing.T) {
|
||||||
t.Run("allowed", func(t *testing.T) {
|
t.Run("allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), "bob@dogs.test", []string{"user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"),
|
||||||
|
flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test")
|
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("not allowed", func(t *testing.T) {
|
t.Run("not allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), "joe@cats.test", []string{"user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"),
|
||||||
|
flows.WithEmail("joe@cats.test"), flows.WithGroups("user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assertDeniedAccess(t, res, "expected Forbidden for cats.test")
|
assertDeniedAccess(t, res, "expected Forbidden for cats.test")
|
||||||
}
|
}
|
||||||
|
@ -50,14 +53,16 @@ func TestAuthorization(t *testing.T) {
|
||||||
t.Run("users", func(t *testing.T) {
|
t.Run("users", func(t *testing.T) {
|
||||||
t.Run("allowed", func(t *testing.T) {
|
t.Run("allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), "bob@dogs.test", []string{"user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"),
|
||||||
|
flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for bob@dogs.test")
|
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for bob@dogs.test")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("not allowed", func(t *testing.T) {
|
t.Run("not allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), "joe@cats.test", []string{"user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"),
|
||||||
|
flows.WithEmail("joe@cats.test"), flows.WithGroups("user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assertDeniedAccess(t, res, "expected Forbidden for joe@cats.test")
|
assertDeniedAccess(t, res, "expected Forbidden for joe@cats.test")
|
||||||
}
|
}
|
||||||
|
@ -66,19 +71,61 @@ func TestAuthorization(t *testing.T) {
|
||||||
t.Run("groups", func(t *testing.T) {
|
t.Run("groups", func(t *testing.T) {
|
||||||
t.Run("allowed", func(t *testing.T) {
|
t.Run("allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), "bob@dogs.test", []string{"admin", "user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"),
|
||||||
|
flows.WithEmail("bob@dogs.test"), flows.WithGroups("admin", "user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for admin")
|
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for admin")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("not allowed", func(t *testing.T) {
|
t.Run("not allowed", func(t *testing.T) {
|
||||||
client := testcluster.NewHTTPClient()
|
client := testcluster.NewHTTPClient()
|
||||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), "joe@cats.test", []string{"user"})
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"),
|
||||||
|
flows.WithEmail("joe@cats.test"), flows.WithGroups("user"))
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assertDeniedAccess(t, res, "expected Forbidden for user, but got %d", res.StatusCode)
|
assertDeniedAccess(t, res, "expected Forbidden for user, but got %d", res.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("refresh", func(t *testing.T) {
|
||||||
|
client := testcluster.NewHTTPClient()
|
||||||
|
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"),
|
||||||
|
flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"), flows.WithTokenExpiration(time.Second))
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test")
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
// poll till we get a new cookie because of a refreshed session
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 500)
|
||||||
|
defer ticker.Stop()
|
||||||
|
deadline := time.NewTimer(time.Second * 10)
|
||||||
|
defer deadline.Stop()
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
case <-deadline.C:
|
||||||
|
t.Fatal("timed out waiting for refreshed session")
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("timed out waiting for refreshed session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = client.Get(mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain").String())
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if !assert.Equal(t, http.StatusOK, res.StatusCode, "failed after %d times", i+1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.Header.Get("Set-Cookie") != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustParseURL(str string) *url.URL {
|
func mustParseURL(str string) *url.URL {
|
||||||
|
|
|
@ -6,7 +6,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/integration/internal/forms"
|
"github.com/pomerium/pomerium/integration/internal/forms"
|
||||||
)
|
)
|
||||||
|
@ -17,9 +19,50 @@ const (
|
||||||
pomeriumCallbackPath = "/.pomerium/callback/"
|
pomeriumCallbackPath = "/.pomerium/callback/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type authenticateConfig struct {
|
||||||
|
email string
|
||||||
|
groups []string
|
||||||
|
tokenExpiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// An AuthenticateOption is an option for authentication.
|
||||||
|
type AuthenticateOption func(cfg *authenticateConfig)
|
||||||
|
|
||||||
|
func getAuthenticateConfig(options ...AuthenticateOption) *authenticateConfig {
|
||||||
|
cfg := &authenticateConfig{
|
||||||
|
tokenExpiration: time.Hour * 24,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEmail sets the email to use.
|
||||||
|
func WithEmail(email string) AuthenticateOption {
|
||||||
|
return func(cfg *authenticateConfig) {
|
||||||
|
cfg.email = email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGroups sets the groups to use.
|
||||||
|
func WithGroups(groups ...string) AuthenticateOption {
|
||||||
|
return func(cfg *authenticateConfig) {
|
||||||
|
cfg.groups = groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTokenExpiration sets the token expiration.
|
||||||
|
func WithTokenExpiration(tokenExpiration time.Duration) AuthenticateOption {
|
||||||
|
return func(cfg *authenticateConfig) {
|
||||||
|
cfg.tokenExpiration = tokenExpiration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate submits a request to a URL, expects a redirect to authenticate and then openid and logs in.
|
// Authenticate submits a request to a URL, expects a redirect to authenticate and then openid and logs in.
|
||||||
// Finally it expects to redirect back to the original page.
|
// Finally it expects to redirect back to the original page.
|
||||||
func Authenticate(ctx context.Context, client *http.Client, url *url.URL, email string, groups []string) (*http.Response, error) {
|
func Authenticate(ctx context.Context, client *http.Client, url *url.URL, options ...AuthenticateOption) (*http.Response, error) {
|
||||||
|
cfg := getAuthenticateConfig(options...)
|
||||||
originalHostname := url.Hostname()
|
originalHostname := url.Hostname()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
|
||||||
|
@ -68,8 +111,11 @@ func Authenticate(ctx context.Context, client *http.Client, url *url.URL, email
|
||||||
forms := forms.Parse(res.Body)
|
forms := forms.Parse(res.Body)
|
||||||
if len(forms) > 0 {
|
if len(forms) > 0 {
|
||||||
f := forms[0]
|
f := forms[0]
|
||||||
f.Inputs["email"] = email
|
f.Inputs["email"] = cfg.email
|
||||||
f.Inputs["groups"] = strings.Join(groups, ",")
|
if len(cfg.groups) > 0 {
|
||||||
|
f.Inputs["groups"] = strings.Join(cfg.groups, ",")
|
||||||
|
}
|
||||||
|
f.Inputs["token_expiration"] = strconv.Itoa(int(cfg.tokenExpiration.Seconds()))
|
||||||
req, err = f.NewRequestWithContext(ctx, req.URL)
|
req, err = f.NewRequestWithContext(ctx, req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -178,6 +178,7 @@ local PomeriumDeployment = function(svc) {
|
||||||
ip: '10.96.1.1',
|
ip: '10.96.1.1',
|
||||||
hostnames: [
|
hostnames: [
|
||||||
'openid.localhost.pomerium.io',
|
'openid.localhost.pomerium.io',
|
||||||
|
'authenticate.localhost.pomerium.io'
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
initContainers: [{
|
initContainers: [{
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,11 +9,13 @@ import (
|
||||||
envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||||
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
envoy_extensions_filters_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
|
envoy_extensions_filters_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
|
||||||
|
envoy_extensions_filters_http_lua_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
|
||||||
envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||||
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||||
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
"github.com/golang/protobuf/ptypes/any"
|
"github.com/golang/protobuf/ptypes/any"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
@ -99,6 +101,28 @@ func (srv *Server) buildHTTPListener(options config.Options) *envoy_config_liste
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
luaConfig, _ := ptypes.MarshalAny(&envoy_extensions_filters_http_lua_v3.Lua{
|
||||||
|
InlineCode: `
|
||||||
|
function envoy_on_request(request_handle)
|
||||||
|
local headers = request_handle:headers()
|
||||||
|
local dynamic_meta = request_handle:streamInfo():dynamicMetadata()
|
||||||
|
if headers:get("x-pomerium-set-cookie") ~= nil then
|
||||||
|
dynamic_meta:set("envoy.filters.http.lua", "pomerium_set_cookie", headers:get("x-pomerium-set-cookie"))
|
||||||
|
headers:remove("x-pomerium-set-cookie")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function envoy_on_response(response_handle)
|
||||||
|
local headers = response_handle:headers()
|
||||||
|
local dynamic_meta = response_handle:streamInfo():dynamicMetadata()
|
||||||
|
local tbl = dynamic_meta:get("envoy.filters.http.lua")
|
||||||
|
if tbl ~= nil and tbl["pomerium_set_cookie"] ~= nil then
|
||||||
|
headers:add("set-cookie", tbl["pomerium_set_cookie"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
tc, _ := ptypes.MarshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
tc, _ := ptypes.MarshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
||||||
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
||||||
StatPrefix: "ingress",
|
StatPrefix: "ingress",
|
||||||
|
@ -115,6 +139,12 @@ func (srv *Server) buildHTTPListener(options config.Options) *envoy_config_liste
|
||||||
TypedConfig: extAuthZ,
|
TypedConfig: extAuthZ,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "envoy.filters.http.lua",
|
||||||
|
ConfigType: &envoy_http_connection_manager.HttpFilter_TypedConfig{
|
||||||
|
TypedConfig: luaConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "envoy.filters.http.router",
|
Name: "envoy.filters.http.router",
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue