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:
parent
c40bca5e65
commit
0b72f7084c
@ -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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package jwtcachefiller implements a controller for filling an authncache.Cache with each
|
// Package jwtcachefiller implements a controller for filling an authncache.Cache with each
|
||||||
@ -17,7 +17,6 @@ import (
|
|||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
|
||||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
|
||||||
"k8s.io/klog/v2"
|
"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.
|
// newJWTAuthenticator creates a jwt authenticator from the provided spec.
|
||||||
func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) {
|
func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) {
|
||||||
rootCAs, caBundle, err := pinnipedauthenticator.CABundle(spec.TLS)
|
rootCAs, _, err := pinnipedauthenticator.CABundle(spec.TLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
|
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
|
usernameClaim := spec.Claims.Username
|
||||||
if usernameClaim == "" {
|
if usernameClaim == "" {
|
||||||
usernameClaim = defaultUsernameClaim
|
usernameClaim = defaultUsernameClaim
|
||||||
@ -199,7 +190,6 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
|
|||||||
if len(providerJSON.JWKSURL) == 0 {
|
if len(providerJSON.JWKSURL) == 0 {
|
||||||
return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer)
|
return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcAuthenticator, err := oidc.New(oidc.Options{
|
oidcAuthenticator, err := oidc.New(oidc.Options{
|
||||||
IssuerURL: spec.Issuer,
|
IssuerURL: spec.Issuer,
|
||||||
KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL),
|
KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL),
|
||||||
@ -207,9 +197,7 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
|
|||||||
UsernameClaim: usernameClaim,
|
UsernameClaim: usernameClaim,
|
||||||
GroupsClaim: groupsClaim,
|
GroupsClaim: groupsClaim,
|
||||||
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
||||||
// this is still needed for distributed claim resolution, meaning this uses a http client that does not honor our TLS config
|
Client: client,
|
||||||
// TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106141
|
|
||||||
CAContentProvider: caContentProvider,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not initialize authenticator: %w", err)
|
return nil, fmt.Errorf("could not initialize authenticator: %w", err)
|
||||||
|
@ -58,6 +58,9 @@ func TestController(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
goodRSASigningAlgo := jose.RS256
|
goodRSASigningAlgo := jose.RS256
|
||||||
|
|
||||||
|
customGroupsClaim := "my-custom-groups-claim"
|
||||||
|
distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tlsserver.AssertTLS(t, r, ptls.Default)
|
tlsserver.AssertTLS(t, r, ptls.Default)
|
||||||
@ -87,6 +90,59 @@ func TestController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, json.NewEncoder(w).Encode(jwks))
|
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
|
goodIssuer := server.URL
|
||||||
|
|
||||||
@ -108,7 +164,7 @@ func TestController(t *testing.T) {
|
|||||||
Audience: goodAudience,
|
Audience: goodAudience,
|
||||||
TLS: tlsSpecFromTLSConfig(server.TLS),
|
TLS: tlsSpecFromTLSConfig(server.TLS),
|
||||||
Claims: auth1alpha1.JWTTokenClaims{
|
Claims: auth1alpha1.JWTTokenClaims{
|
||||||
Groups: "my-custom-groups-claim",
|
Groups: customGroupsClaim,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
@ -384,6 +440,7 @@ func TestController(t *testing.T) {
|
|||||||
goodUsername,
|
goodUsername,
|
||||||
tt.wantUsernameClaim,
|
tt.wantUsernameClaim,
|
||||||
tt.wantGroupsClaim,
|
tt.wantGroupsClaim,
|
||||||
|
goodIssuer,
|
||||||
) {
|
) {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@ -418,6 +475,7 @@ func TestController(t *testing.T) {
|
|||||||
&wellKnownClaims,
|
&wellKnownClaims,
|
||||||
tt.wantGroupsClaim,
|
tt.wantGroupsClaim,
|
||||||
groups,
|
groups,
|
||||||
|
test.distributedGroupsClaimURL,
|
||||||
tt.wantUsernameClaim,
|
tt.wantUsernameClaim,
|
||||||
username,
|
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
|
// 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 {
|
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(
|
func testTableForAuthenticateTokenTests(
|
||||||
@ -462,6 +521,7 @@ func testTableForAuthenticateTokenTests(
|
|||||||
goodUsername string,
|
goodUsername string,
|
||||||
expectedUsernameClaim string,
|
expectedUsernameClaim string,
|
||||||
expectedGroupsClaim string,
|
expectedGroupsClaim string,
|
||||||
|
issuer string,
|
||||||
) []struct {
|
) []struct {
|
||||||
name string
|
name string
|
||||||
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
|
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
|
||||||
@ -469,6 +529,7 @@ func testTableForAuthenticateTokenTests(
|
|||||||
wantResponse *authenticator.Response
|
wantResponse *authenticator.Response
|
||||||
wantAuthenticated bool
|
wantAuthenticated bool
|
||||||
wantErrorRegexp string
|
wantErrorRegexp string
|
||||||
|
distributedGroupsClaimURL string
|
||||||
} {
|
} {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -477,6 +538,7 @@ func testTableForAuthenticateTokenTests(
|
|||||||
wantResponse *authenticator.Response
|
wantResponse *authenticator.Response
|
||||||
wantAuthenticated bool
|
wantAuthenticated bool
|
||||||
wantErrorRegexp string
|
wantErrorRegexp string
|
||||||
|
distributedGroupsClaimURL string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "good token without groups and with EC signature",
|
name: "good token without groups and with EC signature",
|
||||||
@ -514,6 +576,33 @@ func testTableForAuthenticateTokenTests(
|
|||||||
},
|
},
|
||||||
wantAuthenticated: true,
|
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",
|
name: "good token with groups as string",
|
||||||
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
|
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
|
||||||
@ -644,6 +733,7 @@ func createJWT(
|
|||||||
claims *jwt.Claims,
|
claims *jwt.Claims,
|
||||||
groupsClaim string,
|
groupsClaim string,
|
||||||
groupsValue interface{},
|
groupsValue interface{},
|
||||||
|
distributedGroupsClaimURL string,
|
||||||
usernameClaim string,
|
usernameClaim string,
|
||||||
usernameValue string,
|
usernameValue string,
|
||||||
) string {
|
) string {
|
||||||
@ -659,6 +749,10 @@ func createJWT(
|
|||||||
if groupsValue != nil {
|
if groupsValue != nil {
|
||||||
builder = builder.Claims(map[string]interface{}{groupsClaim: groupsValue})
|
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 != "" {
|
if usernameValue != "" {
|
||||||
builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue})
|
builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user