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 }