support both stateful and stateless authenticate (#4765)

Update the initialization logic for the authenticate, authorize, and
proxy services to automatically select between the stateful
authentication flow and the stateless authentication flow, depending on
whether Pomerium is configured to use the hosted authenticate service.

Add a unit test case to verify that the sign_out handler does not 
trigger a sign in redirect.
This commit is contained in:
Kenneth Jenkins 2023-12-07 14:24:13 -08:00 committed by GitHub
parent b9c56074aa
commit 5ccd7a520a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 12 deletions

View file

@ -84,6 +84,7 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
AllowedHeaders: []string{"*"}, AllowedHeaders: []string{"*"},
}) })
sr.Use(c.Handler) sr.Use(c.Handler)
sr.Use(a.RetrieveSession)
// routes that don't need a session: // routes that don't need a session:
sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
@ -91,7 +92,6 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
// routes that need a session: // routes that need a session:
sr = sr.NewRoute().Subrouter() sr = sr.NewRoute().Subrouter()
sr.Use(a.RetrieveSession)
sr.Use(a.VerifySession) sr.Use(a.VerifySession)
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo)) sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
sr.Path("/sign_in").Handler(httputil.HandlerFunc(a.SignIn)) sr.Path("/sign_in").Handler(httputil.HandlerFunc(a.SignIn))
@ -475,7 +475,9 @@ func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter,
return "" return ""
} }
return state.flow.RevokeSession(ctx, r, authenticator, nil) sessionState, _ := a.getSessionFromCtx(ctx)
return state.flow.RevokeSession(ctx, r, authenticator, sessionState)
} }
// Callback handles the result of a successful call to the authenticate service // Callback handles the result of a successful call to the authenticate service

View file

@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -248,6 +249,40 @@ func TestAuthenticate_SignOut(t *testing.T) {
} }
} }
func TestAuthenticate_SignOutDoesNotRequireSession(t *testing.T) {
// A direct sign_out request would not be signed.
f := new(stubFlow)
f.verifySignatureErr = errors.New("no signature")
sessionStore := &mstore.Store{LoadError: errors.New("no session")}
a := &Authenticate{
cfg: getAuthenticateConfig(WithGetIdentityProvider(func(options *config.Options, idpID string) (identity.Authenticator, error) {
return identity.MockProvider{}, nil
})),
state: atomicutil.NewValue(&authenticateState{
cookieSecret: cryptutil.NewKey(),
sessionLoader: sessionStore,
sessionStore: sessionStore,
sharedEncoder: mock.Encoder{},
flow: f,
}),
options: config.NewAtomicOptions(),
}
r := httptest.NewRequest(http.MethodGet, "/.pomerium/sign_out", nil)
w := httptest.NewRecorder()
a.Handler().ServeHTTP(w, r)
result := w.Result()
// The handler should serve a sign out confirmation page, not a login redirect.
expectedStatus := "200 OK"
if result.Status != expectedStatus {
t.Fatalf("wrong status code: got %q want %q", result.Status, expectedStatus)
}
body, _ := io.ReadAll(result.Body)
assert.Contains(t, string(body), `"page":"SignOutConfirm"`)
}
func TestAuthenticate_OAuthCallback(t *testing.T) { func TestAuthenticate_OAuthCallback(t *testing.T) {
t.Parallel() t.Parallel()

View file

@ -144,6 +144,7 @@ func newAuthenticateStateFromConfig(
} }
} }
if cfg.Options.UseStatelessAuthenticateFlow() {
state.flow, err = authenticateflow.NewStateless( state.flow, err = authenticateflow.NewStateless(
cfg, cfg,
cookieStore, cookieStore,
@ -151,6 +152,9 @@ func newAuthenticateStateFromConfig(
authenticateConfig.profileTrimFn, authenticateConfig.profileTrimFn,
authenticateConfig.authEventFn, authenticateConfig.authEventFn,
) )
} else {
state.flow, err = authenticateflow.NewStateful(cfg, cookieStore)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -83,7 +83,11 @@ func newAuthorizeStateFromConfig(
return nil, fmt.Errorf("authorize: invalid session store: %w", err) return nil, fmt.Errorf("authorize: invalid session store: %w", err)
} }
if cfg.Options.UseStatelessAuthenticateFlow() {
state.authenticateFlow, err = authenticateflow.NewStateless(cfg, nil, nil, nil, nil) state.authenticateFlow, err = authenticateflow.NewStateless(cfg, nil, nil, nil, nil)
} else {
state.authenticateFlow, err = authenticateflow.NewStateful(cfg, nil)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -827,6 +827,16 @@ func (o *Options) GetInternalAuthenticateURL() (*url.URL, error) {
return urlutil.ParseAndValidateURL(o.AuthenticateInternalURLString) return urlutil.ParseAndValidateURL(o.AuthenticateInternalURLString)
} }
// UseStatelessAuthenticateFlow returns true if the stateless authentication
// flow should be used (i.e. for hosted authenticate).
func (o *Options) UseStatelessAuthenticateFlow() bool {
u, err := o.GetInternalAuthenticateURL()
if err != nil {
return false
}
return urlutil.IsHostedAuthenticateDomain(u.Hostname())
}
// GetAuthorizeURLs returns the AuthorizeURLs in the options or 127.0.0.1:5443. // GetAuthorizeURLs returns the AuthorizeURLs in the options or 127.0.0.1:5443.
func (o *Options) GetAuthorizeURLs() ([]*url.URL, error) { func (o *Options) GetAuthorizeURLs() ([]*url.URL, error) {
if IsAll(o.Services) && o.AuthorizeURLString == "" && len(o.AuthorizeURLStrings) == 0 { if IsAll(o.Services) && o.AuthorizeURLString == "" && len(o.AuthorizeURLStrings) == 0 {

View file

@ -856,6 +856,21 @@ func TestOptions_DefaultURL(t *testing.T) {
} }
} }
func TestOptions_UseStatelessAuthenticateFlow(t *testing.T) {
t.Run("enabled by default", func(t *testing.T) {
options := &Options{}
assert.True(t, options.UseStatelessAuthenticateFlow())
})
t.Run("enabled explicitly", func(t *testing.T) {
options := &Options{AuthenticateURLString: "https://authenticate.pomerium.app"}
assert.True(t, options.UseStatelessAuthenticateFlow())
})
t.Run("disabled", func(t *testing.T) {
options := &Options{AuthenticateURLString: "https://authenticate.example.com"}
assert.False(t, options.UseStatelessAuthenticateFlow())
})
}
func TestOptions_GetOauthOptions(t *testing.T) { func TestOptions_GetOauthOptions(t *testing.T) {
opts := &Options{AuthenticateURLString: "https://authenticate.example.com"} opts := &Options{AuthenticateURLString: "https://authenticate.example.com"}
oauthOptions, err := opts.GetOauthOptions() oauthOptions, err := opts.GetOauthOptions()

View file

@ -114,8 +114,12 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) {
state.programmaticRedirectDomainWhitelist = cfg.Options.ProgrammaticRedirectDomainWhitelist state.programmaticRedirectDomainWhitelist = cfg.Options.ProgrammaticRedirectDomainWhitelist
if cfg.Options.UseStatelessAuthenticateFlow() {
state.authenticateFlow, err = authenticateflow.NewStateless( state.authenticateFlow, err = authenticateflow.NewStateless(
cfg, state.sessionStore, nil, nil, nil) cfg, state.sessionStore, nil, nil, nil)
} else {
state.authenticateFlow, err = authenticateflow.NewStateful(cfg, state.sessionStore)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }