wiring existing oidc flags with internal API struct
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
		| @@ -19,11 +19,11 @@ package authenticator | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	utilnet "k8s.io/apimachinery/pkg/util/net" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | ||||
| 	"k8s.io/apiserver/pkg/authentication/group" | ||||
| @@ -55,15 +55,8 @@ type Config struct { | ||||
| 	BootstrapToken bool | ||||
|  | ||||
| 	TokenAuthFile               string | ||||
| 	OIDCIssuerURL               string | ||||
| 	OIDCClientID                string | ||||
| 	OIDCCAFile                  string | ||||
| 	OIDCUsernameClaim           string | ||||
| 	OIDCUsernamePrefix          string | ||||
| 	OIDCGroupsClaim             string | ||||
| 	OIDCGroupsPrefix            string | ||||
| 	AuthenticationConfig        *apiserver.AuthenticationConfiguration | ||||
| 	OIDCSigningAlgs             []string | ||||
| 	OIDCRequiredClaims          map[string]string | ||||
| 	ServiceAccountKeyFiles      []string | ||||
| 	ServiceAccountLookup        bool | ||||
| 	ServiceAccountIssuers       []string | ||||
| @@ -153,33 +146,28 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er | ||||
| 	// cache misses for all requests using the other. While the service account plugin | ||||
| 	// simply returns an error, the OpenID Connect plugin may query the provider to | ||||
| 	// update the keys, causing performance hits. | ||||
| 	if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 { | ||||
| 		// TODO(enj): wire up the Notifier and ControllerRunner bits when OIDC supports CA reload | ||||
| 	if config.AuthenticationConfig != nil { | ||||
| 		for _, jwtAuthenticator := range config.AuthenticationConfig.JWT { | ||||
| 			var oidcCAContent oidc.CAContentProvider | ||||
| 		if len(config.OIDCCAFile) != 0 { | ||||
| 			var oidcCAErr error | ||||
| 			oidcCAContent, oidcCAErr = staticCAContentProviderFromFile("oidc-authenticator", config.OIDCCAFile) | ||||
| 			if oidcCAErr != nil { | ||||
| 				return nil, nil, oidcCAErr | ||||
| 			if len(jwtAuthenticator.Issuer.CertificateAuthority) > 0 { | ||||
| 				var oidcCAError error | ||||
| 				oidcCAContent, oidcCAError = dynamiccertificates.NewStaticCAContent("oidc-authenticator", []byte(jwtAuthenticator.Issuer.CertificateAuthority)) | ||||
| 				if oidcCAError != nil { | ||||
| 					return nil, nil, oidcCAError | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 		oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(oidc.Options{ | ||||
| 			IssuerURL:            config.OIDCIssuerURL, | ||||
| 			ClientID:             config.OIDCClientID, | ||||
| 			oidcAuth, err := oidc.New(oidc.Options{ | ||||
| 				JWTAuthenticator:     jwtAuthenticator, | ||||
| 				CAContentProvider:    oidcCAContent, | ||||
| 			UsernameClaim:        config.OIDCUsernameClaim, | ||||
| 			UsernamePrefix:       config.OIDCUsernamePrefix, | ||||
| 			GroupsClaim:          config.OIDCGroupsClaim, | ||||
| 			GroupsPrefix:         config.OIDCGroupsPrefix, | ||||
| 				SupportedSigningAlgs: config.OIDCSigningAlgs, | ||||
| 			RequiredClaims:       config.OIDCRequiredClaims, | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				return nil, nil, err | ||||
| 			} | ||||
| 			tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, oidcAuth)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(config.WebhookTokenAuthnConfigFile) > 0 { | ||||
| 		webhookTokenAuth, err := newWebhookTokenAuthenticator(config) | ||||
| 		if err != nil { | ||||
| @@ -243,31 +231,6 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e | ||||
| 	return tokenAuthenticator, nil | ||||
| } | ||||
|  | ||||
| // newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error. | ||||
| func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token, error) { | ||||
| 	const noUsernamePrefix = "-" | ||||
|  | ||||
| 	if opts.UsernamePrefix == "" && opts.UsernameClaim != "email" { | ||||
| 		// Old behavior. If a usernamePrefix isn't provided, prefix all claims other than "email" | ||||
| 		// with the issuerURL. | ||||
| 		// | ||||
| 		// See https://github.com/kubernetes/kubernetes/issues/31380 | ||||
| 		opts.UsernamePrefix = opts.IssuerURL + "#" | ||||
| 	} | ||||
|  | ||||
| 	if opts.UsernamePrefix == noUsernamePrefix { | ||||
| 		// Special value indicating usernames shouldn't be prefixed. | ||||
| 		opts.UsernamePrefix = "" | ||||
| 	} | ||||
|  | ||||
| 	tokenAuthenticator, err := oidc.New(opts) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return tokenAuthenticator, nil | ||||
| } | ||||
|  | ||||
| // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error | ||||
| func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) { | ||||
| 	allPublicKeys := []interface{}{} | ||||
| @@ -318,12 +281,3 @@ func newWebhookTokenAuthenticator(config Config) (authenticator.Token, error) { | ||||
|  | ||||
| 	return tokencache.New(webhookTokenAuthenticator, false, config.WebhookTokenAuthnCacheTTL, config.WebhookTokenAuthnCacheTTL), nil | ||||
| } | ||||
|  | ||||
| func staticCAContentProviderFromFile(purpose, filename string) (dynamiccertificates.CAContentProvider, error) { | ||||
| 	fileBytes, err := os.ReadFile(filename) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dynamiccertificates.NewStaticCAContent(purpose, fileBytes) | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| @@ -28,6 +29,8 @@ import ( | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"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" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||
| 	genericapiserver "k8s.io/apiserver/pkg/server" | ||||
| 	"k8s.io/apiserver/pkg/server/egressselector" | ||||
| @@ -41,6 +44,7 @@ import ( | ||||
| 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | ||||
| 	authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // BuiltInAuthenticationOptions contains all build-in authentication options for API Server | ||||
| @@ -397,16 +401,68 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if o.OIDC != nil { | ||||
| 		ret.OIDCCAFile = o.OIDC.CAFile | ||||
| 		ret.OIDCClientID = o.OIDC.ClientID | ||||
| 		ret.OIDCGroupsClaim = o.OIDC.GroupsClaim | ||||
| 		ret.OIDCGroupsPrefix = o.OIDC.GroupsPrefix | ||||
| 		ret.OIDCIssuerURL = o.OIDC.IssuerURL | ||||
| 		ret.OIDCUsernameClaim = o.OIDC.UsernameClaim | ||||
| 		ret.OIDCUsernamePrefix = o.OIDC.UsernamePrefix | ||||
| 	if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { | ||||
| 		usernamePrefix := o.OIDC.UsernamePrefix | ||||
|  | ||||
| 		if o.OIDC.UsernamePrefix == "" && o.OIDC.UsernameClaim != "email" { | ||||
| 			// Legacy CLI flag behavior. If a usernamePrefix isn't provided, prefix all claims other than "email" | ||||
| 			// with the issuerURL. | ||||
| 			// | ||||
| 			// See https://github.com/kubernetes/kubernetes/issues/31380 | ||||
| 			usernamePrefix = o.OIDC.IssuerURL + "#" | ||||
| 		} | ||||
| 		if o.OIDC.UsernamePrefix == "-" { | ||||
| 			// Special value indicating usernames shouldn't be prefixed. | ||||
| 			usernamePrefix = "" | ||||
| 		} | ||||
|  | ||||
| 		jwtAuthenticator := apiserver.JWTAuthenticator{ | ||||
| 			Issuer: apiserver.Issuer{ | ||||
| 				URL:       o.OIDC.IssuerURL, | ||||
| 				Audiences: []string{o.OIDC.ClientID}, | ||||
| 			}, | ||||
| 			ClaimMappings: apiserver.ClaimMappings{ | ||||
| 				Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 					Prefix: pointer.String(usernamePrefix), | ||||
| 					Claim:  o.OIDC.UsernameClaim, | ||||
| 				}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		if len(o.OIDC.GroupsClaim) > 0 { | ||||
| 			jwtAuthenticator.ClaimMappings.Groups = apiserver.PrefixedClaimOrExpression{ | ||||
| 				Prefix: pointer.String(o.OIDC.GroupsPrefix), | ||||
| 				Claim:  o.OIDC.GroupsClaim, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(o.OIDC.CAFile) != 0 { | ||||
| 			caContent, err := os.ReadFile(o.OIDC.CAFile) | ||||
| 			if err != nil { | ||||
| 				return kubeauthenticator.Config{}, err | ||||
| 			} | ||||
| 			jwtAuthenticator.Issuer.CertificateAuthority = string(caContent) | ||||
| 		} | ||||
|  | ||||
| 		if len(o.OIDC.RequiredClaims) > 0 { | ||||
| 			claimValidationRules := make([]apiserver.ClaimValidationRule, 0, len(o.OIDC.RequiredClaims)) | ||||
| 			for claim, value := range o.OIDC.RequiredClaims { | ||||
| 				claimValidationRules = append(claimValidationRules, apiserver.ClaimValidationRule{ | ||||
| 					Claim:         claim, | ||||
| 					RequiredValue: value, | ||||
| 				}) | ||||
| 			} | ||||
| 			jwtAuthenticator.ClaimValidationRules = claimValidationRules | ||||
| 		} | ||||
|  | ||||
| 		authConfig := &apiserver.AuthenticationConfiguration{ | ||||
| 			JWT: []apiserver.JWTAuthenticator{jwtAuthenticator}, | ||||
| 		} | ||||
| 		if err := apiservervalidation.ValidateAuthenticationConfiguration(authConfig).ToAggregate(); err != nil { | ||||
| 			return kubeauthenticator.Config{}, err | ||||
| 		} | ||||
| 		ret.AuthenticationConfig = authConfig | ||||
| 		ret.OIDCSigningAlgs = o.OIDC.SigningAlgs | ||||
| 		ret.OIDCRequiredClaims = o.OIDC.RequiredClaims | ||||
| 	} | ||||
|  | ||||
| 	if o.RequestHeader != nil { | ||||
|   | ||||
| @@ -17,6 +17,7 @@ limitations under the License. | ||||
| package options | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| @@ -27,11 +28,13 @@ import ( | ||||
|  | ||||
| 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticatorfactory" | ||||
| 	"k8s.io/apiserver/pkg/authentication/request/headerrequest" | ||||
| 	apiserveroptions "k8s.io/apiserver/pkg/server/options" | ||||
| 	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestAuthenticationValidate(t *testing.T) { | ||||
| @@ -50,7 +53,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -63,7 +66,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| 				Issuers:  []string{"http://foo.bar.com"}, | ||||
| @@ -76,7 +79,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -89,7 +92,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -102,7 +105,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -115,7 +118,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -128,7 +131,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -141,7 +144,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -156,7 +159,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -171,7 +174,7 @@ func TestAuthenticationValidate(t *testing.T) { | ||||
| 			testOIDC: &OIDCAuthenticationOptions{ | ||||
| 				UsernameClaim: "sub", | ||||
| 				SigningAlgs:   []string{"RS256"}, | ||||
| 				IssuerURL:     "testIssuerURL", | ||||
| 				IssuerURL:     "https://testIssuerURL", | ||||
| 				ClientID:      "testClientID", | ||||
| 			}, | ||||
| 			testSA: &ServiceAccountAuthenticationOptions{ | ||||
| @@ -228,10 +231,10 @@ func TestToAuthenticationConfig(t *testing.T) { | ||||
| 			Enable: false, | ||||
| 		}, | ||||
| 		OIDC: &OIDCAuthenticationOptions{ | ||||
| 			CAFile:        "/testCAFile", | ||||
| 			CAFile:        "testdata/root.pem", | ||||
| 			UsernameClaim: "sub", | ||||
| 			SigningAlgs:   []string{"RS256"}, | ||||
| 			IssuerURL:     "testIssuerURL", | ||||
| 			IssuerURL:     "https://testIssuerURL", | ||||
| 			ClientID:      "testClientID", | ||||
| 		}, | ||||
| 		RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{ | ||||
| @@ -258,10 +261,22 @@ func TestToAuthenticationConfig(t *testing.T) { | ||||
| 		BootstrapToken:          false, | ||||
| 		ClientCAContentProvider: nil, // this is nil because you can't compare functions | ||||
| 		TokenAuthFile:           "/testTokenFile", | ||||
| 		OIDCIssuerURL:               "testIssuerURL", | ||||
| 		OIDCClientID:                "testClientID", | ||||
| 		OIDCCAFile:                  "/testCAFile", | ||||
| 		OIDCUsernameClaim:           "sub", | ||||
| 		AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 			JWT: []apiserver.JWTAuthenticator{ | ||||
| 				{ | ||||
| 					Issuer: apiserver.Issuer{ | ||||
| 						URL:       "https://testIssuerURL", | ||||
| 						Audiences: []string{"testClientID"}, | ||||
| 					}, | ||||
| 					ClaimMappings: apiserver.ClaimMappings{ | ||||
| 						Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 							Claim:  "sub", | ||||
| 							Prefix: pointer.String("https://testIssuerURL#"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		OIDCSigningAlgs:             []string{"RS256"}, | ||||
| 		ServiceAccountLookup:        true, | ||||
| 		ServiceAccountIssuers:       []string{"http://foo.bar.com"}, | ||||
| @@ -280,6 +295,12 @@ func TestToAuthenticationConfig(t *testing.T) { | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	fileBytes, err := os.ReadFile("testdata/root.pem") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	expectConfig.AuthenticationConfig.JWT[0].Issuer.CertificateAuthority = string(fileBytes) | ||||
|  | ||||
| 	resultConfig, err := testOptions.ToAuthenticationConfig() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| @@ -385,3 +406,221 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { | ||||
| 		t.Error(cmp.Diff(opts, expected)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestToAuthenticationConfig_OIDC(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name         string | ||||
| 		args         []string | ||||
| 		expectConfig kubeauthenticator.Config | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "username prefix is '-'", | ||||
| 			args: []string{ | ||||
| 				"--oidc-issuer-url=https://testIssuerURL", | ||||
| 				"--oidc-client-id=testClientID", | ||||
| 				"--oidc-username-claim=sub", | ||||
| 				"--oidc-username-prefix=-", | ||||
| 				"--oidc-signing-algs=RS256", | ||||
| 				"--oidc-required-claim=foo=bar", | ||||
| 			}, | ||||
| 			expectConfig: kubeauthenticator.Config{ | ||||
| 				TokenSuccessCacheTTL: 10 * time.Second, | ||||
| 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 					JWT: []apiserver.JWTAuthenticator{ | ||||
| 						{ | ||||
| 							Issuer: apiserver.Issuer{ | ||||
| 								URL:       "https://testIssuerURL", | ||||
| 								Audiences: []string{"testClientID"}, | ||||
| 							}, | ||||
| 							ClaimMappings: apiserver.ClaimMappings{ | ||||
| 								Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "sub", | ||||
| 									Prefix: pointer.String(""), | ||||
| 								}, | ||||
| 							}, | ||||
| 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||
| 								{ | ||||
| 									Claim:         "foo", | ||||
| 									RequiredValue: "bar", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				OIDCSigningAlgs: []string{"RS256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "--oidc-username-prefix is empty, --oidc-username-claim is not email", | ||||
| 			args: []string{ | ||||
| 				"--oidc-issuer-url=https://testIssuerURL", | ||||
| 				"--oidc-client-id=testClientID", | ||||
| 				"--oidc-username-claim=sub", | ||||
| 				"--oidc-signing-algs=RS256", | ||||
| 				"--oidc-required-claim=foo=bar", | ||||
| 			}, | ||||
| 			expectConfig: kubeauthenticator.Config{ | ||||
| 				TokenSuccessCacheTTL: 10 * time.Second, | ||||
| 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 					JWT: []apiserver.JWTAuthenticator{ | ||||
| 						{ | ||||
| 							Issuer: apiserver.Issuer{ | ||||
| 								URL:       "https://testIssuerURL", | ||||
| 								Audiences: []string{"testClientID"}, | ||||
| 							}, | ||||
| 							ClaimMappings: apiserver.ClaimMappings{ | ||||
| 								Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "sub", | ||||
| 									Prefix: pointer.String("https://testIssuerURL#"), | ||||
| 								}, | ||||
| 							}, | ||||
| 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||
| 								{ | ||||
| 									Claim:         "foo", | ||||
| 									RequiredValue: "bar", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				OIDCSigningAlgs: []string{"RS256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "--oidc-username-prefix is empty, --oidc-username-claim is email", | ||||
| 			args: []string{ | ||||
| 				"--oidc-issuer-url=https://testIssuerURL", | ||||
| 				"--oidc-client-id=testClientID", | ||||
| 				"--oidc-username-claim=email", | ||||
| 				"--oidc-signing-algs=RS256", | ||||
| 				"--oidc-required-claim=foo=bar", | ||||
| 			}, | ||||
| 			expectConfig: kubeauthenticator.Config{ | ||||
| 				TokenSuccessCacheTTL: 10 * time.Second, | ||||
| 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 					JWT: []apiserver.JWTAuthenticator{ | ||||
| 						{ | ||||
| 							Issuer: apiserver.Issuer{ | ||||
| 								URL:       "https://testIssuerURL", | ||||
| 								Audiences: []string{"testClientID"}, | ||||
| 							}, | ||||
| 							ClaimMappings: apiserver.ClaimMappings{ | ||||
| 								Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "email", | ||||
| 									Prefix: pointer.String(""), | ||||
| 								}, | ||||
| 							}, | ||||
| 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||
| 								{ | ||||
| 									Claim:         "foo", | ||||
| 									RequiredValue: "bar", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				OIDCSigningAlgs: []string{"RS256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "non empty username prefix", | ||||
| 			args: []string{ | ||||
| 				"--oidc-issuer-url=https://testIssuerURL", | ||||
| 				"--oidc-client-id=testClientID", | ||||
| 				"--oidc-username-claim=sub", | ||||
| 				"--oidc-username-prefix=k8s-", | ||||
| 				"--oidc-signing-algs=RS256", | ||||
| 				"--oidc-required-claim=foo=bar", | ||||
| 			}, | ||||
| 			expectConfig: kubeauthenticator.Config{ | ||||
| 				TokenSuccessCacheTTL: 10 * time.Second, | ||||
| 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 					JWT: []apiserver.JWTAuthenticator{ | ||||
| 						{ | ||||
| 							Issuer: apiserver.Issuer{ | ||||
| 								URL:       "https://testIssuerURL", | ||||
| 								Audiences: []string{"testClientID"}, | ||||
| 							}, | ||||
| 							ClaimMappings: apiserver.ClaimMappings{ | ||||
| 								Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "sub", | ||||
| 									Prefix: pointer.String("k8s-"), | ||||
| 								}, | ||||
| 							}, | ||||
| 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||
| 								{ | ||||
| 									Claim:         "foo", | ||||
| 									RequiredValue: "bar", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				OIDCSigningAlgs: []string{"RS256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "groups claim exists", | ||||
| 			args: []string{ | ||||
| 				"--oidc-issuer-url=https://testIssuerURL", | ||||
| 				"--oidc-client-id=testClientID", | ||||
| 				"--oidc-username-claim=sub", | ||||
| 				"--oidc-username-prefix=-", | ||||
| 				"--oidc-groups-claim=groups", | ||||
| 				"--oidc-groups-prefix=oidc:", | ||||
| 				"--oidc-signing-algs=RS256", | ||||
| 				"--oidc-required-claim=foo=bar", | ||||
| 			}, | ||||
| 			expectConfig: kubeauthenticator.Config{ | ||||
| 				TokenSuccessCacheTTL: 10 * time.Second, | ||||
| 				AuthenticationConfig: &apiserver.AuthenticationConfiguration{ | ||||
| 					JWT: []apiserver.JWTAuthenticator{ | ||||
| 						{ | ||||
| 							Issuer: apiserver.Issuer{ | ||||
| 								URL:       "https://testIssuerURL", | ||||
| 								Audiences: []string{"testClientID"}, | ||||
| 							}, | ||||
| 							ClaimMappings: apiserver.ClaimMappings{ | ||||
| 								Username: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "sub", | ||||
| 									Prefix: pointer.String(""), | ||||
| 								}, | ||||
| 								Groups: apiserver.PrefixedClaimOrExpression{ | ||||
| 									Claim:  "groups", | ||||
| 									Prefix: pointer.String("oidc:"), | ||||
| 								}, | ||||
| 							}, | ||||
| 							ClaimValidationRules: []apiserver.ClaimValidationRule{ | ||||
| 								{ | ||||
| 									Claim:         "foo", | ||||
| 									RequiredValue: "bar", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				OIDCSigningAlgs: []string{"RS256"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, testcase := range testCases { | ||||
| 		t.Run(testcase.name, func(t *testing.T) { | ||||
| 			opts := NewBuiltInAuthenticationOptions().WithOIDC() | ||||
| 			pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError) | ||||
| 			opts.AddFlags(pf) | ||||
|  | ||||
| 			if err := pf.Parse(testcase.args); err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			resultConfig, err := opts.ToAuthenticationConfig() | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(resultConfig, testcase.expectConfig) { | ||||
| 				t.Error(cmp.Diff(resultConfig, testcase.expectConfig)) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,204 @@ | ||||
| /* | ||||
| Copyright 2023 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validation | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	api "k8s.io/apiserver/pkg/apis/apiserver" | ||||
| 	"k8s.io/client-go/util/cert" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	atLeastOneRequiredErrFmt = "at least one %s is required" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	root = field.NewPath("jwt") | ||||
| ) | ||||
|  | ||||
| // ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration. | ||||
| func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	// This stricter validation is solely based on what the current implementation supports. | ||||
| 	// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up, | ||||
| 	// relax this check to allow 0 authenticators. This will allow us to support the case where | ||||
| 	// API server is initially configured with no authenticators and then authenticators are added | ||||
| 	// later via dynamic config. | ||||
| 	if len(c.JWT) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root))) | ||||
| 		return allErrs | ||||
| 	} | ||||
|  | ||||
| 	// This stricter validation is because the --oidc-* flag option is singular. | ||||
| 	// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up, | ||||
| 	// remove the 1 authenticator limit check and add set the limit to 64. | ||||
| 	if len(c.JWT) > 1 { | ||||
| 		allErrs = append(allErrs, field.TooMany(root, len(c.JWT), 1)) | ||||
| 		return allErrs | ||||
| 	} | ||||
|  | ||||
| 	// TODO(aramase): right now we only support a single JWT authenticator as | ||||
| 	// this is wired to the --oidc-* flags. When StructuredAuthenticationConfiguration | ||||
| 	// feature gate is added and wired up, we will remove the 1 authenticator limit | ||||
| 	// check and add validation for duplicate issuers. | ||||
| 	for i, a := range c.JWT { | ||||
| 		fldPath := root.Index(i) | ||||
| 		allErrs = append(allErrs, validateJWTAuthenticator(a, fldPath)...) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| // ValidateJWTAuthenticator validates a given JWTAuthenticator. | ||||
| // This is exported for use in oidc package. | ||||
| func ValidateJWTAuthenticator(authenticator api.JWTAuthenticator) field.ErrorList { | ||||
| 	return validateJWTAuthenticator(authenticator, nil) | ||||
| } | ||||
|  | ||||
| func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...) | ||||
| 	allErrs = append(allErrs, validateClaimValidationRules(authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"))...) | ||||
| 	allErrs = append(allErrs, validateClaimMappings(authenticator.ClaimMappings, fldPath.Child("claimMappings"))...) | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| 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, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...) | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	if len(issuerURL) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath, "URL is required")) | ||||
| 		return allErrs | ||||
| 	} | ||||
|  | ||||
| 	u, err := url.Parse(issuerURL) | ||||
| 	if err != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error())) | ||||
| 		return allErrs | ||||
| 	} | ||||
| 	if u.Scheme != "https" { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL scheme must be https")) | ||||
| 	} | ||||
| 	if u.User != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a username or password")) | ||||
| 	} | ||||
| 	if len(u.RawQuery) > 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a query")) | ||||
| 	} | ||||
| 	if len(u.Fragment) > 0 { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a fragment")) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateAudiences(audiences []string, fldPath *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 | ||||
| 	} | ||||
|  | ||||
| 	for i, audience := range audiences { | ||||
| 		fldPath := fldPath.Index(i) | ||||
| 		if len(audience) == 0 { | ||||
| 			allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty")) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateCertificateAuthority(certificateAuthority string, fldPath *field.Path) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	if len(certificateAuthority) == 0 { | ||||
| 		return allErrs | ||||
| 	} | ||||
| 	_, err := cert.NewPoolFromBytes([]byte(certificateAuthority)) | ||||
| 	if err != nil { | ||||
| 		allErrs = append(allErrs, field.Invalid(fldPath, "<omitted>", err.Error())) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateClaimValidationRules(rules []api.ClaimValidationRule, fldPath *field.Path) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	seenClaims := sets.NewString() | ||||
| 	for i, rule := range rules { | ||||
| 		fldPath := fldPath.Index(i) | ||||
|  | ||||
| 		if len(rule.Claim) == 0 { | ||||
| 			allErrs = append(allErrs, field.Required(fldPath.Child("claim"), "claim name is required")) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if seenClaims.Has(rule.Claim) { | ||||
| 			allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim)) | ||||
| 			continue | ||||
| 		} | ||||
| 		seenClaims.Insert(rule.Claim) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| func validateClaimMappings(m api.ClaimMappings, fldPath *field.Path) field.ErrorList { | ||||
| 	var allErrs field.ErrorList | ||||
|  | ||||
| 	if len(m.Username.Claim) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("username", "claim"), "claim name is required")) | ||||
| 	} | ||||
| 	// TODO(aramase): when Expression is added to PrefixedClaimOrExpression, check prefix and expression are not both set. | ||||
| 	if m.Username.Prefix == nil { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("username", "prefix"), "prefix is required")) | ||||
| 	} | ||||
| 	if len(m.Groups.Claim) > 0 && m.Groups.Prefix == nil { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("groups", "prefix"), "prefix is required when claim is set")) | ||||
| 	} | ||||
| 	if m.Groups.Prefix != nil && len(m.Groups.Claim) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath.Child("groups", "claim"), "non-empty claim name is required when prefix is set")) | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
| @@ -0,0 +1,414 @@ | ||||
| /* | ||||
| Copyright 2023 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validation | ||||
|  | ||||
| import ( | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/elliptic" | ||||
| 	"crypto/rand" | ||||
| 	"encoding/pem" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/errors" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	api "k8s.io/apiserver/pkg/apis/apiserver" | ||||
| 	certutil "k8s.io/client-go/util/cert" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestValidateAuthenticationConfiguration(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   *api.AuthenticationConfiguration | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "jwt authenticator is empty", | ||||
| 			in:   &api.AuthenticationConfiguration{}, | ||||
| 			want: "jwt: Required value: at least one jwt is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: ">1 jwt authenticator", | ||||
| 			in: &api.AuthenticationConfiguration{ | ||||
| 				JWT: []api.JWTAuthenticator{ | ||||
| 					{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}}, | ||||
| 					{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: "jwt: Too many: 2: must have at most 1 items", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "failed issuer validation", | ||||
| 			in: &api.AuthenticationConfiguration{ | ||||
| 				JWT: []api.JWTAuthenticator{ | ||||
| 					{ | ||||
| 						Issuer: api.Issuer{ | ||||
| 							URL:       "invalid-url", | ||||
| 							Audiences: []string{"audience"}, | ||||
| 						}, | ||||
| 						ClaimMappings: api.ClaimMappings{ | ||||
| 							Username: api.PrefixedClaimOrExpression{ | ||||
| 								Claim:  "claim", | ||||
| 								Prefix: pointer.String("prefix"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "failed claimValidationRule validation", | ||||
| 			in: &api.AuthenticationConfiguration{ | ||||
| 				JWT: []api.JWTAuthenticator{ | ||||
| 					{ | ||||
| 						Issuer: api.Issuer{ | ||||
| 							URL:       "https://issuer-url", | ||||
| 							Audiences: []string{"audience"}, | ||||
| 						}, | ||||
| 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||
| 							{ | ||||
| 								Claim:         "foo", | ||||
| 								RequiredValue: "bar", | ||||
| 							}, | ||||
| 							{ | ||||
| 								Claim:         "foo", | ||||
| 								RequiredValue: "baz", | ||||
| 							}, | ||||
| 						}, | ||||
| 						ClaimMappings: api.ClaimMappings{ | ||||
| 							Username: api.PrefixedClaimOrExpression{ | ||||
| 								Claim:  "claim", | ||||
| 								Prefix: pointer.String("prefix"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "failed claimMapping validation", | ||||
| 			in: &api.AuthenticationConfiguration{ | ||||
| 				JWT: []api.JWTAuthenticator{ | ||||
| 					{ | ||||
| 						Issuer: api.Issuer{ | ||||
| 							URL:       "https://issuer-url", | ||||
| 							Audiences: []string{"audience"}, | ||||
| 						}, | ||||
| 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||
| 							{ | ||||
| 								Claim:         "foo", | ||||
| 								RequiredValue: "bar", | ||||
| 							}, | ||||
| 						}, | ||||
| 						ClaimMappings: api.ClaimMappings{ | ||||
| 							Username: api.PrefixedClaimOrExpression{ | ||||
| 								Prefix: pointer.String("prefix"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: "jwt[0].claimMappings.username.claim: Required value: claim name is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid authentication configuration", | ||||
| 			in: &api.AuthenticationConfiguration{ | ||||
| 				JWT: []api.JWTAuthenticator{ | ||||
| 					{ | ||||
| 						Issuer: api.Issuer{ | ||||
| 							URL:       "https://issuer-url", | ||||
| 							Audiences: []string{"audience"}, | ||||
| 						}, | ||||
| 						ClaimValidationRules: []api.ClaimValidationRule{ | ||||
| 							{ | ||||
| 								Claim:         "foo", | ||||
| 								RequiredValue: "bar", | ||||
| 							}, | ||||
| 						}, | ||||
| 						ClaimMappings: api.ClaimMappings{ | ||||
| 							Username: api.PrefixedClaimOrExpression{ | ||||
| 								Claim:  "sub", | ||||
| 								Prefix: pointer.String("prefix"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := ValidateAuthenticationConfiguration(tt.in).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateURL(t *testing.T) { | ||||
| 	fldPath := field.NewPath("issuer", "url") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   string | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "url is empty", | ||||
| 			in:   "", | ||||
| 			want: "issuer.url: Required value: URL is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "url parse error", | ||||
| 			in:   "https://issuer-url:invalid-port", | ||||
| 			want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "url is not https", | ||||
| 			in:   "http://issuer-url", | ||||
| 			want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "url user info is not allowed", | ||||
| 			in:   "https://user:pass@issuer-url", | ||||
| 			want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "url raw query is not allowed", | ||||
| 			in:   "https://issuer-url?query", | ||||
| 			want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "url fragment is not allowed", | ||||
| 			in:   "https://issuer-url#fragment", | ||||
| 			want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid url", | ||||
| 			in:   "https://issuer-url", | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := validateURL(tt.in, fldPath).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("URL validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateAudiences(t *testing.T) { | ||||
| 	fldPath := field.NewPath("issuer", "audiences") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   []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: "valid audience", | ||||
| 			in:   []string{"audience"}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := validateAudiences(tt.in, fldPath).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateCertificateAuthority(t *testing.T) { | ||||
| 	fldPath := field.NewPath("issuer", "certificateAuthority") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   func() string | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "invalid certificate authority", | ||||
| 			in:   func() string { return "invalid" }, | ||||
| 			want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "certificate authority is empty", | ||||
| 			in:   func() string { return "" }, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid certificate authority", | ||||
| 			in: func() string { | ||||
| 				caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||||
| 				if err != nil { | ||||
| 					t.Fatal(err) | ||||
| 				} | ||||
| 				caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) | ||||
| 				if err != nil { | ||||
| 					t.Fatal(err) | ||||
| 				} | ||||
| 				return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})) | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestClaimValidationRules(t *testing.T) { | ||||
| 	fldPath := field.NewPath("issuer", "claimValidationRules") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   []api.ClaimValidationRule | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "claim validation rule claim is empty", | ||||
| 			in:   []api.ClaimValidationRule{{Claim: ""}}, | ||||
| 			want: "issuer.claimValidationRules[0].claim: Required value: claim name is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "duplicate claim", | ||||
| 			in: []api.ClaimValidationRule{{ | ||||
| 				Claim: "claim", RequiredValue: "value1"}, | ||||
| 				{Claim: "claim", RequiredValue: "value2"}, | ||||
| 			}, | ||||
| 			want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid claim validation rule", | ||||
| 			in:   []api.ClaimValidationRule{{Claim: "claim", RequiredValue: "value"}}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid claim validation rule with multiple rules", | ||||
| 			in: []api.ClaimValidationRule{ | ||||
| 				{Claim: "claim1", RequiredValue: "value1"}, | ||||
| 				{Claim: "claim2", RequiredValue: "value2"}, | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := validateClaimValidationRules(tt.in, fldPath).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateClaimMappings(t *testing.T) { | ||||
| 	fldPath := field.NewPath("issuer", "claimMappings") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name string | ||||
| 		in   api.ClaimMappings | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "username claim is empty", | ||||
| 			in:   api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "", Prefix: pointer.String("prefix")}}, | ||||
| 			want: "issuer.claimMappings.username.claim: Required value: claim name is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "username prefix is empty", | ||||
| 			in:   api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "claim"}}, | ||||
| 			want: "issuer.claimMappings.username.prefix: Required value: prefix is required", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "groups prefix is empty", | ||||
| 			in: api.ClaimMappings{ | ||||
| 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||
| 				Groups:   api.PrefixedClaimOrExpression{Claim: "claim"}, | ||||
| 			}, | ||||
| 			want: "issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "groups prefix set but claim is empty", | ||||
| 			in: api.ClaimMappings{ | ||||
| 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||
| 				Groups:   api.PrefixedClaimOrExpression{Prefix: pointer.String("prefix")}, | ||||
| 			}, | ||||
| 			want: "issuer.claimMappings.groups.claim: Required value: non-empty claim name is required when prefix is set", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid claim mappings", | ||||
| 			in: api.ClaimMappings{ | ||||
| 				Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||
| 				Groups:   api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")}, | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := validateClaimMappings(tt.in, fldPath).ToAggregate() | ||||
| 			if d := cmp.Diff(tt.want, errString(got)); d != "" { | ||||
| 				t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func errString(errs errors.Aggregate) string { | ||||
| 	if errs != nil { | ||||
| 		return errs.Error() | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| @@ -32,11 +32,9 @@ import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| @@ -46,6 +44,8 @@ import ( | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/net" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/apiserver/pkg/apis/apiserver" | ||||
| 	apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation" | ||||
| 	"k8s.io/apiserver/pkg/authentication/authenticator" | ||||
| 	"k8s.io/apiserver/pkg/authentication/user" | ||||
| 	certutil "k8s.io/client-go/util/cert" | ||||
| @@ -59,50 +59,17 @@ var ( | ||||
| ) | ||||
|  | ||||
| type Options struct { | ||||
| 	// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss" | ||||
| 	// field of all tokens produced by the provider and is used for configuration | ||||
| 	// discovery. | ||||
| 	// | ||||
| 	// The URL is usually the provider's URL without a path, for example | ||||
| 	// "https://accounts.google.com" or "https://login.salesforce.com". | ||||
| 	// | ||||
| 	// The provider must implement configuration discovery. | ||||
| 	// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig | ||||
| 	IssuerURL string | ||||
|  | ||||
| 	// JWTAuthenticator is the authenticator that will be used to verify the JWT. | ||||
| 	JWTAuthenticator apiserver.JWTAuthenticator | ||||
| 	// Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer. | ||||
| 	KeySet oidc.KeySet | ||||
|  | ||||
| 	// ClientID the JWT must be issued for, the "sub" field. This plugin only trusts a single | ||||
| 	// client to ensure the plugin can be used with public providers. | ||||
| 	// | ||||
| 	// The plugin supports the "authorized party" OpenID Connect claim, which allows | ||||
| 	// specialized providers to issue tokens to a client for a different client. | ||||
| 	// See: https://openid.net/specs/openid-connect-core-1_0.html#IDToken | ||||
| 	ClientID string | ||||
|  | ||||
| 	// PEM encoded root certificate contents of the provider.  Mutually exclusive with Client. | ||||
| 	CAContentProvider CAContentProvider | ||||
|  | ||||
| 	// Optional http.Client used to make all requests to the remote issuer.  Mutually exclusive with CAContentProvider. | ||||
| 	Client *http.Client | ||||
|  | ||||
| 	// UsernameClaim is the JWT field to use as the user's username. | ||||
| 	UsernameClaim string | ||||
|  | ||||
| 	// UsernamePrefix, if specified, causes claims mapping to username to be prefix with | ||||
| 	// the provided value. A value "oidc:" would result in usernames like "oidc:john". | ||||
| 	UsernamePrefix string | ||||
|  | ||||
| 	// GroupsClaim, if specified, causes the OIDCAuthenticator to try to populate the user's | ||||
| 	// groups with an ID Token field. If the GroupsClaim field is present in an ID Token the value | ||||
| 	// must be a string or list of strings. | ||||
| 	GroupsClaim string | ||||
|  | ||||
| 	// GroupsPrefix, if specified, causes claims mapping to group names to be prefixed with the | ||||
| 	// value. A value "oidc:" would result in groups like "oidc:engineering" and "oidc:marketing". | ||||
| 	GroupsPrefix string | ||||
|  | ||||
| 	// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that | ||||
| 	// can be used by the provider to sign tokens. | ||||
| 	// | ||||
| @@ -114,10 +81,6 @@ type Options struct { | ||||
| 	// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation | ||||
| 	SupportedSigningAlgs []string | ||||
|  | ||||
| 	// RequiredClaims, if specified, causes the OIDCAuthenticator to verify that all the | ||||
| 	// required claims key value pairs are present in the ID Token. | ||||
| 	RequiredClaims map[string]string | ||||
|  | ||||
| 	// now is used for testing. It defaults to time.Now. | ||||
| 	now func() time.Time | ||||
| } | ||||
| @@ -192,13 +155,7 @@ func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier { | ||||
| } | ||||
|  | ||||
| type Authenticator struct { | ||||
| 	issuerURL string | ||||
|  | ||||
| 	usernameClaim  string | ||||
| 	usernamePrefix string | ||||
| 	groupsClaim    string | ||||
| 	groupsPrefix   string | ||||
| 	requiredClaims map[string]string | ||||
| 	jwtAuthenticator apiserver.JWTAuthenticator | ||||
|  | ||||
| 	// Contains an *oidc.IDTokenVerifier. Do not access directly use the | ||||
| 	// idTokenVerifier method. | ||||
| @@ -240,19 +197,10 @@ var allowedSigningAlgs = map[string]bool{ | ||||
| } | ||||
|  | ||||
| func New(opts Options) (*Authenticator, error) { | ||||
| 	url, err := url.Parse(opts.IssuerURL) | ||||
| 	if err != nil { | ||||
| 	if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if url.Scheme != "https" { | ||||
| 		return nil, fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", opts.IssuerURL, url.Scheme) | ||||
| 	} | ||||
|  | ||||
| 	if opts.UsernameClaim == "" { | ||||
| 		return nil, errors.New("no username claim provided") | ||||
| 	} | ||||
|  | ||||
| 	supportedSigningAlgs := opts.SupportedSigningAlgs | ||||
| 	if len(supportedSigningAlgs) == 0 { | ||||
| 		// RS256 is the default recommended by OpenID Connect and an 'alg' value | ||||
| @@ -273,6 +221,7 @@ func New(opts Options) (*Authenticator, error) { | ||||
|  | ||||
| 	if client == nil { | ||||
| 		var roots *x509.CertPool | ||||
| 		var err error | ||||
| 		if opts.CAContentProvider != nil { | ||||
| 			// TODO(enj): make this reload CA data dynamically | ||||
| 			roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent()) | ||||
| @@ -302,35 +251,30 @@ func New(opts Options) (*Authenticator, error) { | ||||
| 	} | ||||
|  | ||||
| 	verifierConfig := &oidc.Config{ | ||||
| 		ClientID:             opts.ClientID, | ||||
| 		ClientID:             opts.JWTAuthenticator.Issuer.Audiences[0], | ||||
| 		SupportedSigningAlgs: supportedSigningAlgs, | ||||
| 		Now:                  now, | ||||
| 	} | ||||
|  | ||||
| 	var resolver *claimResolver | ||||
| 	if opts.GroupsClaim != "" { | ||||
| 		resolver = newClaimResolver(opts.GroupsClaim, client, verifierConfig) | ||||
| 	if opts.JWTAuthenticator.ClaimMappings.Groups.Claim != "" { | ||||
| 		resolver = newClaimResolver(opts.JWTAuthenticator.ClaimMappings.Groups.Claim, client, verifierConfig) | ||||
| 	} | ||||
|  | ||||
| 	authenticator := &Authenticator{ | ||||
| 		issuerURL:      opts.IssuerURL, | ||||
| 		usernameClaim:  opts.UsernameClaim, | ||||
| 		usernamePrefix: opts.UsernamePrefix, | ||||
| 		groupsClaim:    opts.GroupsClaim, | ||||
| 		groupsPrefix:   opts.GroupsPrefix, | ||||
| 		requiredClaims: opts.RequiredClaims, | ||||
| 		jwtAuthenticator: opts.JWTAuthenticator, | ||||
| 		cancel:           cancel, | ||||
| 		resolver:         resolver, | ||||
| 	} | ||||
|  | ||||
| 	if opts.KeySet != nil { | ||||
| 		// We already have a key set, synchronously initialize the verifier. | ||||
| 		authenticator.setVerifier(oidc.NewVerifier(opts.IssuerURL, opts.KeySet, verifierConfig)) | ||||
| 		authenticator.setVerifier(oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig)) | ||||
| 	} else { | ||||
| 		// Asynchronously attempt to initialize the authenticator. This enables | ||||
| 		// self-hosted providers, providers that run on top of Kubernetes itself. | ||||
| 		go wait.PollImmediateUntil(10*time.Second, func() (done bool, err error) { | ||||
| 			provider, err := oidc.NewProvider(ctx, opts.IssuerURL) | ||||
| 			provider, err := oidc.NewProvider(ctx, opts.JWTAuthenticator.Issuer.URL) | ||||
| 			if err != nil { | ||||
| 				klog.Errorf("oidc authenticator: initializing plugin: %v", err) | ||||
| 				return false, nil | ||||
| @@ -552,7 +496,7 @@ func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaim | ||||
| } | ||||
|  | ||||
| func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { | ||||
| 	if !hasCorrectIssuer(a.issuerURL, token) { | ||||
| 	if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) { | ||||
| 		return nil, false, nil | ||||
| 	} | ||||
|  | ||||
| @@ -577,11 +521,11 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a | ||||
| 	} | ||||
|  | ||||
| 	var username string | ||||
| 	if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil { | ||||
| 		return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err) | ||||
| 	if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Username.Claim, &username); err != nil { | ||||
| 		return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.jwtAuthenticator.ClaimMappings.Username.Claim, err) | ||||
| 	} | ||||
|  | ||||
| 	if a.usernameClaim == "email" { | ||||
| 	if a.jwtAuthenticator.ClaimMappings.Username.Claim == "email" { | ||||
| 		// If the email_verified claim is present, ensure the email is valid. | ||||
| 		// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims | ||||
| 		if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified { | ||||
| @@ -597,33 +541,36 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if a.usernamePrefix != "" { | ||||
| 		username = a.usernamePrefix + username | ||||
| 	if a.jwtAuthenticator.ClaimMappings.Username.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Username.Prefix != "" { | ||||
| 		username = *a.jwtAuthenticator.ClaimMappings.Username.Prefix + username | ||||
| 	} | ||||
|  | ||||
| 	info := &user.DefaultInfo{Name: username} | ||||
| 	if a.groupsClaim != "" { | ||||
| 		if _, ok := c[a.groupsClaim]; ok { | ||||
| 	if a.jwtAuthenticator.ClaimMappings.Groups.Claim != "" { | ||||
| 		if _, ok := c[a.jwtAuthenticator.ClaimMappings.Groups.Claim]; ok { | ||||
| 			// Some admins want to use string claims like "role" as the group value. | ||||
| 			// Allow the group claim to be a single string instead of an array. | ||||
| 			// | ||||
| 			// See: https://github.com/kubernetes/kubernetes/issues/33290 | ||||
| 			var groups stringOrArray | ||||
| 			if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil { | ||||
| 				return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err) | ||||
| 			if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Groups.Claim, &groups); err != nil { | ||||
| 				return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.jwtAuthenticator.ClaimMappings.Groups.Claim, err) | ||||
| 			} | ||||
| 			info.Groups = []string(groups) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if a.groupsPrefix != "" { | ||||
| 	if a.jwtAuthenticator.ClaimMappings.Groups.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Groups.Prefix != "" { | ||||
| 		for i, group := range info.Groups { | ||||
| 			info.Groups[i] = a.groupsPrefix + group | ||||
| 			info.Groups[i] = *a.jwtAuthenticator.ClaimMappings.Groups.Prefix + group | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check to ensure all required claims are present in the ID token and have matching values. | ||||
| 	for claim, value := range a.requiredClaims { | ||||
| 	for _, claimValidationRule := range a.jwtAuthenticator.ClaimValidationRules { | ||||
| 		claim := claimValidationRule.Claim | ||||
| 		value := claimValidationRule.RequiredValue | ||||
|  | ||||
| 		if !c.hasClaim(claim) { | ||||
| 			return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim) | ||||
| 		} | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -1455,6 +1455,7 @@ k8s.io/apiserver/pkg/apis/apiserver/install | ||||
| k8s.io/apiserver/pkg/apis/apiserver/v1 | ||||
| k8s.io/apiserver/pkg/apis/apiserver/v1alpha1 | ||||
| k8s.io/apiserver/pkg/apis/apiserver/v1beta1 | ||||
| k8s.io/apiserver/pkg/apis/apiserver/validation | ||||
| k8s.io/apiserver/pkg/apis/audit | ||||
| k8s.io/apiserver/pkg/apis/audit/install | ||||
| k8s.io/apiserver/pkg/apis/audit/v1 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Anish Ramasekar
					Anish Ramasekar