diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go index 9a859103cf9..74b1aa002e2 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go @@ -242,7 +242,7 @@ type Issuer struct { AudienceMatchPolicy AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"` } -// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy +// AudienceMatchPolicyType is a set of valid values for issuer.audienceMatchPolicy type AudienceMatchPolicyType string // Valid types for AudienceMatchPolicyType diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go index 01a0538c1fe..eb0d3dd6de7 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go @@ -219,6 +219,7 @@ func validateClaimValidationRules(compiler authenticationcel.Compiler, celMapper compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimValidationCondition{ Expression: rule.Expression, + Message: rule.Message, }, fldPath.Child("expression")) if err != nil { diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go index 79ef5c92076..0b1f682c530 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go @@ -578,10 +578,8 @@ func (v *idTokenVerifier) verifyAudience(t *oidc.IDToken) error { 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 - } + if v.audiences.HasAny(t.Audience...) { + return nil } return fmt.Errorf("oidc: expected audience in %q got %q", sets.List(v.audiences), t.Audience) diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go index 1cdc2831bee..55184ff99b7 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go @@ -1554,6 +1554,39 @@ func TestToken(t *testing.T) { Name: "jane", }, }, + { + name: "multiple-audiences in authentication config, multiple matches", + options: Options{ + JWTAuthenticator: apiserver.JWTAuthenticator{ + Issuer: apiserver.Issuer{ + URL: "https://auth.example.com", + Audiences: []string{"random-client", "my-client", "other-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", "other-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{ @@ -1585,6 +1618,82 @@ func TestToken(t *testing.T) { }`, valid.Unix()), wantErr: `oidc: verify token: oidc: expected audience in ["my-client" "random-client"] got ["not-my-client"]`, }, + { + name: "nuanced audience validation using claim validation rules", + options: Options{ + JWTAuthenticator: apiserver.JWTAuthenticator{ + Issuer: apiserver.Issuer{ + URL: "https://auth.example.com", + Audiences: []string{"bar", "foo", "baz"}, + AudienceMatchPolicy: "MatchAny", + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: pointer.String(""), + }, + }, + ClaimValidationRules: []apiserver.ClaimValidationRule{ + { + Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`, + Message: "audience must exactly contain [bar, foo, baz]", + }, + }, + }, + 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": ["foo", "bar", "baz"], + "azp": "not-my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "audience validation using claim validation rules fails", + options: Options{ + JWTAuthenticator: apiserver.JWTAuthenticator{ + Issuer: apiserver.Issuer{ + URL: "https://auth.example.com", + Audiences: []string{"bar", "foo", "baz"}, + AudienceMatchPolicy: "MatchAny", + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: pointer.String(""), + }, + }, + ClaimValidationRules: []apiserver.ClaimValidationRule{ + { + Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`, + Message: "audience must exactly contain [bar, foo, baz]", + }, + }, + }, + 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": ["foo", "baz"], + "azp": "not-my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + wantErr: `oidc: error evaluating claim validation expression: validation expression 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' failed: audience must exactly contain [bar, foo, baz]`, + }, { name: "invalid-issuer", options: Options{ diff --git a/test/integration/apiserver/oidc/oidc_test.go b/test/integration/apiserver/oidc/oidc_test.go index ea6da90cad9..f7725cd68f0 100644 --- a/test/integration/apiserver/oidc/oidc_test.go +++ b/test/integration/apiserver/oidc/oidc_test.go @@ -761,6 +761,58 @@ jwt: }, wantUser: nil, }, + { + name: "multiple audiences check with claim validation rule is ok", + authConfigFn: func(t *testing.T, issuerURL, caCert string) string { + return fmt.Sprintf(` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: %s + audiences: + - baz + - foo + audienceMatchPolicy: MatchAny + certificateAuthority: | + %s + claimMappings: + username: + expression: "'k8s-' + claims.sub" + uid: + expression: "claims.uid" + claimValidationRules: + - expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' + message: 'aud claim must be exactly match list ["bar", "foo", "baz"]' +`, issuerURL, indentCertificateAuthority(caCert)) + }, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], + configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { + idTokenLifetime := time.Second * 1200 + oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( + t, + signingPrivateKey, + map[string]interface{}{ + "iss": oidcServer.URL(), + "sub": defaultOIDCClaimedUsername, + "aud": []string{"foo", "bar", "baz"}, + "exp": time.Now().Add(idTokenLifetime).Unix(), + "uid": "1234", + }, + defaultStubAccessToken, + defaultStubRefreshToken, + )) + }, + configureClient: configureClientFetchingOIDCCredentials, + assertErrFn: func(t *testing.T, errorToCheck error) { + assert.NoError(t, errorToCheck) + }, + wantUser: &authenticationv1.UserInfo{ + Username: "k8s-john_doe", + Groups: []string{"system:authenticated"}, + UID: "1234", + }, + }, } for _, tt := range tests {