Skip to content

Commit 92d37d5

Browse files
author
Eric Chiang
committed
plugin/pkg/auth/authenticator/token/oidc: get groups from custom claim
1 parent bd67b8a commit 92d37d5

File tree

8 files changed

+80
-31
lines changed

8 files changed

+80
-31
lines changed

cmd/kube-apiserver/app/options/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type APIServer struct {
7171
OIDCClientID string
7272
OIDCIssuerURL string
7373
OIDCUsernameClaim string
74+
OIDCGroupsClaim string
7475
RuntimeConfig util.ConfigurationMap
7576
SSHKeyfile string
7677
SSHUser string
@@ -165,6 +166,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
165166
fs.StringVar(&s.OIDCUsernameClaim, "oidc-username-claim", "sub", ""+
166167
"The OpenID claim to use as the user name. Note that claims other than the default ('sub') is not "+
167168
"guaranteed to be unique and immutable. This flag is experimental, please see the authentication documentation for further details.")
169+
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, please see the authentication documentation for further details.")
168170
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
169171
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
170172
fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")

cmd/kube-apiserver/app/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ func Run(s *options.APIServer) error {
322322
OIDCClientID: s.OIDCClientID,
323323
OIDCCAFile: s.OIDCCAFile,
324324
OIDCUsernameClaim: s.OIDCUsernameClaim,
325+
OIDCGroupsClaim: s.OIDCGroupsClaim,
325326
ServiceAccountKeyFile: s.ServiceAccountKeyFile,
326327
ServiceAccountLookup: s.ServiceAccountLookup,
327328
ServiceAccountTokenGetter: serviceAccountGetter,

docs/admin/authentication.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ to the OpenID provider.
6767
- `--oidc-username-claim` (optional, experimental) specifies which OpenID claim to use as the user name. By default, `sub`
6868
will be used, which should be unique and immutable under the issuer's domain. Cluster administrator can
6969
choose other claims such as `email` to use as the user name, but the uniqueness and immutability is not guaranteed.
70+
- `--oidc-groups-claim` (optional, experimental) the name of a custom OpenID Connect claim for specifying user groups. The claim
71+
value is expected to be an array of strings.
7072

7173
Please note that this flag is still experimental until we settle more on how to handle the mapping of the OpenID user to the Kubernetes user. Thus further changes are possible.
7274

docs/admin/kube-apiserver.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ kube-apiserver
8989
--min-request-timeout=1800: An optional field indicating the minimum number of seconds a handler must keep a request open before timing it out. Currently only honored by the watch request handler, which picks a randomized value above this number as the connection timeout, to spread out load.
9090
--oidc-ca-file="": If set, the OpenID server's certificate will be verified by one of the authorities in the oidc-ca-file, otherwise the host's root CA set will be used
9191
--oidc-client-id="": The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set
92+
--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, please see the authentication documentation for further details.
9293
--oidc-issuer-url="": The URL of the OpenID issuer, only HTTPS scheme will be accepted. If set, it will be used to verify the OIDC JSON Web Token (JWT)
9394
--oidc-username-claim="sub": The OpenID claim to use as the user name. Note that claims other than the default ('sub') is not guaranteed to be unique and immutable. This flag is experimental, please see the authentication documentation for further details.
9495
--profiling[=true]: Enable profiling via web interface host:port/debug/pprof/
@@ -109,7 +110,7 @@ kube-apiserver
109110
--watch-cache-sizes=[]: List of watch cache sizes for every resource (pods, nodes, etc.), comma separated. The individual override format: resource#size, where size is a number. It takes effect when watch-cache is enabled.
110111
```
111112

112-
###### Auto generated by spf13/cobra on 5-Feb-2016
113+
###### Auto generated by spf13/cobra on 10-Feb-2016
113114

114115

115116
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->

hack/verify-flags/known-flags.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ num-nodes
249249
oidc-ca-file
250250
oidc-client-id
251251
oidc-issuer-url
252+
oidc-groups-claim
252253
oidc-username-claim
253254
only-idl
254255
oom-score-adj

pkg/apiserver/authenticator/authn.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type AuthenticatorConfig struct {
4040
OIDCClientID string
4141
OIDCCAFile string
4242
OIDCUsernameClaim string
43+
OIDCGroupsClaim string
4344
ServiceAccountKeyFile string
4445
ServiceAccountLookup bool
4546
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
@@ -76,7 +77,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) {
7677
}
7778

7879
if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 {
79-
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim)
80+
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCGroupsClaim)
8081
if err != nil {
8182
return nil, err
8283
}
@@ -136,8 +137,8 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
136137
}
137138

138139
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Request or an error.
139-
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim string) (authenticator.Request, error) {
140-
tokenAuthenticator, err := oidc.New(issuerURL, clientID, caFile, usernameClaim)
140+
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (authenticator.Request, error) {
141+
tokenAuthenticator, err := oidc.New(issuerURL, clientID, caFile, usernameClaim, groupsClaim)
141142
if err != nil {
142143
return nil, err
143144
}

plugin/pkg/auth/authenticator/token/oidc/oidc.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ type OIDCAuthenticator struct {
4242
clientConfig oidc.ClientConfig
4343
client *oidc.Client
4444
usernameClaim string
45+
groupsClaim string
4546
stopSyncProvider chan struct{}
4647
}
4748

4849
// New creates a new OpenID Connect client with the given issuerURL and clientID.
4950
// NOTE(yifan): For now we assume the server provides the "jwks_uri" so we don't
5051
// need to manager the key sets by ourselves.
51-
func New(issuerURL, clientID, caFile, usernameClaim string) (*OIDCAuthenticator, error) {
52+
func New(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (*OIDCAuthenticator, error) {
5253
var cfg oidc.ProviderConfig
5354
var err error
5455
var roots *x509.CertPool
@@ -117,7 +118,7 @@ func New(issuerURL, clientID, caFile, usernameClaim string) (*OIDCAuthenticator,
117118
// and maximum threshold.
118119
stop := client.SyncProviderConfig(issuerURL)
119120

120-
return &OIDCAuthenticator{ccfg, client, usernameClaim, stop}, nil
121+
return &OIDCAuthenticator{ccfg, client, usernameClaim, groupsClaim, stop}, nil
121122
}
122123

123124
// AuthenticateToken decodes and verifies a JWT using the OIDC client, if the verification succeeds,
@@ -155,8 +156,20 @@ func (a *OIDCAuthenticator) AuthenticateToken(value string) (user.Info, bool, er
155156
username = fmt.Sprintf("%s#%s", a.clientConfig.ProviderConfig.Issuer, claim)
156157
}
157158

158-
// TODO(yifan): Add UID and Group, also populate the issuer to upper layer.
159-
return &user.DefaultInfo{Name: username}, true, nil
159+
// TODO(yifan): Add UID, also populate the issuer to upper layer.
160+
info := &user.DefaultInfo{Name: username}
161+
162+
if a.groupsClaim != "" {
163+
groups, found, err := claims.StringsClaim(a.groupsClaim)
164+
if err != nil {
165+
// Custom claim is present, but isn't an array of strings.
166+
return nil, false, fmt.Errorf("custom group claim contains invalid object: %v", err)
167+
}
168+
if found {
169+
info.Groups = groups
170+
}
171+
}
172+
return info, true, nil
160173
}
161174

162175
// Close closes the OIDC authenticator, this will close the provider sync goroutine.

plugin/pkg/auth/authenticator/token/oidc/oidc_test.go

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@ func (op *oidcProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
9999
w.Write(b)
100100
}
101101

102-
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value string, iat, exp time.Time) string {
102+
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string {
103103
signer := op.privKey.Signer()
104104
claims := oidc.NewClaims(iss, sub, aud, iat, exp)
105105
claims.Add(usernameClaim, value)
106+
if groups != nil && groupsClaim != "" {
107+
claims.Add(groupsClaim, groups)
108+
}
106109

107110
jwt, err := jose.NewSignedJWT(claims, signer)
108111
if err != nil {
@@ -112,16 +115,16 @@ func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, userna
112115
return jwt.Encode()
113116
}
114117

115-
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
116-
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now(), time.Now().Add(time.Hour))
118+
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
119+
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
117120
}
118121

119-
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
120-
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now(), time.Now().Add(time.Hour)) + "randombits"
122+
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
123+
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
121124
}
122125

123-
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
124-
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
126+
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
127+
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
125128
}
126129

127130
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
@@ -192,7 +195,7 @@ func TestOIDCDiscoveryTimeout(t *testing.T) {
192195
retryBackoff = time.Second
193196
expectErr := fmt.Errorf("failed to fetch provider config after 3 retries")
194197

195-
_, err := New("https://foo/bar", "client-foo", "", "sub")
198+
_, err := New("https://foo/bar", "client-foo", "", "sub", "")
196199
if !reflect.DeepEqual(err, expectErr) {
197200
t.Errorf("Expecting %v, but got %v", expectErr, err)
198201
}
@@ -224,7 +227,7 @@ func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) {
224227
Issuer: srv.URL,
225228
}
226229

227-
_, err = New(srv.URL, "client-foo", cert, "sub")
230+
_, err = New(srv.URL, "client-foo", cert, "sub", "")
228231
if !reflect.DeepEqual(err, expectErr) {
229232
t.Errorf("Expecting %v, but got %v", expectErr, err)
230233
}
@@ -247,7 +250,7 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) {
247250

248251
expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http")
249252

250-
_, err := New(srv.URL, "client-foo", "", "sub")
253+
_, err := New(srv.URL, "client-foo", "", "sub", "")
251254
if !reflect.DeepEqual(err, expectErr) {
252255
t.Errorf("Expecting %v, but got %v", expectErr, err)
253256
}
@@ -282,7 +285,7 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) {
282285
}
283286

284287
// Create a client using cert2, should fail.
285-
_, err = New(tlsSrv.URL, "client-foo", cert2, "sub")
288+
_, err = New(tlsSrv.URL, "client-foo", cert2, "sub", "")
286289
if err == nil {
287290
t.Fatalf("Expecting error, but got nothing")
288291
}
@@ -317,61 +320,86 @@ func TestOIDCAuthentication(t *testing.T) {
317320
}
318321

319322
tests := []struct {
320-
userClaim string
321-
token string
322-
userInfo user.Info
323-
verified bool
324-
err string
323+
userClaim string
324+
groupsClaim string
325+
token string
326+
userInfo user.Info
327+
verified bool
328+
err string
325329
}{
326330
{
327331
"sub",
328-
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
332+
"",
333+
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
329334
&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
330335
true,
331336
"",
332337
},
333338
{
334339
// Use user defined claim (email here).
335340
"email",
336-
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "[email protected]"),
341+
"",
342+
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "[email protected]", "", nil),
343+
&user.DefaultInfo{Name: "[email protected]"},
344+
true,
345+
"",
346+
},
347+
{
348+
// Use user defined claim (email here).
349+
"email",
350+
"",
351+
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "[email protected]", "groups", []string{"group1", "group2"}),
337352
&user.DefaultInfo{Name: "[email protected]"},
338353
true,
339354
"",
340355
},
356+
{
357+
// Use user defined claim (email here).
358+
"email",
359+
"groups",
360+
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "[email protected]", "groups", []string{"group1", "group2"}),
361+
&user.DefaultInfo{Name: "[email protected]", Groups: []string{"group1", "group2"}},
362+
true,
363+
"",
364+
},
341365
{
342366
"sub",
343-
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
367+
"",
368+
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
344369
nil,
345370
false,
346371
"malformed JWS, unable to decode signature",
347372
},
348373
{
349374
// Invalid 'aud'.
350375
"sub",
351-
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo"),
376+
"",
377+
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
352378
nil,
353379
false,
354380
"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
355381
},
356382
{
357383
// Invalid issuer.
358384
"sub",
359-
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo"),
385+
"",
386+
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
360387
nil,
361388
false,
362389
"oidc: JWT claims invalid: invalid claim value: 'iss'.",
363390
},
364391
{
365392
"sub",
366-
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
393+
"",
394+
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
367395
nil,
368396
false,
369397
"oidc: JWT claims invalid: token is expired",
370398
},
371399
}
372400

373401
for i, tt := range tests {
374-
client, err := New(srv.URL, "client-foo", cert, tt.userClaim)
402+
client, err := New(srv.URL, "client-foo", cert, tt.userClaim, tt.groupsClaim)
375403
if err != nil {
376404
t.Fatalf("Unexpected error: %v", err)
377405
}

0 commit comments

Comments
 (0)