diff --git a/pkg/genericapiserver/options/server_run_options.go b/pkg/genericapiserver/options/server_run_options.go index 2f2c59559be..ca5e3852abe 100644 --- a/pkg/genericapiserver/options/server_run_options.go +++ b/pkg/genericapiserver/options/server_run_options.go @@ -401,7 +401,7 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { fs.StringVar(&s.OIDCGroupsClaim, "oidc-groups-claim", "", ""+ "If provided, the name of a custom OpenID Connect claim for specifying user groups. "+ - "The claim value is expected to be an array of strings. This flag is experimental, "+ + "The claim value is expected to be a string or array of strings. This flag is experimental, "+ "please see the authentication documentation for further details.") fs.Var(&s.RuntimeConfig, "runtime-config", ""+ diff --git a/plugin/pkg/auth/authenticator/token/oidc/oidc.go b/plugin/pkg/auth/authenticator/token/oidc/oidc.go index 5f04b6e87da..21d38d8f89c 100644 --- a/plugin/pkg/auth/authenticator/token/oidc/oidc.go +++ b/plugin/pkg/auth/authenticator/token/oidc/oidc.go @@ -73,7 +73,7 @@ type OIDCOptions struct { // GroupsClaim, if specified, causes the OIDCAuthenticator to try to populate the user's // groups with a ID Token field. If the GrouppClaim field is present in a ID Token the value - // must be a list of strings. + // must be a string or list of strings. GroupsClaim string } @@ -251,10 +251,14 @@ func (a *OIDCAuthenticator) AuthenticateToken(value string) (user.Info, bool, er if a.groupsClaim != "" { groups, found, err := claims.StringsClaim(a.groupsClaim) if err != nil { - // Custom claim is present, but isn't an array of strings. - return nil, false, fmt.Errorf("custom group claim contains invalid object: %v", err) - } - if found { + // Groups type is present but is not an array of strings, try to decode as a string. + group, _, err := claims.StringClaim(a.groupsClaim) + if err != nil { + // Custom claim is present, but isn't an array of strings or a string. + return nil, false, fmt.Errorf("custom group claim contains invalid type: %T", claims[a.groupsClaim]) + } + info.Groups = []string{group} + } else if found { info.Groups = groups } } diff --git a/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go b/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go index 9195d2420f1..061cd9218cc 100644 --- a/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go +++ b/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go @@ -33,7 +33,7 @@ import ( oidctesting "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/oidc/testing" ) -func generateToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string { +func generateToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}, iat, exp time.Time) string { signer := op.PrivKey.Signer() claims := oidc.NewClaims(iss, sub, aud, iat, exp) claims.Add(usernameClaim, value) @@ -49,15 +49,15 @@ func generateToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud str return jwt.Encode() } -func generateGoodToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string { +func generateGoodToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string { return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) } -func generateMalformedToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string { +func generateMalformedToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string { return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits" } -func generateExpiredToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string { +func generateExpiredToken(t *testing.T, op *oidctesting.OIDCProvider, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups interface{}) string { return generateToken(t, op, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour)) } @@ -239,6 +239,24 @@ func TestOIDCAuthentication(t *testing.T) { true, "", }, + { + // Group claim is a string rather than an array. Map that string to a single group. + "email", + "groups", + generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", "group1"), + &user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1"}}, + true, + "", + }, + { + // Group claim is not a string or array of strings. Throw out this as invalid. + "email", + "groups", + generateGoodToken(t, op, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", 1), + nil, + false, + "custom group claim contains invalid type: float64", + }, { "sub", "",