mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-19 11:06:17 +00:00
feat(openid/config): validate client assertion alg against provider metadata
Fail at startup rather than on the first token request when the identity provider does not accept the client assertion algorithm. The check is skipped when the provider omits the field, which is optional in Discovery.
This commit is contained in:
@@ -30,7 +30,7 @@ func NewConfig(ctx context.Context, cfg *wonderwallconfig.Config) (Config, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providerCfg, err := NewProviderConfig(ctx, cfg)
|
||||
providerCfg, err := NewProviderConfig(ctx, cfg, clientCfg.ClientJWKAlgorithm())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (p *provider) SidClaimRequired() bool {
|
||||
// wellKnownTimeout bounds the fetch of the provider's metadata document at startup.
|
||||
const wellKnownTimeout = 10 * time.Second
|
||||
|
||||
func NewProviderConfig(ctx context.Context, cfg *config.Config) (Provider, error) {
|
||||
func NewProviderConfig(ctx context.Context, cfg *config.Config, clientAssertionAlg jwa.KeyAlgorithm) (Provider, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, wellKnownTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -115,7 +115,7 @@ func NewProviderConfig(ctx context.Context, cfg *config.Config) (Provider, error
|
||||
return nil, fmt.Errorf("decoding well known configuration: %w", err)
|
||||
}
|
||||
|
||||
err = providerCfg.Validate(cfg.OpenID)
|
||||
err = providerCfg.Validate(cfg.OpenID, clientAssertionAlg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating well known configuration: %w", err)
|
||||
}
|
||||
@@ -163,6 +163,7 @@ type ProviderMetadata struct {
|
||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported"`
|
||||
UILocalesSupported Supported `json:"ui_locales_supported"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint"`
|
||||
}
|
||||
@@ -172,7 +173,7 @@ func (c *ProviderMetadata) Print() {
|
||||
Debugf("openid provider config: %+v", c)
|
||||
}
|
||||
|
||||
func (c *ProviderMetadata) Validate(cfg config.OpenID) error {
|
||||
func (c *ProviderMetadata) Validate(cfg config.OpenID, clientAssertionAlg jwa.KeyAlgorithm) error {
|
||||
err := c.validateAcrValues(cfg.ACRValues)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -188,7 +189,7 @@ func (c *ProviderMetadata) Validate(cfg config.OpenID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.validateClientAssertionSigningAlg(clientAssertionAlg)
|
||||
}
|
||||
|
||||
func (c *ProviderMetadata) validateAcrValues(acrValue string) error {
|
||||
@@ -221,6 +222,18 @@ func (c *ProviderMetadata) validateJWKSFallbackAlg(algorithm string) error {
|
||||
return fmt.Errorf("identity provider does not support '%s=%s', must be one of %s", config.OpenIDJWKSFallbackAlg, algorithm, c.IDTokenSigningAlgValuesSupported)
|
||||
}
|
||||
|
||||
func (c *ProviderMetadata) validateClientAssertionSigningAlg(algorithm jwa.KeyAlgorithm) error {
|
||||
if len(c.TokenEndpointAuthSigningAlgValuesSupported) == 0 || algorithm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if slices.Contains(c.TokenEndpointAuthSigningAlgValuesSupported, algorithm.String()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("identity provider does not support client assertion signing algorithm %q, must be one of %s", algorithm, c.TokenEndpointAuthSigningAlgValuesSupported)
|
||||
}
|
||||
|
||||
type Supported []string
|
||||
|
||||
func (in Supported) Contains(value string) bool {
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestNewProviderConfig_NonOK(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.OpenID.WellKnownURL = server.URL
|
||||
|
||||
_, err := openidconfig.NewProviderConfig(context.Background(), cfg)
|
||||
_, err := openidconfig.NewProviderConfig(context.Background(), cfg, nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "responded with HTTP")
|
||||
}
|
||||
@@ -37,6 +37,6 @@ func TestNewProviderConfig_CancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err := openidconfig.NewProviderConfig(ctx, cfg)
|
||||
_, err := openidconfig.NewProviderConfig(ctx, cfg, nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ package config_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/nais/wonderwall/internal/crypto"
|
||||
"github.com/nais/wonderwall/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
openidconfig "github.com/nais/wonderwall/pkg/openid/config"
|
||||
@@ -65,8 +68,43 @@ func TestProviderMetadata_Validate(t *testing.T) {
|
||||
cfg.OpenID.JWKSFallbackAlg = tt.config.JWKSFallbackAlg
|
||||
}
|
||||
|
||||
err := metadata.Validate(cfg.OpenID)
|
||||
err := metadata.Validate(cfg.OpenID, nil)
|
||||
tt.assertion(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderMetadata_ValidateClientAssertionSigningAlg(t *testing.T) {
|
||||
key, err := crypto.NewJwk()
|
||||
require.NoError(t, err)
|
||||
algorithm, ok := key.Algorithm()
|
||||
require.True(t, ok)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
supported []string
|
||||
clientAlg jwa.KeyAlgorithm
|
||||
assertion assert.ErrorAssertionFunc
|
||||
}{
|
||||
{name: "supported", supported: []string{"RS256"}, clientAlg: algorithm, assertion: assert.NoError},
|
||||
{name: "unsupported", supported: []string{"PS256"}, clientAlg: algorithm, assertion: func(t assert.TestingT, err error, msgAndArgs ...any) bool {
|
||||
return assert.ErrorContains(t, err, "does not support client assertion signing algorithm", msgAndArgs...)
|
||||
}},
|
||||
{name: "metadata field absent", clientAlg: algorithm, assertion: assert.NoError},
|
||||
// client_secret has no assertion alg; PS256 proves the check is skipped, not passed.
|
||||
{name: "no client assertion algorithm", supported: []string{"PS256"}, assertion: assert.NoError},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// acr and locale pass on empty input, so zeroing them isolates this test.
|
||||
cfg := mock.Config()
|
||||
cfg.OpenID.ACRValues = ""
|
||||
cfg.OpenID.UILocales = ""
|
||||
|
||||
metadata := &openidconfig.ProviderMetadata{
|
||||
IDTokenSigningAlgValuesSupported: openidconfig.Supported{cfg.OpenID.JWKSFallbackAlg},
|
||||
TokenEndpointAuthSigningAlgValuesSupported: tt.supported,
|
||||
}
|
||||
tt.assertion(t, metadata.Validate(cfg.OpenID, tt.clientAlg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user