k8s.io/kubernetes@v1.29.3/test/integration/apiserver/oidc/oidc_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package oidc
    18  
    19  import (
    20  	"context"
    21  	"crypto/rand"
    22  	"crypto/rsa"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"encoding/json"
    26  	"fmt"
    27  	"net"
    28  	"net/http"
    29  	"net/url"
    30  	"os"
    31  	"path/filepath"
    32  	"strings"
    33  	"testing"
    34  	"time"
    35  
    36  	"github.com/stretchr/testify/assert"
    37  	"github.com/stretchr/testify/require"
    38  
    39  	authenticationv1 "k8s.io/api/authentication/v1"
    40  	rbacv1 "k8s.io/api/rbac/v1"
    41  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    42  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    43  	"k8s.io/apiserver/pkg/features"
    44  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    45  	"k8s.io/client-go/kubernetes"
    46  	_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
    47  	"k8s.io/client-go/rest"
    48  	"k8s.io/client-go/tools/clientcmd/api"
    49  	certutil "k8s.io/client-go/util/cert"
    50  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    51  	kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    52  	"k8s.io/kubernetes/pkg/apis/rbac"
    53  	"k8s.io/kubernetes/test/integration/framework"
    54  	utilsoidc "k8s.io/kubernetes/test/utils/oidc"
    55  	utilsnet "k8s.io/utils/net"
    56  )
    57  
    58  const (
    59  	defaultNamespace           = "default"
    60  	defaultOIDCClientID        = "f403b682-603f-4ec9-b3e4-cf111ef36f7c"
    61  	defaultOIDCClaimedUsername = "john_doe"
    62  	defaultOIDCUsernamePrefix  = "k8s-"
    63  	defaultRBACRoleName        = "developer-role"
    64  	defaultRBACRoleBindingName = "developer-role-binding"
    65  
    66  	defaultStubRefreshToken = "_fake_refresh_token_"
    67  	defaultStubAccessToken  = "_fake_access_token_"
    68  
    69  	rsaKeyBitSize = 2048
    70  )
    71  
    72  var (
    73  	defaultRole = &rbacv1.Role{
    74  		TypeMeta:   metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
    75  		ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleName},
    76  		Rules: []rbacv1.PolicyRule{
    77  			{
    78  				Verbs:         []string{"list"},
    79  				Resources:     []string{"pods"},
    80  				APIGroups:     []string{""},
    81  				ResourceNames: []string{},
    82  			},
    83  		},
    84  	}
    85  	defaultRoleBinding = &rbacv1.RoleBinding{
    86  		TypeMeta:   metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"},
    87  		ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleBindingName},
    88  		Subjects: []rbacv1.Subject{
    89  			{
    90  				APIGroup: rbac.GroupName,
    91  				Kind:     rbacv1.UserKind,
    92  				Name:     defaultOIDCUsernamePrefix + defaultOIDCClaimedUsername,
    93  			},
    94  		},
    95  		RoleRef: rbacv1.RoleRef{
    96  			APIGroup: rbac.GroupName,
    97  			Kind:     "Role",
    98  			Name:     defaultRBACRoleName,
    99  		},
   100  	}
   101  )
   102  
   103  // authenticationConfigFunc is a function that returns a string representation of an authentication config.
   104  type authenticationConfigFunc func(t *testing.T, issuerURL, caCert string) string
   105  
   106  func TestOIDC(t *testing.T) {
   107  	t.Log("Testing OIDC authenticator with --oidc-* flags")
   108  	runTests(t, false)
   109  }
   110  
   111  func TestStructuredAuthenticationConfig(t *testing.T) {
   112  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
   113  
   114  	t.Log("Testing OIDC authenticator with authentication config")
   115  	runTests(t, true)
   116  }
   117  
   118  func runTests(t *testing.T, useAuthenticationConfig bool) {
   119  	var tests = []struct {
   120  		name                    string
   121  		configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) (
   122  			oidcServer *utilsoidc.TestServer,
   123  			apiServer *kubeapiserverapptesting.TestServer,
   124  			signingPrivateKey *rsa.PrivateKey,
   125  			caCertContent []byte,
   126  			caFilePath string,
   127  		)
   128  		configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
   129  		configureClient              func(
   130  			t *testing.T,
   131  			restCfg *rest.Config,
   132  			caCert []byte,
   133  			certPath,
   134  			oidcServerURL,
   135  			oidcServerTokenURL string,
   136  		) kubernetes.Interface
   137  		assertErrFn func(t *testing.T, errorToCheck error)
   138  	}{
   139  		{
   140  			name:                    "ID token is ok",
   141  			configureInfrastructure: configureTestInfrastructure,
   142  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   143  				idTokenLifetime := time.Second * 1200
   144  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   145  					t,
   146  					signingPrivateKey,
   147  					map[string]interface{}{
   148  						"iss": oidcServer.URL(),
   149  						"sub": defaultOIDCClaimedUsername,
   150  						"aud": defaultOIDCClientID,
   151  						"exp": time.Now().Add(idTokenLifetime).Unix(),
   152  					},
   153  					defaultStubAccessToken,
   154  					defaultStubRefreshToken,
   155  				))
   156  			},
   157  			configureClient: configureClientFetchingOIDCCredentials,
   158  			assertErrFn: func(t *testing.T, errorToCheck error) {
   159  				assert.NoError(t, errorToCheck)
   160  			},
   161  		},
   162  		{
   163  			name:                    "ID token is expired",
   164  			configureInfrastructure: configureTestInfrastructure,
   165  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   166  				configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey)
   167  			},
   168  			configureClient: configureClientFetchingOIDCCredentials,
   169  			assertErrFn: func(t *testing.T, errorToCheck error) {
   170  				assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
   171  			},
   172  		},
   173  		{
   174  			name:                    "wrong client ID",
   175  			configureInfrastructure: configureTestInfrastructure,
   176  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {
   177  				oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID)
   178  			},
   179  			configureClient: configureClientWithEmptyIDToken,
   180  			assertErrFn: func(t *testing.T, errorToCheck error) {
   181  				urlError, ok := errorToCheck.(*url.Error)
   182  				require.True(t, ok)
   183  				assert.Equal(
   184  					t,
   185  					"failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: client ID is bad\n",
   186  					urlError.Err.Error(),
   187  				)
   188  			},
   189  		},
   190  		{
   191  			name:                         "client has wrong CA",
   192  			configureInfrastructure:      configureTestInfrastructure,
   193  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {},
   194  			configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
   195  				tempDir := t.TempDir()
   196  				certFilePath := filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
   197  
   198  				_, _, wantErr := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
   199  				require.NoError(t, wantErr)
   200  
   201  				return configureClientWithEmptyIDToken(t, restCfg, caCert, certFilePath, oidcServerURL, oidcServerTokenURL)
   202  			},
   203  			assertErrFn: func(t *testing.T, errorToCheck error) {
   204  				expectedErr := new(x509.UnknownAuthorityError)
   205  				assert.ErrorAs(t, errorToCheck, expectedErr)
   206  			},
   207  		},
   208  		{
   209  			name:                    "refresh flow does not return ID Token",
   210  			configureInfrastructure: configureTestInfrastructure,
   211  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   212  				configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
   213  				oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{
   214  					IDToken:      "",
   215  					AccessToken:  defaultStubAccessToken,
   216  					RefreshToken: defaultStubRefreshToken,
   217  					ExpiresIn:    time.Now().Add(time.Second * 1200).Unix(),
   218  				}, nil)
   219  			},
   220  			configureClient: configureClientFetchingOIDCCredentials,
   221  			assertErrFn: func(t *testing.T, errorToCheck error) {
   222  				expectedError := new(apierrors.StatusError)
   223  				assert.ErrorAs(t, errorToCheck, &expectedError)
   224  				assert.Equal(
   225  					t,
   226  					`pods is forbidden: User "system:anonymous" cannot list resource "pods" in API group "" in the namespace "default"`,
   227  					errorToCheck.Error(),
   228  				)
   229  			},
   230  		},
   231  		{
   232  			name: "ID token signature can not be verified due to wrong JWKs",
   233  			configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc) (
   234  				oidcServer *utilsoidc.TestServer,
   235  				apiServer *kubeapiserverapptesting.TestServer,
   236  				signingPrivateKey *rsa.PrivateKey,
   237  				caCertContent []byte,
   238  				caFilePath string,
   239  			) {
   240  				caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
   241  
   242  				signingPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
   243  				require.NoError(t, wantErr)
   244  
   245  				oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
   246  
   247  				if useAuthenticationConfig {
   248  					authenticationConfig := fmt.Sprintf(`
   249  apiVersion: apiserver.config.k8s.io/v1alpha1
   250  kind: AuthenticationConfiguration
   251  jwt:
   252  - issuer:
   253      url: %s
   254      audiences:
   255      - %s
   256      certificateAuthority: |
   257          %s
   258    claimMappings:
   259      username:
   260        claim: sub
   261        prefix: %s
   262  `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix)
   263  					apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
   264  				} else {
   265  					apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
   266  				}
   267  
   268  				adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
   269  				configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
   270  
   271  				anotherSigningPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
   272  				require.NoError(t, wantErr)
   273  				oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey))
   274  
   275  				return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
   276  			},
   277  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   278  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   279  					t,
   280  					signingPrivateKey,
   281  					map[string]interface{}{
   282  						"iss": oidcServer.URL(),
   283  						"sub": defaultOIDCClaimedUsername,
   284  						"aud": defaultOIDCClientID,
   285  						"exp": time.Now().Add(time.Second * 1200).Unix(),
   286  					},
   287  					defaultStubAccessToken,
   288  					defaultStubRefreshToken,
   289  				))
   290  			},
   291  			configureClient: configureClientFetchingOIDCCredentials,
   292  			assertErrFn: func(t *testing.T, errorToCheck error) {
   293  				assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
   294  			},
   295  		},
   296  	}
   297  
   298  	for _, tt := range tests {
   299  		t.Run(tt.name, func(t *testing.T) {
   300  			fn := func(t *testing.T, issuerURL, caCert string) string { return "" }
   301  			if useAuthenticationConfig {
   302  				fn = func(t *testing.T, issuerURL, caCert string) string {
   303  					return fmt.Sprintf(`
   304  apiVersion: apiserver.config.k8s.io/v1alpha1
   305  kind: AuthenticationConfiguration
   306  jwt:
   307  - issuer:
   308      url: %s
   309      audiences:
   310      - %s
   311      certificateAuthority: |
   312          %s
   313    claimMappings:
   314      username:
   315        claim: sub
   316        prefix: %s
   317  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix)
   318  				}
   319  			}
   320  			oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn)
   321  
   322  			tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
   323  
   324  			tokenURL, err := oidcServer.TokenURL()
   325  			require.NoError(t, err)
   326  
   327  			client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
   328  
   329  			ctx := testContext(t)
   330  			_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
   331  
   332  			tt.assertErrFn(t, err)
   333  		})
   334  	}
   335  }
   336  
   337  func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
   338  	var tests = []struct {
   339  		name                            string
   340  		configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
   341  		assertErrFn                     func(t *testing.T, errorToCheck error)
   342  	}{
   343  		{
   344  			name: "cache returns stale client if refresh token is not updated in config",
   345  			configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   346  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   347  					t,
   348  					signingPrivateKey,
   349  					map[string]interface{}{
   350  						"iss": oidcServer.URL(),
   351  						"sub": defaultOIDCClaimedUsername,
   352  						"aud": defaultOIDCClientID,
   353  						"exp": time.Now().Add(time.Second * 1200).Unix(),
   354  					},
   355  					defaultStubAccessToken,
   356  					defaultStubRefreshToken,
   357  				))
   358  				configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
   359  			},
   360  			assertErrFn: func(t *testing.T, errorToCheck error) {
   361  				urlError, ok := errorToCheck.(*url.Error)
   362  				require.True(t, ok)
   363  				assert.Equal(
   364  					t,
   365  					"failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: refresh token is expired\n",
   366  					urlError.Err.Error(),
   367  				)
   368  			},
   369  		},
   370  	}
   371  
   372  	oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" })
   373  
   374  	tokenURL, err := oidcServer.TokenURL()
   375  	require.NoError(t, err)
   376  
   377  	for _, tt := range tests {
   378  		t.Run(tt.name, func(t *testing.T) {
   379  			expiredIDToken, stubRefreshToken := fetchExpiredToken(t, oidcServer, caCert, signingPrivateKey)
   380  			clientConfig := configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, expiredIDToken, stubRefreshToken, oidcServer.URL())
   381  			expiredClient := kubernetes.NewForConfigOrDie(clientConfig)
   382  			configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
   383  
   384  			ctx := testContext(t)
   385  			_, err = expiredClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
   386  			assert.Error(t, err)
   387  
   388  			tt.configureUpdatingTokenBehaviour(t, oidcServer, signingPrivateKey)
   389  			idToken, stubRefreshToken := fetchOIDCCredentials(t, tokenURL, caCert)
   390  			clientConfig = configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServer.URL())
   391  			expectedOkClient := kubernetes.NewForConfigOrDie(clientConfig)
   392  			_, err = expectedOkClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
   393  
   394  			tt.assertErrFn(t, err)
   395  		})
   396  	}
   397  }
   398  
   399  func TestStructuredAuthenticationConfigCEL(t *testing.T) {
   400  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
   401  
   402  	tests := []struct {
   403  		name                    string
   404  		authConfigFn            authenticationConfigFunc
   405  		configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) (
   406  			oidcServer *utilsoidc.TestServer,
   407  			apiServer *kubeapiserverapptesting.TestServer,
   408  			signingPrivateKey *rsa.PrivateKey,
   409  			caCertContent []byte,
   410  			caFilePath string,
   411  		)
   412  		configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
   413  		configureClient              func(
   414  			t *testing.T,
   415  			restCfg *rest.Config,
   416  			caCert []byte,
   417  			certPath,
   418  			oidcServerURL,
   419  			oidcServerTokenURL string,
   420  		) kubernetes.Interface
   421  		assertErrFn func(t *testing.T, errorToCheck error)
   422  		wantUser    *authenticationv1.UserInfo
   423  	}{
   424  		{
   425  			name: "username CEL expression is ok",
   426  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   427  				return fmt.Sprintf(`
   428  apiVersion: apiserver.config.k8s.io/v1alpha1
   429  kind: AuthenticationConfiguration
   430  jwt:
   431  - issuer:
   432      url: %s
   433      audiences:
   434      - %s
   435      certificateAuthority: |
   436          %s
   437    claimMappings:
   438      username:
   439        expression: "'k8s-' + claims.sub"
   440  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   441  			},
   442  			configureInfrastructure: configureTestInfrastructure,
   443  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   444  				idTokenLifetime := time.Second * 1200
   445  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   446  					t,
   447  					signingPrivateKey,
   448  					map[string]interface{}{
   449  						"iss": oidcServer.URL(),
   450  						"sub": defaultOIDCClaimedUsername,
   451  						"aud": defaultOIDCClientID,
   452  						"exp": time.Now().Add(idTokenLifetime).Unix(),
   453  					},
   454  					defaultStubAccessToken,
   455  					defaultStubRefreshToken,
   456  				))
   457  			},
   458  			configureClient: configureClientFetchingOIDCCredentials,
   459  			assertErrFn: func(t *testing.T, errorToCheck error) {
   460  				assert.NoError(t, errorToCheck)
   461  			},
   462  			wantUser: &authenticationv1.UserInfo{
   463  				Username: "k8s-john_doe",
   464  				Groups:   []string{"system:authenticated"},
   465  			},
   466  		},
   467  		{
   468  			name: "groups CEL expression is ok",
   469  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   470  				return fmt.Sprintf(`
   471  apiVersion: apiserver.config.k8s.io/v1alpha1
   472  kind: AuthenticationConfiguration
   473  jwt:
   474  - issuer:
   475      url: %s
   476      audiences:
   477      - %s
   478      certificateAuthority: |
   479          %s
   480    claimMappings:
   481      username:
   482        expression: "'k8s-' + claims.sub"
   483      groups:
   484        expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)'
   485  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   486  			},
   487  			configureInfrastructure: configureTestInfrastructure,
   488  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   489  				idTokenLifetime := time.Second * 1200
   490  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   491  					t,
   492  					signingPrivateKey,
   493  					map[string]interface{}{
   494  						"iss":         oidcServer.URL(),
   495  						"sub":         defaultOIDCClaimedUsername,
   496  						"aud":         defaultOIDCClientID,
   497  						"exp":         time.Now().Add(idTokenLifetime).Unix(),
   498  						"roles":       "foo,bar",
   499  						"other_roles": "baz,qux",
   500  					},
   501  					defaultStubAccessToken,
   502  					defaultStubRefreshToken,
   503  				))
   504  			},
   505  			configureClient: configureClientFetchingOIDCCredentials,
   506  			assertErrFn: func(t *testing.T, errorToCheck error) {
   507  				assert.NoError(t, errorToCheck)
   508  			},
   509  			wantUser: &authenticationv1.UserInfo{
   510  				Username: "k8s-john_doe",
   511  				Groups:   []string{"prefix:foo", "prefix:bar", "prefix:baz", "prefix:qux", "system:authenticated"},
   512  			},
   513  		},
   514  		{
   515  			name: "claim validation rule fails",
   516  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   517  				return fmt.Sprintf(`
   518  apiVersion: apiserver.config.k8s.io/v1alpha1
   519  kind: AuthenticationConfiguration
   520  jwt:
   521  - issuer:
   522      url: %s
   523      audiences:
   524      - %s
   525      certificateAuthority: |
   526          %s
   527    claimMappings:
   528      username:
   529        expression: "'k8s-' + claims.sub"
   530    claimValidationRules:
   531    - expression: 'claims.hd == "example.com"'
   532      message: "the hd claim must be set to example.com"
   533  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   534  			},
   535  			configureInfrastructure: configureTestInfrastructure,
   536  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   537  				idTokenLifetime := time.Second * 1200
   538  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   539  					t,
   540  					signingPrivateKey,
   541  					map[string]interface{}{
   542  						"iss": oidcServer.URL(),
   543  						"sub": defaultOIDCClaimedUsername,
   544  						"aud": defaultOIDCClientID,
   545  						"exp": time.Now().Add(idTokenLifetime).Unix(),
   546  						"hd":  "notexample.com",
   547  					},
   548  					defaultStubAccessToken,
   549  					defaultStubRefreshToken,
   550  				))
   551  			},
   552  			configureClient: configureClientFetchingOIDCCredentials,
   553  			assertErrFn: func(t *testing.T, errorToCheck error) {
   554  				assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
   555  			},
   556  		},
   557  		{
   558  			name: "extra mapping CEL expressions are ok",
   559  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   560  				return fmt.Sprintf(`
   561  apiVersion: apiserver.config.k8s.io/v1alpha1
   562  kind: AuthenticationConfiguration
   563  jwt:
   564  - issuer:
   565      url: %s
   566      audiences:
   567      - %s
   568      certificateAuthority: |
   569          %s
   570    claimMappings:
   571      username:
   572        expression: "'k8s-' + claims.sub"
   573      extra:
   574      - key: "example.org/foo"
   575        valueExpression: "'bar'"
   576      - key: "example.org/baz"
   577        valueExpression: "claims.baz"
   578    userValidationRules:
   579    - expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']"
   580      message: "example.org/foo must be bar and example.org/baz must be qux"
   581  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   582  			},
   583  			configureInfrastructure: configureTestInfrastructure,
   584  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   585  				idTokenLifetime := time.Second * 1200
   586  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   587  					t,
   588  					signingPrivateKey,
   589  					map[string]interface{}{
   590  						"iss": oidcServer.URL(),
   591  						"sub": defaultOIDCClaimedUsername,
   592  						"aud": defaultOIDCClientID,
   593  						"exp": time.Now().Add(idTokenLifetime).Unix(),
   594  						"baz": "qux",
   595  					},
   596  					defaultStubAccessToken,
   597  					defaultStubRefreshToken,
   598  				))
   599  			},
   600  			configureClient: configureClientFetchingOIDCCredentials,
   601  			assertErrFn: func(t *testing.T, errorToCheck error) {
   602  				assert.NoError(t, errorToCheck)
   603  			},
   604  			wantUser: &authenticationv1.UserInfo{
   605  				Username: "k8s-john_doe",
   606  				Groups:   []string{"system:authenticated"},
   607  				Extra: map[string]authenticationv1.ExtraValue{
   608  					"example.org/foo": {"bar"},
   609  					"example.org/baz": {"qux"},
   610  				},
   611  			},
   612  		},
   613  		{
   614  			name: "uid CEL expression is ok",
   615  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   616  				return fmt.Sprintf(`
   617  apiVersion: apiserver.config.k8s.io/v1alpha1
   618  kind: AuthenticationConfiguration
   619  jwt:
   620  - issuer:
   621      url: %s
   622      audiences:
   623      - %s
   624      certificateAuthority: |
   625          %s
   626    claimMappings:
   627      username:
   628        expression: "'k8s-' + claims.sub"
   629      uid:
   630        expression: "claims.uid"
   631  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   632  			},
   633  			configureInfrastructure: configureTestInfrastructure,
   634  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   635  				idTokenLifetime := time.Second * 1200
   636  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   637  					t,
   638  					signingPrivateKey,
   639  					map[string]interface{}{
   640  						"iss": oidcServer.URL(),
   641  						"sub": defaultOIDCClaimedUsername,
   642  						"aud": defaultOIDCClientID,
   643  						"exp": time.Now().Add(idTokenLifetime).Unix(),
   644  						"uid": "1234",
   645  					},
   646  					defaultStubAccessToken,
   647  					defaultStubRefreshToken,
   648  				))
   649  			},
   650  			configureClient: configureClientFetchingOIDCCredentials,
   651  			assertErrFn: func(t *testing.T, errorToCheck error) {
   652  				assert.NoError(t, errorToCheck)
   653  			},
   654  			wantUser: &authenticationv1.UserInfo{
   655  				Username: "k8s-john_doe",
   656  				Groups:   []string{"system:authenticated"},
   657  				UID:      "1234",
   658  			},
   659  		},
   660  		{
   661  			name: "user validation rule fails",
   662  			authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
   663  				return fmt.Sprintf(`
   664  apiVersion: apiserver.config.k8s.io/v1alpha1
   665  kind: AuthenticationConfiguration
   666  jwt:
   667  - issuer:
   668      url: %s
   669      audiences:
   670      - %s
   671      certificateAuthority: |
   672          %s
   673    claimMappings:
   674      username:
   675        expression: "'k8s-' + claims.sub"
   676      groups:
   677        expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "system:" + role)'
   678    userValidationRules:
   679    - expression: "user.groups.all(group, !group.startsWith('system:'))"
   680      message: "groups cannot used reserved system: prefix"
   681  `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
   682  			},
   683  			configureInfrastructure: configureTestInfrastructure,
   684  			configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   685  				idTokenLifetime := time.Second * 1200
   686  				oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   687  					t,
   688  					signingPrivateKey,
   689  					map[string]interface{}{
   690  						"iss":         oidcServer.URL(),
   691  						"sub":         defaultOIDCClaimedUsername,
   692  						"aud":         defaultOIDCClientID,
   693  						"exp":         time.Now().Add(idTokenLifetime).Unix(),
   694  						"roles":       "foo,bar",
   695  						"other_roles": "baz,qux",
   696  					},
   697  					defaultStubAccessToken,
   698  					defaultStubRefreshToken,
   699  				))
   700  			},
   701  			configureClient: configureClientFetchingOIDCCredentials,
   702  			assertErrFn: func(t *testing.T, errorToCheck error) {
   703  				assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
   704  			},
   705  			wantUser: nil,
   706  		},
   707  	}
   708  
   709  	for _, tt := range tests {
   710  		t.Run(tt.name, func(t *testing.T) {
   711  			oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn)
   712  
   713  			tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
   714  
   715  			tokenURL, err := oidcServer.TokenURL()
   716  			require.NoError(t, err)
   717  
   718  			client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
   719  
   720  			ctx := testContext(t)
   721  
   722  			if tt.wantUser != nil {
   723  				res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
   724  				require.NoError(t, err)
   725  				assert.Equal(t, *tt.wantUser, res.Status.UserInfo)
   726  			}
   727  
   728  			_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
   729  			tt.assertErrFn(t, err)
   730  		})
   731  	}
   732  }
   733  
   734  func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) (
   735  	oidcServer *utilsoidc.TestServer,
   736  	apiServer *kubeapiserverapptesting.TestServer,
   737  	signingPrivateKey *rsa.PrivateKey,
   738  	caCertContent []byte,
   739  	caFilePath string,
   740  ) {
   741  	t.Helper()
   742  
   743  	caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
   744  
   745  	signingPrivateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
   746  	require.NoError(t, err)
   747  
   748  	oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
   749  
   750  	authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
   751  	if len(authenticationConfig) > 0 {
   752  		apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
   753  	} else {
   754  		apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
   755  	}
   756  
   757  	oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey))
   758  
   759  	adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
   760  	configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
   761  
   762  	return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
   763  }
   764  
   765  func configureClientFetchingOIDCCredentials(t *testing.T, restCfg *rest.Config, caCert []byte, certPath, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
   766  	idToken, stubRefreshToken := fetchOIDCCredentials(t, oidcServerTokenURL, caCert)
   767  	clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServerURL)
   768  	return kubernetes.NewForConfigOrDie(clientConfig)
   769  }
   770  
   771  func configureClientWithEmptyIDToken(t *testing.T, restCfg *rest.Config, _ []byte, certPath, oidcServerURL, _ string) kubernetes.Interface {
   772  	emptyIDToken, stubRefreshToken := "", defaultStubRefreshToken
   773  	clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, emptyIDToken, stubRefreshToken, oidcServerURL)
   774  	return kubernetes.NewForConfigOrDie(clientConfig)
   775  }
   776  
   777  func configureRBAC(t *testing.T, clientset kubernetes.Interface, role *rbacv1.Role, binding *rbacv1.RoleBinding) {
   778  	t.Helper()
   779  
   780  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
   781  	defer cancel()
   782  
   783  	_, err := clientset.RbacV1().Roles(defaultNamespace).Create(ctx, role, metav1.CreateOptions{})
   784  	require.NoError(t, err)
   785  	_, err = clientset.RbacV1().RoleBindings(defaultNamespace).Create(ctx, binding, metav1.CreateOptions{})
   786  	require.NoError(t, err)
   787  }
   788  
   789  func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, caFilePath, idToken, refreshToken, oidcServerURL string) *rest.Config {
   790  	t.Helper()
   791  	cfg := rest.AnonymousClientConfig(config)
   792  	cfg.AuthProvider = &api.AuthProviderConfig{
   793  		Name: "oidc",
   794  		Config: map[string]string{
   795  			"client-id":                 clientID,
   796  			"id-token":                  idToken,
   797  			"idp-issuer-url":            oidcServerURL,
   798  			"idp-certificate-authority": caFilePath,
   799  			"refresh-token":             refreshToken,
   800  		},
   801  	}
   802  
   803  	return cfg
   804  }
   805  
   806  func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string) *kubeapiserverapptesting.TestServer {
   807  	t.Helper()
   808  
   809  	var customFlags []string
   810  	if authenticationConfigYAML != "" {
   811  		customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, authenticationConfigYAML))}
   812  	} else {
   813  		customFlags = []string{
   814  			fmt.Sprintf("--oidc-issuer-url=%s", oidcURL),
   815  			fmt.Sprintf("--oidc-client-id=%s", oidcClientID),
   816  			fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath),
   817  			fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix),
   818  		}
   819  	}
   820  	customFlags = append(customFlags, "--authorization-mode=RBAC")
   821  
   822  	server, err := kubeapiserverapptesting.StartTestServer(
   823  		t,
   824  		kubeapiserverapptesting.NewDefaultTestServerOptions(),
   825  		customFlags,
   826  		framework.SharedEtcd(),
   827  	)
   828  	require.NoError(t, err)
   829  
   830  	t.Cleanup(server.TearDownFn)
   831  
   832  	return &server
   833  }
   834  
   835  func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) {
   836  	t.Helper()
   837  
   838  	req, err := http.NewRequest(http.MethodGet, oidcTokenURL, http.NoBody)
   839  	require.NoError(t, err)
   840  
   841  	caPool := x509.NewCertPool()
   842  	ok := caPool.AppendCertsFromPEM(caCertContent)
   843  	require.True(t, ok)
   844  
   845  	client := http.Client{Transport: &http.Transport{
   846  		TLSClientConfig: &tls.Config{
   847  			RootCAs: caPool,
   848  		},
   849  	}}
   850  
   851  	token := new(utilsoidc.Token)
   852  
   853  	resp, err := client.Do(req)
   854  	require.NoError(t, err)
   855  
   856  	err = json.NewDecoder(resp.Body).Decode(token)
   857  	require.NoError(t, err)
   858  
   859  	return token.IDToken, token.RefreshToken
   860  }
   861  
   862  func fetchExpiredToken(t *testing.T, oidcServer *utilsoidc.TestServer, caCertContent []byte, signingPrivateKey *rsa.PrivateKey) (expiredToken, stubRefreshToken string) {
   863  	t.Helper()
   864  
   865  	tokenURL, err := oidcServer.TokenURL()
   866  	require.NoError(t, err)
   867  
   868  	configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
   869  	expiredToken, stubRefreshToken = fetchOIDCCredentials(t, tokenURL, caCertContent)
   870  
   871  	return expiredToken, stubRefreshToken
   872  }
   873  
   874  func configureOIDCServerToReturnExpiredIDToken(t *testing.T, returningExpiredTokenTimes int, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
   875  	t.Helper()
   876  
   877  	oidcServer.TokenHandler().EXPECT().Token().Times(returningExpiredTokenTimes).DoAndReturn(func() (utilsoidc.Token, error) {
   878  		token, err := utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
   879  			t,
   880  			signingPrivateKey,
   881  			map[string]interface{}{
   882  				"iss": oidcServer.URL(),
   883  				"sub": defaultOIDCClaimedUsername,
   884  				"aud": defaultOIDCClientID,
   885  				"exp": time.Now().Add(-time.Millisecond).Unix(),
   886  			},
   887  			defaultStubAccessToken,
   888  			defaultStubRefreshToken,
   889  		)()
   890  		return token, err
   891  	})
   892  }
   893  
   894  func configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer *utilsoidc.TestServer) {
   895  	oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrRefreshTokenExpired)
   896  }
   897  
   898  func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath string) {
   899  	t.Helper()
   900  
   901  	tempDir := t.TempDir()
   902  	certFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
   903  	keyFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.key")
   904  
   905  	cert, key, err := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
   906  	require.NoError(t, err)
   907  
   908  	return cert, key, certFilePath, keyFilePath
   909  }
   910  
   911  func writeTempFile(t *testing.T, content string) string {
   912  	t.Helper()
   913  	file, err := os.CreateTemp("", "oidc-test")
   914  	if err != nil {
   915  		t.Fatal(err)
   916  	}
   917  	t.Cleanup(func() {
   918  		if err := os.Remove(file.Name()); err != nil {
   919  			t.Fatal(err)
   920  		}
   921  	})
   922  	if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
   923  		t.Fatal(err)
   924  	}
   925  	return file.Name()
   926  }
   927  
   928  // indentCertificateAuthority indents the certificate authority to match
   929  // the format of the generated authentication config.
   930  func indentCertificateAuthority(caCert string) string {
   931  	return strings.ReplaceAll(caCert, "\n", "\n        ")
   932  }
   933  
   934  func testContext(t *testing.T) context.Context {
   935  	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
   936  	t.Cleanup(cancel)
   937  	return ctx
   938  }