mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-19 02:56:15 +00:00
feat(openid/config): require and expose the client JWK algorithm
Both the assertion signer and the provider validation derived the algorithm from the key and had to handle a missing "alg" that NewClientConfig already rejects. Validate it once at construction and keep the result.
This commit is contained in:
@@ -34,7 +34,7 @@ The following flags are available:
|
||||
| `openid.acr-values` | string | | Space separated string that configures the default security level (`acr_values`) parameter for authorization requests. |
|
||||
| `openid.audiences` | strings | | List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation. |
|
||||
| `openid.client-id` | string | | Client ID for the OpenID client. |
|
||||
| `openid.client-jwk` | string | | JWK containing the private key for the OpenID client in string format. If configured, this takes precedence over `openid.client-secret`. |
|
||||
| `openid.client-jwk` | string | | JWK containing the private key for the OpenID client in string format. Must declare the `alg` header. If configured, this takes precedence over `openid.client-secret`. |
|
||||
| `openid.client-secret` | string | | Client secret for the OpenID client. Overridden by `openid.client-jwk`, if configured. |
|
||||
| `openid.domain-hint` | string | | Domain hint to include in authorization request for IdPs that support this parameter (e.g. Entra ID). |
|
||||
| `openid.jwks-fallback-alg` | string | `RS256` | JWA value (as defined in RFC 7518) to assign to provider JWKS keys when their `alg` header is not set. |
|
||||
|
||||
@@ -75,7 +75,7 @@ func openidFlags() {
|
||||
flag.String(OpenIDACRValues, "", "Space separated string that configures the default security level (acr_values) parameter for authorization requests.")
|
||||
flag.StringSlice(OpenIDAudiences, []string{}, "List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation.")
|
||||
flag.String(OpenIDClientID, "", "Client ID for the OpenID client.")
|
||||
flag.String(OpenIDClientJWK, "", "JWK containing the private key for the OpenID client in string format. If configured, this takes precedence over 'openid.client-secret'.")
|
||||
flag.String(OpenIDClientJWK, "", "JWK containing the private key for the OpenID client in string format. Must declare the 'alg' header. If configured, this takes precedence over 'openid.client-secret'.")
|
||||
flag.String(OpenIDClientSecret, "", "Client secret for the OpenID client. Overridden by 'openid.client-jwk', if configured.")
|
||||
flag.String(OpenIDDomainHint, "", "Domain hint to include in authorization request for IdPs that support this parameter (e.g. Entra ID).")
|
||||
flag.String(OpenIDJWKSFallbackAlg, jwa.RS256().String(), "JWA value (as defined in RFC 7518) to assign to provider JWKS keys when their 'alg' header is not set.")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/nais/wonderwall/internal/crypto"
|
||||
"github.com/nais/wonderwall/pkg/config"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
type TestClientConfiguration struct {
|
||||
*config.Config
|
||||
clientJwk jwk.Key
|
||||
clientJwkAlg jwa.KeyAlgorithm
|
||||
trustedAudiences map[string]bool
|
||||
}
|
||||
|
||||
@@ -36,6 +38,10 @@ func (c *TestClientConfiguration) ClientJWK() jwk.Key {
|
||||
return c.clientJwk
|
||||
}
|
||||
|
||||
func (c *TestClientConfiguration) ClientJWKAlgorithm() jwa.KeyAlgorithm {
|
||||
return c.clientJwkAlg
|
||||
}
|
||||
|
||||
func (c *TestClientConfiguration) ClientSecret() string {
|
||||
return c.OpenID.ClientSecret
|
||||
}
|
||||
@@ -78,9 +84,15 @@ func clientConfiguration(cfg *config.Config) *TestClientConfiguration {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alg, ok := key.Algorithm()
|
||||
if !ok {
|
||||
panic("test client JWK is missing an algorithm")
|
||||
}
|
||||
|
||||
return &TestClientConfiguration{
|
||||
Config: cfg,
|
||||
clientJwk: key,
|
||||
clientJwkAlg: alg,
|
||||
trustedAudiences: cfg.OpenID.TrustedAudiences(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +184,7 @@ func (c *Client) ClientAuthenticationAssertion(expiration time.Duration) (string
|
||||
return "", fmt.Errorf("building client assertion: %w", err)
|
||||
}
|
||||
|
||||
alg, ok := key.Algorithm()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing algorithm on client key")
|
||||
}
|
||||
alg := clientCfg.ClientJWKAlgorithm()
|
||||
|
||||
opts := make([]jwt.Option, 0)
|
||||
if c.cfg.Client().NewClientAuthJWTType() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@@ -23,6 +24,7 @@ type Client interface {
|
||||
AuthMethod() AuthMethod
|
||||
ClientID() string
|
||||
ClientJWK() jwk.Key
|
||||
ClientJWKAlgorithm() jwa.KeyAlgorithm
|
||||
ClientSecret() string
|
||||
DomainHint() string
|
||||
NewClientAuthJWTType() bool
|
||||
@@ -37,6 +39,7 @@ type client struct {
|
||||
config.OpenID
|
||||
authMethod AuthMethod
|
||||
clientJwk jwk.Key
|
||||
clientJwkAlg jwa.KeyAlgorithm
|
||||
trustedAudiences map[string]bool
|
||||
}
|
||||
|
||||
@@ -62,6 +65,12 @@ 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
|
||||
}
|
||||
@@ -117,8 +126,13 @@ func NewClientConfig(cfg *config.Config) (Client, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/nais/wonderwall/internal/crypto"
|
||||
"github.com/nais/wonderwall/pkg/config"
|
||||
openidconfig "github.com/nais/wonderwall/pkg/openid/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewClientConfigRequiresClientJWKAlgorithm(t *testing.T) {
|
||||
key, err := crypto.NewJwk()
|
||||
require.NoError(t, err)
|
||||
|
||||
rawKey, err := json.Marshal(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
var keyFields map[string]any
|
||||
require.NoError(t, json.Unmarshal(rawKey, &keyFields))
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
removeAlg bool
|
||||
wantConfig bool
|
||||
}{
|
||||
{name: "algorithm is present", wantConfig: true},
|
||||
{name: "algorithm is missing", removeAlg: true},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fields := make(map[string]any, len(keyFields))
|
||||
for name, value := range keyFields {
|
||||
fields[name] = value
|
||||
}
|
||||
if tt.removeAlg {
|
||||
delete(fields, "alg")
|
||||
}
|
||||
|
||||
clientJWK, err := json.Marshal(fields)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{OpenID: config.OpenID{
|
||||
ClientID: "client-id",
|
||||
ClientJWK: string(clientJWK),
|
||||
Provider: config.ProviderOpenID,
|
||||
WellKnownURL: "https://issuer.example/.well-known/openid-configuration",
|
||||
}}
|
||||
|
||||
client, err := openidconfig.NewClientConfig(cfg)
|
||||
if tt.wantConfig {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
require.Equal(t, jwa.RS256(), client.ClientJWKAlgorithm())
|
||||
return
|
||||
}
|
||||
|
||||
require.ErrorContains(t, err, "client JWK is missing required")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user