JWTAuthenticator distributed claims resolution honors tls config

Kube 1.23 introduced a new field on the OIDC Authenticator which
allows us to pass in a client with our own TLS config. See
https://github.com/kubernetes/kubernetes/pull/106141.

Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Margo Crawford 2022-04-18 11:46:33 -07:00
parent c40bca5e65
commit 0b72f7084c
2 changed files with 112 additions and 30 deletions

View File

@ -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)

View File

@ -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})
}