Merge pull request #21001 from ericchiang/oidc_groups

Auto commit by PR queue bot
This commit is contained in:
k8s-merge-robot 2016-02-14 05:24:43 -08:00
commit 43fb544a4a
8 changed files with 80 additions and 31 deletions

View File

@ -71,6 +71,7 @@ type APIServer struct {
OIDCClientID string
OIDCIssuerURL string
OIDCUsernameClaim string
OIDCGroupsClaim string
RuntimeConfig util.ConfigurationMap
SSHKeyfile string
SSHUser string
@ -165,6 +166,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.OIDCUsernameClaim, "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.")
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.")
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.")
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")

View File

@ -322,6 +322,7 @@ func Run(s *options.APIServer) error {
OIDCClientID: s.OIDCClientID,
OIDCCAFile: s.OIDCCAFile,
OIDCUsernameClaim: s.OIDCUsernameClaim,
OIDCGroupsClaim: s.OIDCGroupsClaim,
ServiceAccountKeyFile: s.ServiceAccountKeyFile,
ServiceAccountLookup: s.ServiceAccountLookup,
ServiceAccountTokenGetter: serviceAccountGetter,

View File

@ -67,6 +67,8 @@ to the OpenID provider.
- `--oidc-username-claim` (optional, experimental) specifies which OpenID claim to use as the user name. By default, `sub`
will be used, which should be unique and immutable under the issuer's domain. Cluster administrator can
choose other claims such as `email` to use as the user name, but the uniqueness and immutability is not guaranteed.
- `--oidc-groups-claim` (optional, experimental) the name of a custom OpenID Connect claim for specifying user groups. The claim
value is expected to be an array of strings.
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.

View File

@ -89,6 +89,7 @@ kube-apiserver
--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.
--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
--oidc-client-id="": The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set
--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.
--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)
--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.
--profiling[=true]: Enable profiling via web interface host:port/debug/pprof/
@ -109,7 +110,7 @@ kube-apiserver
--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.
```
###### Auto generated by spf13/cobra on 5-Feb-2016
###### Auto generated by spf13/cobra on 10-Feb-2016
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->

View File

@ -252,6 +252,7 @@ num-nodes
oidc-ca-file
oidc-client-id
oidc-issuer-url
oidc-groups-claim
oidc-username-claim
only-idl
oom-score-adj

View File

@ -40,6 +40,7 @@ type AuthenticatorConfig struct {
OIDCClientID string
OIDCCAFile string
OIDCUsernameClaim string
OIDCGroupsClaim string
ServiceAccountKeyFile string
ServiceAccountLookup bool
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
@ -76,7 +77,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) {
}
if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 {
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim)
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCGroupsClaim)
if err != nil {
return nil, err
}
@ -136,8 +137,8 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
}
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Request or an error.
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim string) (authenticator.Request, error) {
tokenAuthenticator, err := oidc.New(issuerURL, clientID, caFile, usernameClaim)
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (authenticator.Request, error) {
tokenAuthenticator, err := oidc.New(issuerURL, clientID, caFile, usernameClaim, groupsClaim)
if err != nil {
return nil, err
}

View File

@ -42,13 +42,14 @@ type OIDCAuthenticator struct {
clientConfig oidc.ClientConfig
client *oidc.Client
usernameClaim string
groupsClaim string
stopSyncProvider chan struct{}
}
// New creates a new OpenID Connect client with the given issuerURL and clientID.
// NOTE(yifan): For now we assume the server provides the "jwks_uri" so we don't
// need to manager the key sets by ourselves.
func New(issuerURL, clientID, caFile, usernameClaim string) (*OIDCAuthenticator, error) {
func New(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (*OIDCAuthenticator, error) {
var cfg oidc.ProviderConfig
var err error
var roots *x509.CertPool
@ -117,7 +118,7 @@ func New(issuerURL, clientID, caFile, usernameClaim string) (*OIDCAuthenticator,
// and maximum threshold.
stop := client.SyncProviderConfig(issuerURL)
return &OIDCAuthenticator{ccfg, client, usernameClaim, stop}, nil
return &OIDCAuthenticator{ccfg, client, usernameClaim, groupsClaim, stop}, nil
}
// 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
username = fmt.Sprintf("%s#%s", a.clientConfig.ProviderConfig.Issuer, claim)
}
// TODO(yifan): Add UID and Group, also populate the issuer to upper layer.
return &user.DefaultInfo{Name: username}, true, nil
// TODO(yifan): Add UID, also populate the issuer to upper layer.
info := &user.DefaultInfo{Name: username}
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 {
info.Groups = groups
}
}
return info, true, nil
}
// Close closes the OIDC authenticator, this will close the provider sync goroutine.

View File

@ -99,10 +99,13 @@ func (op *oidcProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
w.Write(b)
}
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value string, iat, exp time.Time) string {
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string {
signer := op.privKey.Signer()
claims := oidc.NewClaims(iss, sub, aud, iat, exp)
claims.Add(usernameClaim, value)
if groups != nil && groupsClaim != "" {
claims.Add(groupsClaim, groups)
}
jwt, err := jose.NewSignedJWT(claims, signer)
if err != nil {
@ -112,16 +115,16 @@ func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, userna
return jwt.Encode()
}
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now(), time.Now().Add(time.Hour))
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
}
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now(), time.Now().Add(time.Hour)) + "randombits"
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
}
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
}
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
@ -192,7 +195,7 @@ func TestOIDCDiscoveryTimeout(t *testing.T) {
retryBackoff = time.Second
expectErr := fmt.Errorf("failed to fetch provider config after 3 retries")
_, err := New("https://foo/bar", "client-foo", "", "sub")
_, err := New("https://foo/bar", "client-foo", "", "sub", "")
if !reflect.DeepEqual(err, expectErr) {
t.Errorf("Expecting %v, but got %v", expectErr, err)
}
@ -224,7 +227,7 @@ func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) {
Issuer: srv.URL,
}
_, err = New(srv.URL, "client-foo", cert, "sub")
_, err = New(srv.URL, "client-foo", cert, "sub", "")
if !reflect.DeepEqual(err, expectErr) {
t.Errorf("Expecting %v, but got %v", expectErr, err)
}
@ -247,7 +250,7 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) {
expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http")
_, err := New(srv.URL, "client-foo", "", "sub")
_, err := New(srv.URL, "client-foo", "", "sub", "")
if !reflect.DeepEqual(err, expectErr) {
t.Errorf("Expecting %v, but got %v", expectErr, err)
}
@ -282,7 +285,7 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) {
}
// Create a client using cert2, should fail.
_, err = New(tlsSrv.URL, "client-foo", cert2, "sub")
_, err = New(tlsSrv.URL, "client-foo", cert2, "sub", "")
if err == nil {
t.Fatalf("Expecting error, but got nothing")
}
@ -318,15 +321,17 @@ func TestOIDCAuthentication(t *testing.T) {
}
tests := []struct {
userClaim string
token string
userInfo user.Info
verified bool
err string
userClaim string
groupsClaim string
token string
userInfo user.Info
verified bool
err string
}{
{
"sub",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
"",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
true,
"",
@ -334,14 +339,34 @@ func TestOIDCAuthentication(t *testing.T) {
{
// Use user defined claim (email here).
"email",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com"),
"",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "", nil),
&user.DefaultInfo{Name: "foo@example.com"},
true,
"",
},
{
// Use user defined claim (email here).
"email",
"",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
&user.DefaultInfo{Name: "foo@example.com"},
true,
"",
},
{
// Use user defined claim (email here).
"email",
"groups",
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
&user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1", "group2"}},
true,
"",
},
{
"sub",
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
"",
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
nil,
false,
"malformed JWS, unable to decode signature",
@ -349,7 +374,8 @@ func TestOIDCAuthentication(t *testing.T) {
{
// Invalid 'aud'.
"sub",
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo"),
"",
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
nil,
false,
"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
@ -357,14 +383,16 @@ func TestOIDCAuthentication(t *testing.T) {
{
// Invalid issuer.
"sub",
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo"),
"",
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
nil,
false,
"oidc: JWT claims invalid: invalid claim value: 'iss'.",
},
{
"sub",
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo"),
"",
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
nil,
false,
"oidc: JWT claims invalid: token is expired",
@ -372,7 +400,7 @@ func TestOIDCAuthentication(t *testing.T) {
}
for i, tt := range tests {
client, err := New(srv.URL, "client-foo", cert, tt.userClaim)
client, err := New(srv.URL, "client-foo", cert, tt.userClaim, tt.groupsClaim)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}