diff --git a/pkg/kubeapiserver/authenticator/config.go b/pkg/kubeapiserver/authenticator/config.go index bed3e288a1..cbe894a194 100644 --- a/pkg/kubeapiserver/authenticator/config.go +++ b/pkg/kubeapiserver/authenticator/config.go @@ -159,7 +159,18 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe // 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 { - oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCUsernamePrefix, config.OIDCGroupsClaim, config.OIDCGroupsPrefix, config.OIDCSigningAlgs, config.OIDCRequiredClaims) + oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(oidc.Options{ + IssuerURL: config.OIDCIssuerURL, + ClientID: config.OIDCClientID, + APIAudiences: config.APIAudiences, + CAFile: config.OIDCCAFile, + 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 } @@ -238,33 +249,23 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e } // newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error. -func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, usernamePrefix, groupsClaim, groupsPrefix string, signingAlgs []string, requiredClaims map[string]string) (authenticator.Token, error) { +func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token, error) { const noUsernamePrefix = "-" - if usernamePrefix == "" && usernameClaim != "email" { + 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 - usernamePrefix = issuerURL + "#" + opts.UsernamePrefix = opts.IssuerURL + "#" } - if usernamePrefix == noUsernamePrefix { + if opts.UsernamePrefix == noUsernamePrefix { // Special value indicating usernames shouldn't be prefixed. - usernamePrefix = "" + opts.UsernamePrefix = "" } - tokenAuthenticator, err := oidc.New(oidc.Options{ - IssuerURL: issuerURL, - ClientID: clientID, - CAFile: caFile, - UsernameClaim: usernameClaim, - UsernamePrefix: usernamePrefix, - GroupsClaim: groupsClaim, - GroupsPrefix: groupsPrefix, - SupportedSigningAlgs: signingAlgs, - RequiredClaims: requiredClaims, - }) + tokenAuthenticator, err := oidc.New(opts) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/BUILD b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/BUILD index 3863bb823f..9a1078dfab 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/BUILD +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/BUILD @@ -13,6 +13,7 @@ go_test( data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ + "//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/github.com/coreos/go-oidc:go_default_library", "//vendor/github.com/golang/glog:go_default_library", diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go index f53fc2ddd4..3f44f5d757 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go @@ -78,6 +78,12 @@ type Options struct { // See: https://openid.net/specs/openid-connect-core-1_0.html#IDToken ClientID string + // APIAudiences are the audiences that the API server identitifes as. The + // (API audiences unioned with the ClientIDs) should have a non-empty + // intersection with the request's target audience. This preserves the + // behavior of the OIDC authenticator pre-introduction of API audiences. + APIAudiences authenticator.Audiences + // Path to a PEM encoded root certificate of the provider. CAFile string @@ -188,6 +194,8 @@ type Authenticator struct { groupsClaim string groupsPrefix string requiredClaims map[string]string + clientIDs authenticator.Audiences + apiAudiences authenticator.Audiences // Contains an *oidc.IDTokenVerifier. Do not access directly use the // idTokenVerifier method. @@ -317,6 +325,8 @@ func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Au groupsClaim: opts.GroupsClaim, groupsPrefix: opts.GroupsPrefix, requiredClaims: opts.RequiredClaims, + clientIDs: authenticator.Audiences{opts.ClientID}, + apiAudiences: opts.APIAudiences, cancel: cancel, resolver: resolver, } @@ -532,6 +542,11 @@ func (r *claimResolver) resolve(endpoint endpoint, allClaims claims) error { } func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { + if reqAuds, ok := authenticator.AudiencesFrom(ctx); ok { + if len(reqAuds.Intersect(a.clientIDs)) == 0 && len(reqAuds.Intersect(a.apiAudiences)) == 0 { + return nil, false, nil + } + } if !hasCorrectIssuer(a.issuerURL, token) { return nil, false, nil } diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go index 0ceb72e83b..87e8ffc526 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go @@ -38,6 +38,8 @@ import ( oidc "github.com/coreos/go-oidc" "github.com/golang/glog" jose "gopkg.in/square/go-jose.v2" + + "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" ) @@ -140,6 +142,7 @@ type claimsTest struct { wantInitErr bool claimToResponseMap map[string]string openIDConfig string + reqAudiences authenticator.Audiences } // Replace formats the contents of v into the provided template. @@ -296,7 +299,12 @@ func (c *claimsTest) run(t *testing.T) { t.Fatalf("serialize token: %v", err) } - got, ok, err := a.AuthenticateToken(context.Background(), token) + ctx := context.Background() + if c.reqAudiences != nil { + ctx = authenticator.WithAudiences(ctx, c.reqAudiences) + } + + got, ok, err := a.AuthenticateToken(ctx, token) if err != nil { if !c.wantErr { @@ -1388,6 +1396,148 @@ func TestToken(t *testing.T) { Name: "thomas.jefferson@gmail.com", }, }, + { + name: "good token with api req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"api"}, + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "good token with multiple api req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api", "other"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"api"}, + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "good token with client_id req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"my-client"}, + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "good token with client_id and api req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"my-client", "api"}, + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "good token with client_id and api req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"my-client", "api"}, + want: &user.DefaultInfo{ + Name: "jane", + }, + }, + { + name: "good token with client_id and bad req audience", + options: Options{ + IssuerURL: "https://auth.example.com", + ClientID: "my-client", + APIAudiences: authenticator.Audiences{"api"}, + UsernameClaim: "username", + now: func() time.Time { return now }, + }, + signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), + pubKeys: []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + }, + claims: fmt.Sprintf(`{ + "iss": "https://auth.example.com", + "aud": "my-client", + "username": "jane", + "exp": %d + }`, valid.Unix()), + reqAudiences: authenticator.Audiences{"other"}, + wantSkip: true, + }, } for _, test := range tests { t.Run(test.name, test.run)