diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index f45cd058..c73a351d 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package jwtcachefiller implements a controller for filling an authncache.Cache with each @@ -17,7 +17,6 @@ import ( "gopkg.in/square/go-jose.v2" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apiserver/pkg/authentication/authenticator" - "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" "k8s.io/klog/v2" @@ -150,19 +149,11 @@ func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwt // newJWTAuthenticator creates a jwt authenticator from the provided spec. func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) { - rootCAs, caBundle, err := pinnipedauthenticator.CABundle(spec.TLS) + rootCAs, _, err := pinnipedauthenticator.CABundle(spec.TLS) if err != nil { return nil, fmt.Errorf("invalid TLS configuration: %w", err) } - var caContentProvider oidc.CAContentProvider - if len(caBundle) != 0 { - var caContentProviderErr error - caContentProvider, caContentProviderErr = dynamiccertificates.NewStaticCAContent("ignored", caBundle) - if caContentProviderErr != nil { - return nil, caContentProviderErr // impossible since caBundle is validated already - } - } usernameClaim := spec.Claims.Username if usernameClaim == "" { usernameClaim = defaultUsernameClaim @@ -199,7 +190,6 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica if len(providerJSON.JWKSURL) == 0 { return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer) } - oidcAuthenticator, err := oidc.New(oidc.Options{ IssuerURL: spec.Issuer, KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL), @@ -207,9 +197,7 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica UsernameClaim: usernameClaim, GroupsClaim: groupsClaim, SupportedSigningAlgs: defaultSupportedSigningAlgos(), - // this is still needed for distributed claim resolution, meaning this uses a http client that does not honor our TLS config - // TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106141 - CAContentProvider: caContentProvider, + Client: client, }) if err != nil { return nil, fmt.Errorf("could not initialize authenticator: %w", err) diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go index 40de21a8..be6e05c7 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go @@ -58,6 +58,9 @@ func TestController(t *testing.T) { require.NoError(t, err) goodRSASigningAlgo := jose.RS256 + customGroupsClaim := "my-custom-groups-claim" + distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"} + mux := http.NewServeMux() server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) @@ -87,6 +90,59 @@ func TestController(t *testing.T) { } require.NoError(t, json.NewEncoder(w).Encode(jwks)) })) + // Claims without the subject, to be used distributed claims tests. + // OIDC 1.0 section 5.6.2: + // A sub (subject) Claim SHOULD NOT be returned from the Claims Provider unless its value + // is an identifier for the End-User at the Claims Provider (and not for the OpenID Provider or another party); + // this typically means that a sub Claim SHOULD NOT be provided. + claimsWithoutSubject := jwt.Claims{ + Issuer: server.URL, + Audience: []string{goodAudience}, + Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), + NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + } + mux.Handle("/claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Unfortunately we have to set this up pretty early in the test because we can't redeclare + // mux.Handle. This means that we can't return a different groups claim per test; we have to + // return both and predecide which groups are returned. + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID), + ) + require.NoError(t, err) + + builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + + builder = builder.Claims(map[string]interface{}{customGroupsClaim: distributedGroups}) + builder = builder.Claims(map[string]interface{}{"groups": distributedGroups}) + + distributedClaimsJwt, err := builder.CompactSerialize() + require.NoError(t, err) + + _, err = w.Write([]byte(distributedClaimsJwt)) + require.NoError(t, err) + })) + mux.Handle("/wrong_claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Unfortunately we have to set this up pretty early in the test because we can't redeclare + // mux.Handle. This means that we can't return a different groups claim per test; we have to + // return both and predecide which groups are returned. + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID), + ) + require.NoError(t, err) + + builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + + builder = builder.Claims(map[string]interface{}{"some-other-claim": distributedGroups}) + + distributedClaimsJwt, err := builder.CompactSerialize() + require.NoError(t, err) + + _, err = w.Write([]byte(distributedClaimsJwt)) + require.NoError(t, err) + })) goodIssuer := server.URL @@ -108,7 +164,7 @@ func TestController(t *testing.T) { Audience: goodAudience, TLS: tlsSpecFromTLSConfig(server.TLS), Claims: auth1alpha1.JWTTokenClaims{ - Groups: "my-custom-groups-claim", + Groups: customGroupsClaim, }, } otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{ @@ -384,6 +440,7 @@ func TestController(t *testing.T) { goodUsername, tt.wantUsernameClaim, tt.wantGroupsClaim, + goodIssuer, ) { test := test t.Run(test.name, func(t *testing.T) { @@ -418,6 +475,7 @@ func TestController(t *testing.T) { &wellKnownClaims, tt.wantGroupsClaim, groups, + test.distributedGroupsClaimURL, tt.wantUsernameClaim, username, ) @@ -447,9 +505,10 @@ func TestController(t *testing.T) { } // isNotInitialized checks if the error is the internally-defined "oidc: authenticator not initialized" error from -// the underlying OIDC authenticator, which is initialized asynchronously. +// the underlying OIDC authenticator or "verifier is not initialized" from verifying distributed claims, +// both of which are initialized asynchronously. func isNotInitialized(err error) bool { - return err != nil && strings.Contains(err.Error(), "authenticator not initialized") + return err != nil && (strings.Contains(err.Error(), "authenticator not initialized") || strings.Contains(err.Error(), "verifier not initialized")) } func testTableForAuthenticateTokenTests( @@ -462,21 +521,24 @@ func testTableForAuthenticateTokenTests( goodUsername string, expectedUsernameClaim string, expectedGroupsClaim string, + issuer string, ) []struct { - name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) - jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) - wantResponse *authenticator.Response - wantAuthenticated bool - wantErrorRegexp string + name string + jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) + jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) + wantResponse *authenticator.Response + wantAuthenticated bool + wantErrorRegexp string + distributedGroupsClaimURL string } { tests := []struct { - name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) - jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) - wantResponse *authenticator.Response - wantAuthenticated bool - wantErrorRegexp string + name string + jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) + jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) + wantResponse *authenticator.Response + wantAuthenticated bool + wantErrorRegexp string + distributedGroupsClaimURL string }{ { name: "good token without groups and with EC signature", @@ -514,6 +576,33 @@ func testTableForAuthenticateTokenTests( }, wantAuthenticated: true, }, + { + name: "good token with good distributed groups", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/claim_source", + wantResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: goodUsername, + Groups: []string{"some-distributed-group-1", "some-distributed-group-2"}, + }, + }, + wantAuthenticated: true, + }, + { + name: "distributed groups returns a 404", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/not_found_claim_source", + wantErrorRegexp: `oidc: could not expand distributed claims: while getting distributed claim "` + expectedGroupsClaim + `": error while getting distributed claim JWT: 404 Not Found`, + }, + { + name: "distributed groups doesn't return the right claim", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/wrong_claim_source", + wantErrorRegexp: `oidc: could not expand distributed claims: jwt returned by distributed claim endpoint "` + issuer + `/wrong_claim_source" did not contain claim: `, + }, { name: "good token with groups as string", jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) { @@ -644,6 +733,7 @@ func createJWT( claims *jwt.Claims, groupsClaim string, groupsValue interface{}, + distributedGroupsClaimURL string, usernameClaim string, usernameValue string, ) string { @@ -659,6 +749,10 @@ func createJWT( if groupsValue != nil { builder = builder.Claims(map[string]interface{}{groupsClaim: groupsValue}) } + if distributedGroupsClaimURL != "" { + builder = builder.Claims(map[string]interface{}{"_claim_names": map[string]string{groupsClaim: "src1"}}) + builder = builder.Claims(map[string]interface{}{"_claim_sources": map[string]interface{}{"src1": map[string]string{"endpoint": distributedGroupsClaimURL}}}) + } if usernameValue != "" { builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue}) }