Merge pull request #123282 from enj/enj/i/authn_config_algs

Support all key algs with structured authn config
This commit is contained in:
Kubernetes Prow Robot 2024-02-14 18:08:32 -08:00 committed by GitHub
commit 72c3c7c924
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 220 additions and 77 deletions

View File

@ -40,6 +40,7 @@ import (
"k8s.io/apiserver/pkg/server/egressselector" "k8s.io/apiserver/pkg/server/egressselector"
genericoptions "k8s.io/apiserver/pkg/server/options" genericoptions "k8s.io/apiserver/pkg/server/options"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
v1listers "k8s.io/client-go/listers/core/v1" v1listers "k8s.io/client-go/listers/core/v1"
@ -461,6 +462,9 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
if ret.AuthenticationConfig, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil { if ret.AuthenticationConfig, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil {
return kubeauthenticator.Config{}, err return kubeauthenticator.Config{}, err
} }
// all known signing algs are allowed when using authentication config
// TODO: what we really want to express is 'any alg is fine as long it matches a public key'
ret.OIDCSigningAlgs = oidc.AllValidSigningAlgorithms()
} else if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { } else if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 {
usernamePrefix := o.OIDC.UsernamePrefix usernamePrefix := o.OIDC.UsernamePrefix

View File

@ -446,6 +446,8 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
} }
func TestToAuthenticationConfig_OIDC(t *testing.T) { func TestToAuthenticationConfig_OIDC(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -640,6 +642,43 @@ func TestToAuthenticationConfig_OIDC(t *testing.T) {
OIDCSigningAlgs: []string{"RS256"}, OIDCSigningAlgs: []string{"RS256"},
}, },
}, },
{
name: "basic authentication configuration",
args: []string{
"--authentication-config=" + writeTempFile(t, `
apiVersion: apiserver.config.k8s.io/v1alpha1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://test-issuer
audiences: [ "🐼" ]
claimMappings:
username:
claim: sub
prefix: ""
`),
},
expectConfig: kubeauthenticator.Config{
TokenSuccessCacheTTL: 10 * time.Second,
AuthenticationConfig: &apiserver.AuthenticationConfiguration{
JWT: []apiserver.JWTAuthenticator{
{
Issuer: apiserver.Issuer{
URL: "https://test-issuer",
Audiences: []string{"🐼"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "sub",
Prefix: pointer.String(""),
},
},
},
},
},
OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"},
},
},
} }
for _, testcase := range testCases { for _, testcase := range testCases {

View File

@ -204,8 +204,11 @@ func (a *Authenticator) Close() {
a.cancel() a.cancel()
} }
// whitelist of signing algorithms to ensure users don't mistakenly pass something func AllValidSigningAlgorithms() []string {
// goofy. return sets.List(sets.KeySet(allowedSigningAlgs))
}
// allowlist of signing algorithms to ensure users don't mistakenly pass something goofy.
var allowedSigningAlgs = map[string]bool{ var allowedSigningAlgs = map[string]bool{
oidc.RS256: true, oidc.RS256: true,
oidc.RS384: true, oidc.RS384: true,

View File

@ -18,6 +18,8 @@ package oidc
import ( import (
"context" "context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/tls" "crypto/tls"
@ -35,11 +37,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
authenticationv1 "k8s.io/api/authentication/v1" authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
@ -116,29 +120,10 @@ func TestStructuredAuthenticationConfig(t *testing.T) {
} }
func runTests(t *testing.T, useAuthenticationConfig bool) { func runTests(t *testing.T, useAuthenticationConfig bool) {
var tests = []struct { var tests = []singleTest[*rsa.PrivateKey, *rsa.PublicKey]{
name string
configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) (
oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey,
caCertContent []byte,
caFilePath string,
)
configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
configureClient func(
t *testing.T,
restCfg *rest.Config,
caCert []byte,
certPath,
oidcServerURL,
oidcServerTokenURL string,
) kubernetes.Interface
assertErrFn func(t *testing.T, errorToCheck error)
}{
{ {
name: "ID token is ok", name: "ID token is ok",
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -161,7 +146,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
}, },
{ {
name: "ID token is expired", name: "ID token is expired",
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey) configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey)
}, },
@ -172,7 +157,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
}, },
{ {
name: "wrong client ID", name: "wrong client ID",
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {
oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID) oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID)
}, },
@ -189,7 +174,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
}, },
{ {
name: "client has wrong CA", name: "client has wrong CA",
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {}, configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {},
configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface { configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
tempDir := t.TempDir() tempDir := t.TempDir()
@ -207,7 +192,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
}, },
{ {
name: "refresh flow does not return ID Token", name: "refresh flow does not return ID Token",
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey) configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{ oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{
@ -230,7 +215,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
}, },
{ {
name: "ID token signature can not be verified due to wrong JWKs", name: "ID token signature can not be verified due to wrong JWKs",
configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc) ( configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) (
oidcServer *utilsoidc.TestServer, oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer, apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey, signingPrivateKey *rsa.PrivateKey,
@ -239,8 +224,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
) { ) {
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t) caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
signingPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) signingPrivateKey, _ = keyFunc(t)
require.NoError(t, wantErr)
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
@ -260,16 +244,16 @@ jwt:
claim: sub claim: sub
prefix: %s prefix: %s
`, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix) `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix)
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig, &signingPrivateKey.PublicKey)
} else { } else {
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "", &signingPrivateKey.PublicKey)
} }
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig) adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding) configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
anotherSigningPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) anotherSigningPrivateKey, _ := keyFunc(t)
require.NoError(t, wantErr)
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey)) oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey))
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
@ -296,11 +280,69 @@ jwt:
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, singleTestRunner(useAuthenticationConfig, rsaGenerateKey, tt))
fn := func(t *testing.T, issuerURL, caCert string) string { return "" } }
if useAuthenticationConfig {
fn = func(t *testing.T, issuerURL, caCert string) string { for _, tt := range []singleTest[*ecdsa.PrivateKey, *ecdsa.PublicKey]{
return fmt.Sprintf(` {
name: "ID token is ok",
configureInfrastructure: configureTestInfrastructure[*ecdsa.PrivateKey, *ecdsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *ecdsa.PrivateKey) {
idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
t,
signingPrivateKey,
map[string]interface{}{
"iss": oidcServer.URL(),
"sub": defaultOIDCClaimedUsername,
"aud": defaultOIDCClientID,
"exp": time.Now().Add(idTokenLifetime).Unix(),
},
defaultStubAccessToken,
defaultStubRefreshToken,
))
},
configureClient: configureClientFetchingOIDCCredentials,
assertErrFn: func(t *testing.T, errorToCheck error) {
assert.NoError(t, errorToCheck)
},
},
} {
t.Run(tt.name, singleTestRunner(useAuthenticationConfig, ecdsaGenerateKey, tt))
}
}
type singleTest[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct {
name string
configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey K,
caCertContent []byte,
caFilePath string,
)
configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
configureClient func(
t *testing.T,
restCfg *rest.Config,
caCert []byte,
certPath,
oidcServerURL,
oidcServerTokenURL string,
) kubernetes.Interface
assertErrFn func(t *testing.T, errorToCheck error)
}
func singleTestRunner[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](
useAuthenticationConfig bool,
keyFunc func(t *testing.T) (K, L),
tt singleTest[K, L],
) func(t *testing.T) {
return func(t *testing.T) {
fn := func(t *testing.T, issuerURL, caCert string) string { return "" }
if useAuthenticationConfig {
fn = func(t *testing.T, issuerURL, caCert string) string {
return fmt.Sprintf(`
apiVersion: apiserver.config.k8s.io/v1alpha1 apiVersion: apiserver.config.k8s.io/v1alpha1
kind: AuthenticationConfiguration kind: AuthenticationConfiguration
jwt: jwt:
@ -315,31 +357,32 @@ jwt:
claim: sub claim: sub
prefix: %s prefix: %s
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix)
}
} }
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn) }
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn, keyFunc)
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
tokenURL, err := oidcServer.TokenURL() tokenURL, err := oidcServer.TokenURL()
require.NoError(t, err) require.NoError(t, err)
client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL) client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
ctx := testContext(t) ctx := testContext(t)
_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{}) _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
tt.assertErrFn(t, err) tt.assertErrFn(t, err)
})
} }
} }
func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
var tests = []struct { type testRun[K utilsoidc.JosePrivateKey] struct {
name string name string
configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
assertErrFn func(t *testing.T, errorToCheck error) assertErrFn func(t *testing.T, errorToCheck error)
}{ }
var tests = []testRun[*rsa.PrivateKey]{
{ {
name: "cache returns stale client if refresh token is not updated in config", name: "cache returns stale client if refresh token is not updated in config",
configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
@ -369,7 +412,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
}, },
} }
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" }) oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" }, rsaGenerateKey)
tokenURL, err := oidcServer.TokenURL() tokenURL, err := oidcServer.TokenURL()
require.NoError(t, err) require.NoError(t, err)
@ -399,17 +442,17 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
func TestStructuredAuthenticationConfigCEL(t *testing.T) { func TestStructuredAuthenticationConfigCEL(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
tests := []struct { type testRun[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct {
name string name string
authConfigFn authenticationConfigFunc authConfigFn authenticationConfigFunc
configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) ( configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
oidcServer *utilsoidc.TestServer, oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer, apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey, signingPrivateKey *rsa.PrivateKey,
caCertContent []byte, caCertContent []byte,
caFilePath string, caFilePath string,
) )
configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
configureClient func( configureClient func(
t *testing.T, t *testing.T,
restCfg *rest.Config, restCfg *rest.Config,
@ -420,7 +463,9 @@ func TestStructuredAuthenticationConfigCEL(t *testing.T) {
) kubernetes.Interface ) kubernetes.Interface
assertErrFn func(t *testing.T, errorToCheck error) assertErrFn func(t *testing.T, errorToCheck error)
wantUser *authenticationv1.UserInfo wantUser *authenticationv1.UserInfo
}{ }
tests := []testRun[*rsa.PrivateKey, *rsa.PublicKey]{
{ {
name: "username CEL expression is ok", name: "username CEL expression is ok",
authConfigFn: func(t *testing.T, issuerURL, caCert string) string { authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
@ -441,7 +486,7 @@ jwt:
expression: "'k8s-' + claims.sub" expression: "'k8s-' + claims.sub"
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -488,7 +533,7 @@ jwt:
expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)' expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)'
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -538,7 +583,7 @@ jwt:
message: "the hd claim must be set to example.com" message: "the hd claim must be set to example.com"
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -588,7 +633,7 @@ jwt:
message: "example.org/foo must be bar and example.org/baz must be qux" message: "example.org/foo must be bar and example.org/baz must be qux"
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -640,7 +685,7 @@ jwt:
expression: "claims.uid" expression: "claims.uid"
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -692,7 +737,7 @@ jwt:
message: "groups cannot used reserved system: prefix" message: "groups cannot used reserved system: prefix"
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
}, },
configureInfrastructure: configureTestInfrastructure, configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200 idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
@ -720,7 +765,7 @@ jwt:
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn) oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn, rsaGenerateKey)
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
@ -743,10 +788,28 @@ jwt:
} }
} }
func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) ( func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
t.Helper()
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
require.NoError(t, err)
return privateKey, &privateKey.PublicKey
}
func ecdsaGenerateKey(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
return privateKey, &privateKey.PublicKey
}
func configureTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
oidcServer *utilsoidc.TestServer, oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer, apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey, signingPrivateKey K,
caCertContent []byte, caCertContent []byte,
caFilePath string, caFilePath string,
) { ) {
@ -754,19 +817,18 @@ func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) (
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t) caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
signingPrivateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) signingPrivateKey, publicKey := keyFunc(t)
require.NoError(t, err)
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent)) authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
if len(authenticationConfig) > 0 { if len(authenticationConfig) > 0 {
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig, publicKey)
} else { } else {
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "", publicKey)
} }
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey)) oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig) adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding) configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
@ -815,7 +877,7 @@ func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, c
return cfg return cfg
} }
func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string) *kubeapiserverapptesting.TestServer { func startTestAPIServerForOIDC[L utilsoidc.JosePublicKey](t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string, publicKey L) *kubeapiserverapptesting.TestServer {
t.Helper() t.Helper()
var customFlags []string var customFlags []string
@ -828,6 +890,7 @@ func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePa
fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath), fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath),
fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix), fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix),
} }
customFlags = append(customFlags, maybeSetSigningAlgs(publicKey)...)
} }
customFlags = append(customFlags, "--authorization-mode=RBAC") customFlags = append(customFlags, "--authorization-mode=RBAC")
@ -844,6 +907,18 @@ func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePa
return &server return &server
} }
func maybeSetSigningAlgs[K utilsoidc.JoseKey](key K) []string {
alg := utilsoidc.GetSignatureAlgorithm(key)
if alg == jose.RS256 && randomBool() {
return nil // check the default case of RS256 by not always setting the flag
}
return []string{
fmt.Sprintf("--oidc-signing-algs=%s", alg), // all other algs need to be manually set
}
}
func randomBool() bool { return utilrand.Int()%2 == 1 }
func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) { func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) {
t.Helper() t.Helper()

View File

@ -18,6 +18,7 @@ package oidc
import ( import (
"crypto" "crypto"
"crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"crypto/tls" "crypto/tls"
"encoding/hex" "encoding/hex"
@ -170,17 +171,21 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
return oidcServer return oidcServer
} }
type JosePrivateKey interface {
*rsa.PrivateKey | *ecdsa.PrivateKey
}
// TokenHandlerBehaviorReturningPredefinedJWT describes the scenario when signed JWT token is being created. // TokenHandlerBehaviorReturningPredefinedJWT describes the scenario when signed JWT token is being created.
// This behavior should being applied to the MockTokenHandler. // This behavior should being applied to the MockTokenHandler.
func TokenHandlerBehaviorReturningPredefinedJWT( func TokenHandlerBehaviorReturningPredefinedJWT[K JosePrivateKey](
t *testing.T, t *testing.T,
rsaPrivateKey *rsa.PrivateKey, privateKey K,
claims map[string]interface{}, accessToken, refreshToken string, claims map[string]interface{}, accessToken, refreshToken string,
) func() (Token, error) { ) func() (Token, error) {
t.Helper() t.Helper()
return func() (Token, error) { return func() (Token, error) {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: rsaPrivateKey}, nil) signer, err := jose.NewSigner(jose.SigningKey{Algorithm: GetSignatureAlgorithm(privateKey), Key: privateKey}, nil)
require.NoError(t, err) require.NoError(t, err)
payloadJSON, err := json.Marshal(claims) payloadJSON, err := json.Marshal(claims)
@ -199,13 +204,17 @@ func TokenHandlerBehaviorReturningPredefinedJWT(
} }
} }
type JosePublicKey interface {
*rsa.PublicKey | *ecdsa.PublicKey
}
// DefaultJwksHandlerBehavior describes the scenario when JSON Web Key Set token is being returned. // DefaultJwksHandlerBehavior describes the scenario when JSON Web Key Set token is being returned.
// This behavior should being applied to the MockJWKsHandler. // This behavior should being applied to the MockJWKsHandler.
func DefaultJwksHandlerBehavior(t *testing.T, verificationPublicKey *rsa.PublicKey) func() jose.JSONWebKeySet { func DefaultJwksHandlerBehavior[K JosePublicKey](t *testing.T, verificationPublicKey K) func() jose.JSONWebKeySet {
t.Helper() t.Helper()
return func() jose.JSONWebKeySet { return func() jose.JSONWebKeySet {
key := jose.JSONWebKey{Key: verificationPublicKey, Use: "sig", Algorithm: string(jose.RS256)} key := jose.JSONWebKey{Key: verificationPublicKey, Use: "sig", Algorithm: string(GetSignatureAlgorithm(verificationPublicKey))}
thumbprint, err := key.Thumbprint(crypto.SHA256) thumbprint, err := key.Thumbprint(crypto.SHA256)
require.NoError(t, err) require.NoError(t, err)
@ -216,3 +225,16 @@ func DefaultJwksHandlerBehavior(t *testing.T, verificationPublicKey *rsa.PublicK
} }
} }
} }
type JoseKey interface{ JosePrivateKey | JosePublicKey }
func GetSignatureAlgorithm[K JoseKey](key K) jose.SignatureAlgorithm {
switch any(key).(type) {
case *rsa.PrivateKey, *rsa.PublicKey:
return jose.RS256
case *ecdsa.PrivateKey, *ecdsa.PublicKey:
return jose.ES256
default:
panic("unknown key type") // should be impossible
}
}