package config import ( "fmt" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwk" log "github.com/sirupsen/logrus" "github.com/nais/wonderwall/pkg/config" "github.com/nais/wonderwall/pkg/openid/scopes" ) type AuthMethod string const ( AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" AuthMethodClientSecret AuthMethod = "client_secret" ) type Client interface { ACRValues() string Audiences() map[string]bool AuthMethod() AuthMethod ClientID() string ClientJWK() jwk.Key ClientJWKAlgorithm() jwa.KeyAlgorithm ClientSecret() string DomainHint() string NewClientAuthJWTType() bool PostLogoutRedirectURI() string ResourceIndicator() string Scopes() scopes.Scopes UILocales() string WellKnownURL() string } type client struct { config.OpenID authMethod AuthMethod clientJwk jwk.Key clientJwkAlg jwa.KeyAlgorithm trustedAudiences map[string]bool } var _ Client = (*client)(nil) func (in *client) ACRValues() string { return in.OpenID.ACRValues } func (in *client) Audiences() map[string]bool { return in.trustedAudiences } func (in *client) AuthMethod() AuthMethod { return in.authMethod } func (in *client) ClientID() string { return in.OpenID.ClientID } func (in *client) ClientJWK() jwk.Key { return in.clientJwk } // ClientJWKAlgorithm returns the algorithm declared by the client JWK, or nil // when authenticating with a client secret. NewClientConfig guarantees it is set. func (in *client) ClientJWKAlgorithm() jwa.KeyAlgorithm { return in.clientJwkAlg } func (in *client) ClientSecret() string { return in.OpenID.ClientSecret } func (in *client) DomainHint() string { return in.OpenID.DomainHint } func (in *client) NewClientAuthJWTType() bool { return in.OpenID.NewClientAuthJWTType } func (in *client) PostLogoutRedirectURI() string { return in.OpenID.PostLogoutRedirectURI } func (in *client) ResourceIndicator() string { return in.OpenID.ResourceIndicator } func (in *client) Scopes() scopes.Scopes { return scopes.DefaultScopes().WithAdditional(in.OpenID.Scopes...) } func (in *client) UILocales() string { return in.OpenID.UILocales } func (in *client) WellKnownURL() string { return in.OpenID.WellKnownURL } func NewClientConfig(cfg *config.Config) (Client, error) { c := &client{ OpenID: cfg.OpenID, trustedAudiences: cfg.OpenID.TrustedAudiences(), } if len(cfg.OpenID.ClientJWK) == 0 && len(cfg.OpenID.ClientSecret) == 0 { return nil, fmt.Errorf("missing required config: at least one of %q or %q must be set", config.OpenIDClientJWK, config.OpenIDClientSecret) } if len(cfg.OpenID.ClientSecret) > 0 { c.authMethod = AuthMethodClientSecret } if len(cfg.OpenID.ClientJWK) > 0 { if c.authMethod == AuthMethodClientSecret { log.WithField("logger", "wonderwall.config").Debug("both client JWK and client secret were set; using client JWK...") } clientJwk, err := jwk.ParseKey([]byte(cfg.OpenID.ClientJWK)) if err != nil { return nil, fmt.Errorf("parsing client JWK: %w", err) } alg, ok := clientJwk.Algorithm() if !ok { return nil, fmt.Errorf("client JWK is missing required %q", jwk.AlgorithmKey) } c.clientJwk = clientJwk c.clientJwkAlg = alg c.authMethod = AuthMethodPrivateKeyJWT } var clientConfig Client switch cfg.OpenID.Provider { case config.ProviderIDPorten: clientConfig = c.IDPorten() case config.ProviderAzure: clientConfig = c.Azure() case "": return nil, fmt.Errorf("missing required config %q", config.OpenIDProvider) default: clientConfig = c } if len(clientConfig.ClientID()) == 0 { return nil, fmt.Errorf("missing required config %q", config.OpenIDClientID) } if len(clientConfig.WellKnownURL()) == 0 { return nil, fmt.Errorf("missing required config %q", config.OpenIDWellKnownURL) } return clientConfig, nil } type azure struct { *client } func (in *client) Azure() *azure { return &azure{ client: in, } } func (in *azure) Scopes() scopes.Scopes { return scopes.DefaultScopes(). WithAzureScope(in.OpenID.ClientID). WithOfflineAccess(). WithAdditional(in.OpenID.Scopes...) } type idporten struct { *client } func (in *client) IDPorten() *idporten { return &idporten{ client: in, } }