pomerium/authenticate/state.go
Kenneth Jenkins 5ccd7a520a
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.
2023-12-07 14:24:13 -08:00

163 lines
4.7 KiB
Go

package authenticate
import (
"context"
"crypto/cipher"
"fmt"
"net/http"
"net/url"
"github.com/go-jose/go-jose/v3"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/authenticateflow"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/handlers"
"github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/sessions/cookie"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
type flow interface {
VerifyAuthenticateSignature(r *http.Request) error
SignIn(w http.ResponseWriter, r *http.Request, sessionState *sessions.State) error
PersistSession(ctx context.Context, w http.ResponseWriter, sessionState *sessions.State, claims identity.SessionClaims, accessToken *oauth2.Token) error
VerifySession(ctx context.Context, r *http.Request, sessionState *sessions.State) error
RevokeSession(ctx context.Context, r *http.Request, authenticator identity.Authenticator, sessionState *sessions.State) string
GetUserInfoData(r *http.Request, sessionState *sessions.State) handlers.UserInfoData
LogAuthenticateEvent(r *http.Request)
GetIdentityProviderIDForURLValues(url.Values) string
}
type authenticateState struct {
flow flow
redirectURL *url.URL
// sharedEncoder is the encoder to use to serialize data to be consumed
// by other services
sharedEncoder encoding.MarshalUnmarshaler
// sharedKey is the secret to encrypt and authenticate data shared between services
sharedKey []byte
// sharedCipher is the cipher to use to encrypt/decrypt data shared between services
sharedCipher cipher.AEAD
// cookieSecret is the secret to encrypt and authenticate session data
cookieSecret []byte
// cookieCipher is the cipher to use to encrypt/decrypt session data
cookieCipher cipher.AEAD
// sessionStore is the session store used to persist a user's session
sessionStore sessions.SessionStore
// sessionLoaders are a collection of session loaders to attempt to pull
// a user's session state from
sessionLoader sessions.SessionLoader
jwk *jose.JSONWebKeySet
}
func newAuthenticateState() *authenticateState {
return &authenticateState{
jwk: new(jose.JSONWebKeySet),
}
}
func newAuthenticateStateFromConfig(
cfg *config.Config, authenticateConfig *authenticateConfig,
) (*authenticateState, error) {
err := ValidateOptions(cfg.Options)
if err != nil {
return nil, err
}
state := &authenticateState{}
authenticateURL, err := cfg.Options.GetAuthenticateURL()
if err != nil {
return nil, err
}
state.redirectURL, err = urlutil.DeepCopy(authenticateURL)
if err != nil {
return nil, err
}
state.redirectURL.Path = cfg.Options.AuthenticateCallbackPath
// shared cipher to encrypt data before passing data between services
state.sharedKey, err = cfg.Options.GetSharedKey()
if err != nil {
return nil, err
}
state.sharedCipher, err = cryptutil.NewAEADCipher(state.sharedKey)
if err != nil {
return nil, err
}
// shared state encoder setup
state.sharedEncoder, err = jws.NewHS256Signer(state.sharedKey)
if err != nil {
return nil, err
}
// private state encoder setup, used to encrypt oauth2 tokens
state.cookieSecret, err = cfg.Options.GetCookieSecret()
if err != nil {
return nil, err
}
state.cookieCipher, err = cryptutil.NewAEADCipher(state.cookieSecret)
if err != nil {
return nil, err
}
cookieStore, err := cookie.NewStore(func() cookie.Options {
return cookie.Options{
Name: cfg.Options.CookieName + "_authenticate",
Domain: cfg.Options.CookieDomain,
Secure: cfg.Options.CookieSecure,
HTTPOnly: cfg.Options.CookieHTTPOnly,
Expire: cfg.Options.CookieExpire,
SameSite: cfg.Options.GetCookieSameSite(),
}
}, state.sharedEncoder)
if err != nil {
return nil, err
}
state.sessionStore = cookieStore
state.sessionLoader = cookieStore
state.jwk = new(jose.JSONWebKeySet)
signingKey, err := cfg.Options.GetSigningKey()
if err != nil {
return nil, err
}
if len(signingKey) > 0 {
ks, err := cryptutil.PublicJWKsFromBytes(signingKey)
if err != nil {
return nil, fmt.Errorf("authenticate: failed to convert jwks: %w", err)
}
for _, k := range ks {
state.jwk.Keys = append(state.jwk.Keys, *k)
}
}
if cfg.Options.UseStatelessAuthenticateFlow() {
state.flow, err = authenticateflow.NewStateless(
cfg,
cookieStore,
authenticateConfig.getIdentityProvider,
authenticateConfig.profileTrimFn,
authenticateConfig.authEventFn,
)
} else {
state.flow, err = authenticateflow.NewStateful(cfg, cookieStore)
}
if err != nil {
return nil, err
}
return state, nil
}