Merge pull request #123165 from aramase/aramase/f/kep_3331_audience_match_policy
Add `AudienceMatchPolicy` and support multiple audiences in AuthenticationConfiguration
This commit is contained in:
		@@ -180,8 +180,17 @@ type Issuer struct {
 | 
			
		||||
	URL                  string
 | 
			
		||||
	CertificateAuthority string
 | 
			
		||||
	Audiences            []string
 | 
			
		||||
	AudienceMatchPolicy  AudienceMatchPolicyType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy
 | 
			
		||||
type AudienceMatchPolicyType string
 | 
			
		||||
 | 
			
		||||
// Valid types for AudienceMatchPolicyType
 | 
			
		||||
const (
 | 
			
		||||
	AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ClaimValidationRule provides the configuration for a single claim validation rule.
 | 
			
		||||
type ClaimValidationRule struct {
 | 
			
		||||
	Claim         string
 | 
			
		||||
 
 | 
			
		||||
@@ -225,8 +225,32 @@ type Issuer struct {
 | 
			
		||||
	// Required to be non-empty.
 | 
			
		||||
	// +required
 | 
			
		||||
	Audiences []string `json:"audiences"`
 | 
			
		||||
 | 
			
		||||
	// audienceMatchPolicy defines how the "audiences" field is used to match the "aud" claim in the presented JWT.
 | 
			
		||||
	// Allowed values are:
 | 
			
		||||
	// 1. "MatchAny" when multiple audiences are specified and
 | 
			
		||||
	// 2. empty (or unset) or "MatchAny" when a single audience is specified.
 | 
			
		||||
	//
 | 
			
		||||
	// - MatchAny: the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field.
 | 
			
		||||
	// For example, if "audiences" is ["foo", "bar"], the "aud" claim in the presented JWT must contain either "foo" or "bar" (and may contain both).
 | 
			
		||||
	//
 | 
			
		||||
	// - "": The match policy can be empty (or unset) when a single audience is specified in the "audiences" field. The "aud" claim in the presented JWT must contain the single audience (and may contain others).
 | 
			
		||||
	//
 | 
			
		||||
	// For more nuanced audience validation, use claimValidationRules.
 | 
			
		||||
	//   example: claimValidationRule[].expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' to require an exact match.
 | 
			
		||||
	// +optional
 | 
			
		||||
	AudienceMatchPolicy AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy
 | 
			
		||||
type AudienceMatchPolicyType string
 | 
			
		||||
 | 
			
		||||
// Valid types for AudienceMatchPolicyType
 | 
			
		||||
const (
 | 
			
		||||
	// MatchAny means the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field.
 | 
			
		||||
	AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ClaimValidationRule provides the configuration for a single claim validation rule.
 | 
			
		||||
type ClaimValidationRule struct {
 | 
			
		||||
	// claim is the name of a required claim.
 | 
			
		||||
 
 | 
			
		||||
@@ -582,6 +582,7 @@ func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.
 | 
			
		||||
	out.URL = in.URL
 | 
			
		||||
	out.CertificateAuthority = in.CertificateAuthority
 | 
			
		||||
	out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
 | 
			
		||||
	out.AudienceMatchPolicy = apiserver.AudienceMatchPolicyType(in.AudienceMatchPolicy)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -594,6 +595,7 @@ func autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out *
 | 
			
		||||
	out.URL = in.URL
 | 
			
		||||
	out.CertificateAuthority = in.CertificateAuthority
 | 
			
		||||
	out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
 | 
			
		||||
	out.AudienceMatchPolicy = AudienceMatchPolicyType(in.AudienceMatchPolicy)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,7 @@ func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	var allErrs field.ErrorList
 | 
			
		||||
 | 
			
		||||
	allErrs = append(allErrs, validateURL(issuer.URL, fldPath.Child("url"))...)
 | 
			
		||||
	allErrs = append(allErrs, validateAudiences(issuer.Audiences, fldPath.Child("audiences"))...)
 | 
			
		||||
	allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...)
 | 
			
		||||
	allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
 | 
			
		||||
 | 
			
		||||
	return allErrs
 | 
			
		||||
@@ -136,25 +136,31 @@ func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
	return allErrs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateAudiences(audiences []string, fldPath *field.Path) field.ErrorList {
 | 
			
		||||
func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path) field.ErrorList {
 | 
			
		||||
	var allErrs field.ErrorList
 | 
			
		||||
 | 
			
		||||
	if len(audiences) == 0 {
 | 
			
		||||
		allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath)))
 | 
			
		||||
		return allErrs
 | 
			
		||||
	}
 | 
			
		||||
	// This stricter validation is because the --oidc-client-id flag option is singular.
 | 
			
		||||
	// This will be removed when we support multiple audiences with the StructuredAuthenticationConfiguration feature gate.
 | 
			
		||||
	if len(audiences) > 1 {
 | 
			
		||||
		allErrs = append(allErrs, field.TooMany(fldPath, len(audiences), 1))
 | 
			
		||||
		return allErrs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	seenAudiences := sets.NewString()
 | 
			
		||||
	for i, audience := range audiences {
 | 
			
		||||
		fldPath := fldPath.Index(i)
 | 
			
		||||
		if len(audience) == 0 {
 | 
			
		||||
			allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty"))
 | 
			
		||||
		}
 | 
			
		||||
		if seenAudiences.Has(audience) {
 | 
			
		||||
			allErrs = append(allErrs, field.Duplicate(fldPath, audience))
 | 
			
		||||
		}
 | 
			
		||||
		seenAudiences.Insert(audience)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(audiences) > 1 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny {
 | 
			
		||||
		allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be MatchAny for multiple audiences"))
 | 
			
		||||
	}
 | 
			
		||||
	if len(audiences) == 1 && (len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny) {
 | 
			
		||||
		allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return allErrs
 | 
			
		||||
 
 | 
			
		||||
@@ -269,37 +269,63 @@ func TestValidateURL(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestValidateAudiences(t *testing.T) {
 | 
			
		||||
	fldPath := field.NewPath("issuer", "audiences")
 | 
			
		||||
	audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy")
 | 
			
		||||
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		in   []string
 | 
			
		||||
		want string
 | 
			
		||||
		name        string
 | 
			
		||||
		in          []string
 | 
			
		||||
		matchPolicy string
 | 
			
		||||
		want        string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "audiences is empty",
 | 
			
		||||
			in:   []string{},
 | 
			
		||||
			want: "issuer.audiences: Required value: at least one issuer.audiences is required",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "at most one audiences is allowed",
 | 
			
		||||
			in:   []string{"audience1", "audience2"},
 | 
			
		||||
			want: "issuer.audiences: Too many: 2: must have at most 1 items",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "audience is empty",
 | 
			
		||||
			in:   []string{""},
 | 
			
		||||
			want: "issuer.audiences[0]: Required value: audience can't be empty",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "invalid match policy with single audience",
 | 
			
		||||
			in:          []string{"audience"},
 | 
			
		||||
			matchPolicy: "MatchExact",
 | 
			
		||||
			want:        `issuer.audienceMatchPolicy: Invalid value: "MatchExact": audienceMatchPolicy must be empty or MatchAny for single audience`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid audience",
 | 
			
		||||
			in:   []string{"audience"},
 | 
			
		||||
			want: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "valid audience with MatchAny policy",
 | 
			
		||||
			in:          []string{"audience"},
 | 
			
		||||
			matchPolicy: "MatchAny",
 | 
			
		||||
			want:        "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "duplicate audience",
 | 
			
		||||
			in:          []string{"audience", "audience"},
 | 
			
		||||
			matchPolicy: "MatchAny",
 | 
			
		||||
			want:        `issuer.audiences[1]: Duplicate value: "audience"`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "match policy not set with multiple audiences",
 | 
			
		||||
			in:   []string{"audience1", "audience2"},
 | 
			
		||||
			want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "valid multiple audiences",
 | 
			
		||||
			in:          []string{"audience1", "audience2"},
 | 
			
		||||
			matchPolicy: "MatchAny",
 | 
			
		||||
			want:        "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range testCases {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got := validateAudiences(tt.in, fldPath).ToAggregate()
 | 
			
		||||
			got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath).ToAggregate()
 | 
			
		||||
			if d := cmp.Diff(tt.want, errString(got)); d != "" {
 | 
			
		||||
				t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d)
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@ import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/net"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/wait"
 | 
			
		||||
	"k8s.io/apiserver/pkg/apis/apiserver"
 | 
			
		||||
	apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
 | 
			
		||||
@@ -99,12 +100,12 @@ type CAContentProvider interface {
 | 
			
		||||
 | 
			
		||||
// initVerifier creates a new ID token verifier for the given configuration and issuer URL.  On success, calls setVerifier with the
 | 
			
		||||
// resulting verifier.
 | 
			
		||||
func initVerifier(ctx context.Context, config *oidc.Config, iss string) (*oidc.IDTokenVerifier, error) {
 | 
			
		||||
func initVerifier(ctx context.Context, config *oidc.Config, iss string, audiences sets.Set[string]) (*idTokenVerifier, error) {
 | 
			
		||||
	provider, err := oidc.NewProvider(ctx, iss)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("init verifier failed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return provider.Verifier(config), nil
 | 
			
		||||
	return &idTokenVerifier{provider.Verifier(config), audiences}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// asyncIDTokenVerifier is an ID token verifier that allows async initialization
 | 
			
		||||
@@ -115,13 +116,13 @@ type asyncIDTokenVerifier struct {
 | 
			
		||||
	// v is the ID token verifier initialized asynchronously.  It remains nil
 | 
			
		||||
	// up until it is eventually initialized.
 | 
			
		||||
	// Guarded by m
 | 
			
		||||
	v *oidc.IDTokenVerifier
 | 
			
		||||
	v *idTokenVerifier
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAsyncIDTokenVerifier creates a new asynchronous token verifier.  The
 | 
			
		||||
// verifier is available immediately, but may remain uninitialized for some time
 | 
			
		||||
// after creation.
 | 
			
		||||
func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *asyncIDTokenVerifier {
 | 
			
		||||
func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string, audiences sets.Set[string]) *asyncIDTokenVerifier {
 | 
			
		||||
	t := &asyncIDTokenVerifier{}
 | 
			
		||||
 | 
			
		||||
	sync := make(chan struct{})
 | 
			
		||||
@@ -129,7 +130,7 @@ func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *a
 | 
			
		||||
	// verifier, or until context canceled.
 | 
			
		||||
	initFn := func() (done bool, err error) {
 | 
			
		||||
		klog.V(4).Infof("oidc authenticator: attempting init: iss=%v", iss)
 | 
			
		||||
		v, err := initVerifier(ctx, c, iss)
 | 
			
		||||
		v, err := initVerifier(ctx, c, iss, audiences)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			klog.Errorf("oidc authenticator: async token verifier for issuer: %q: %v", iss, err)
 | 
			
		||||
			return false, nil
 | 
			
		||||
@@ -155,7 +156,7 @@ func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *a
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// verifier returns the underlying ID token verifier, or nil if one is not yet initialized.
 | 
			
		||||
func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier {
 | 
			
		||||
func (a *asyncIDTokenVerifier) verifier() *idTokenVerifier {
 | 
			
		||||
	a.m.Lock()
 | 
			
		||||
	defer a.m.Unlock()
 | 
			
		||||
	return a.v
 | 
			
		||||
@@ -181,13 +182,20 @@ type Authenticator struct {
 | 
			
		||||
	requiredClaims map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
 | 
			
		||||
// idTokenVerifier is a wrapper around oidc.IDTokenVerifier. It uses the oidc.IDTokenVerifier
 | 
			
		||||
// to verify the raw ID token and then performs audience validation locally.
 | 
			
		||||
type idTokenVerifier struct {
 | 
			
		||||
	verifier  *oidc.IDTokenVerifier
 | 
			
		||||
	audiences sets.Set[string]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Authenticator) setVerifier(v *idTokenVerifier) {
 | 
			
		||||
	a.verifier.Store(v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Authenticator) idTokenVerifier() (*oidc.IDTokenVerifier, bool) {
 | 
			
		||||
func (a *Authenticator) idTokenVerifier() (*idTokenVerifier, bool) {
 | 
			
		||||
	if v := a.verifier.Load(); v != nil {
 | 
			
		||||
		return v.(*oidc.IDTokenVerifier), true
 | 
			
		||||
		return v.(*idTokenVerifier), true
 | 
			
		||||
	}
 | 
			
		||||
	return nil, false
 | 
			
		||||
}
 | 
			
		||||
@@ -265,16 +273,26 @@ func New(opts Options) (*Authenticator, error) {
 | 
			
		||||
		now = time.Now
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	audiences := sets.New[string](opts.JWTAuthenticator.Issuer.Audiences...)
 | 
			
		||||
	verifierConfig := &oidc.Config{
 | 
			
		||||
		ClientID:             opts.JWTAuthenticator.Issuer.Audiences[0],
 | 
			
		||||
		SupportedSigningAlgs: supportedSigningAlgs,
 | 
			
		||||
		Now:                  now,
 | 
			
		||||
	}
 | 
			
		||||
	if audiences.Len() > 1 {
 | 
			
		||||
		verifierConfig.ClientID = ""
 | 
			
		||||
		// SkipClientIDCheck is set to true because we want to support multiple audiences
 | 
			
		||||
		// in the authentication configuration.
 | 
			
		||||
		// The go oidc library does not support validating
 | 
			
		||||
		// multiple audiences, so we have to skip the client ID check and do it ourselves.
 | 
			
		||||
		// xref: https://github.com/coreos/go-oidc/issues/397
 | 
			
		||||
		verifierConfig.SkipClientIDCheck = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var resolver *claimResolver
 | 
			
		||||
	groupsClaim := opts.JWTAuthenticator.ClaimMappings.Groups.Claim
 | 
			
		||||
	if groupsClaim != "" {
 | 
			
		||||
		resolver = newClaimResolver(groupsClaim, client, verifierConfig)
 | 
			
		||||
		resolver = newClaimResolver(groupsClaim, client, verifierConfig, audiences)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requiredClaims := make(map[string]string)
 | 
			
		||||
@@ -294,7 +312,10 @@ func New(opts Options) (*Authenticator, error) {
 | 
			
		||||
 | 
			
		||||
	if opts.KeySet != nil {
 | 
			
		||||
		// We already have a key set, synchronously initialize the verifier.
 | 
			
		||||
		authenticator.setVerifier(oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig))
 | 
			
		||||
		authenticator.setVerifier(&idTokenVerifier{
 | 
			
		||||
			oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig),
 | 
			
		||||
			audiences,
 | 
			
		||||
		})
 | 
			
		||||
	} else {
 | 
			
		||||
		// Asynchronously attempt to initialize the authenticator. This enables
 | 
			
		||||
		// self-hosted providers, providers that run on top of Kubernetes itself.
 | 
			
		||||
@@ -306,7 +327,7 @@ func New(opts Options) (*Authenticator, error) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			verifier := provider.Verifier(verifierConfig)
 | 
			
		||||
			authenticator.setVerifier(verifier)
 | 
			
		||||
			authenticator.setVerifier(&idTokenVerifier{verifier, audiences})
 | 
			
		||||
			return true, nil
 | 
			
		||||
		}, ctx.Done())
 | 
			
		||||
	}
 | 
			
		||||
@@ -374,6 +395,10 @@ type claimResolver struct {
 | 
			
		||||
	// claim is the distributed claim that may be resolved.
 | 
			
		||||
	claim string
 | 
			
		||||
 | 
			
		||||
	// audiences is the set of acceptable audiences the JWT must be issued to.
 | 
			
		||||
	// At least one of the entries must match the "aud" claim in presented JWTs.
 | 
			
		||||
	audiences sets.Set[string]
 | 
			
		||||
 | 
			
		||||
	// client is the to use for resolving distributed claims
 | 
			
		||||
	client *http.Client
 | 
			
		||||
 | 
			
		||||
@@ -390,19 +415,25 @@ type claimResolver struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newClaimResolver creates a new resolver for distributed claims.
 | 
			
		||||
func newClaimResolver(claim string, client *http.Client, config *oidc.Config) *claimResolver {
 | 
			
		||||
	return &claimResolver{claim: claim, client: client, config: config, verifierPerIssuer: map[string]*asyncIDTokenVerifier{}}
 | 
			
		||||
func newClaimResolver(claim string, client *http.Client, config *oidc.Config, audiences sets.Set[string]) *claimResolver {
 | 
			
		||||
	return &claimResolver{
 | 
			
		||||
		claim:             claim,
 | 
			
		||||
		audiences:         audiences,
 | 
			
		||||
		client:            client,
 | 
			
		||||
		config:            config,
 | 
			
		||||
		verifierPerIssuer: map[string]*asyncIDTokenVerifier{},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verifier returns either the verifier for the specified issuer, or error.
 | 
			
		||||
func (r *claimResolver) Verifier(iss string) (*oidc.IDTokenVerifier, error) {
 | 
			
		||||
func (r *claimResolver) Verifier(iss string) (*idTokenVerifier, error) {
 | 
			
		||||
	r.m.Lock()
 | 
			
		||||
	av := r.verifierPerIssuer[iss]
 | 
			
		||||
	if av == nil {
 | 
			
		||||
		// This lazy init should normally be very quick.
 | 
			
		||||
		// TODO: Make this context cancelable.
 | 
			
		||||
		ctx := oidc.ClientContext(context.Background(), r.client)
 | 
			
		||||
		av = newAsyncIDTokenVerifier(ctx, r.config, iss)
 | 
			
		||||
		av = newAsyncIDTokenVerifier(ctx, r.config, iss, r.audiences)
 | 
			
		||||
		r.verifierPerIssuer[iss] = av
 | 
			
		||||
	}
 | 
			
		||||
	r.m.Unlock()
 | 
			
		||||
@@ -520,6 +551,39 @@ func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaim
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *idTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) {
 | 
			
		||||
	t, err := v.verifier.Verify(ctx, rawIDToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := v.verifyAudience(t); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return t, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// verifyAudience verifies the audience field in the ID token matches the expected audience.
 | 
			
		||||
// This is added based on https://github.com/coreos/go-oidc/blob/b203e58c24394ddf5e816706a7645f01280245c7/oidc/verify.go#L275-L281
 | 
			
		||||
// with the difference that we allow multiple audiences.
 | 
			
		||||
//
 | 
			
		||||
// AuthenticationConfiguration has a audienceMatchPolicy field, but the only supported value now is "MatchAny".
 | 
			
		||||
// So, The default match behavior is to match at least one of the audiences in the ID token.
 | 
			
		||||
func (v *idTokenVerifier) verifyAudience(t *oidc.IDToken) error {
 | 
			
		||||
	// We validate audience field is not empty in the authentication configuration.
 | 
			
		||||
	// This check ensures callers of "Verify" using idTokenVerifier are not passing
 | 
			
		||||
	// an empty audience.
 | 
			
		||||
	if v.audiences.Len() == 0 {
 | 
			
		||||
		return fmt.Errorf("oidc: invalid configuration, audiences cannot be empty")
 | 
			
		||||
	}
 | 
			
		||||
	for _, aud := range t.Audience {
 | 
			
		||||
		if v.audiences.Has(aud) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("oidc: expected audience in %q got %q", sets.List(v.audiences), t.Audience)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
 | 
			
		||||
	if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -1521,6 +1521,70 @@ func TestToken(t *testing.T) {
 | 
			
		||||
				Name: "jane",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple-audiences in authentication config",
 | 
			
		||||
			options: Options{
 | 
			
		||||
				JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
			
		||||
					Issuer: apiserver.Issuer{
 | 
			
		||||
						URL:                 "https://auth.example.com",
 | 
			
		||||
						Audiences:           []string{"random-client", "my-client"},
 | 
			
		||||
						AudienceMatchPolicy: "MatchAny",
 | 
			
		||||
					},
 | 
			
		||||
					ClaimMappings: apiserver.ClaimMappings{
 | 
			
		||||
						Username: apiserver.PrefixedClaimOrExpression{
 | 
			
		||||
							Claim:  "username",
 | 
			
		||||
							Prefix: pointer.String(""),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				now: func() time.Time { return now },
 | 
			
		||||
			},
 | 
			
		||||
			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
			
		||||
			pubKeys: []*jose.JSONWebKey{
 | 
			
		||||
				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
			
		||||
			},
 | 
			
		||||
			claims: fmt.Sprintf(`{
 | 
			
		||||
				"iss": "https://auth.example.com",
 | 
			
		||||
				"aud": ["not-my-client", "my-client"],
 | 
			
		||||
				"azp": "not-my-client",
 | 
			
		||||
				"username": "jane",
 | 
			
		||||
				"exp": %d
 | 
			
		||||
			}`, valid.Unix()),
 | 
			
		||||
			want: &user.DefaultInfo{
 | 
			
		||||
				Name: "jane",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple-audiences in authentication config, no match",
 | 
			
		||||
			options: Options{
 | 
			
		||||
				JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
			
		||||
					Issuer: apiserver.Issuer{
 | 
			
		||||
						URL:                 "https://auth.example.com",
 | 
			
		||||
						Audiences:           []string{"random-client", "my-client"},
 | 
			
		||||
						AudienceMatchPolicy: "MatchAny",
 | 
			
		||||
					},
 | 
			
		||||
					ClaimMappings: apiserver.ClaimMappings{
 | 
			
		||||
						Username: apiserver.PrefixedClaimOrExpression{
 | 
			
		||||
							Claim:  "username",
 | 
			
		||||
							Prefix: pointer.String(""),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				now: func() time.Time { return now },
 | 
			
		||||
			},
 | 
			
		||||
			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
			
		||||
			pubKeys: []*jose.JSONWebKey{
 | 
			
		||||
				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
			
		||||
			},
 | 
			
		||||
			claims: fmt.Sprintf(`{
 | 
			
		||||
				"iss": "https://auth.example.com",
 | 
			
		||||
				"aud": ["not-my-client"],
 | 
			
		||||
				"azp": "not-my-client",
 | 
			
		||||
				"username": "jane",
 | 
			
		||||
				"exp": %d
 | 
			
		||||
			}`, valid.Unix()),
 | 
			
		||||
			wantErr: `oidc: verify token: oidc: expected audience in ["my-client" "random-client"] got ["not-my-client"]`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid-issuer",
 | 
			
		||||
			options: Options{
 | 
			
		||||
 
 | 
			
		||||
@@ -432,6 +432,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
@@ -475,6 +477,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
@@ -522,6 +526,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
@@ -565,6 +571,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
@@ -621,6 +629,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
@@ -668,6 +678,8 @@ jwt:
 | 
			
		||||
    url: %s
 | 
			
		||||
    audiences:
 | 
			
		||||
    - %s
 | 
			
		||||
    - another-audience
 | 
			
		||||
    audienceMatchPolicy: MatchAny
 | 
			
		||||
    certificateAuthority: |
 | 
			
		||||
        %s
 | 
			
		||||
  claimMappings:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user