1
16
17 package oidc
18
19 import (
20 "context"
21 "crypto/ecdsa"
22 "crypto/elliptic"
23 "crypto/rand"
24 "crypto/rsa"
25 "crypto/tls"
26 "crypto/x509"
27 "encoding/json"
28 "fmt"
29 "net"
30 "net/http"
31 "net/url"
32 "os"
33 "path/filepath"
34 "regexp"
35 "strings"
36 "testing"
37 "time"
38
39 "github.com/google/go-cmp/cmp"
40 "github.com/stretchr/testify/assert"
41 "github.com/stretchr/testify/require"
42 "gopkg.in/square/go-jose.v2"
43
44 authenticationv1 "k8s.io/api/authentication/v1"
45 rbacv1 "k8s.io/api/rbac/v1"
46 apierrors "k8s.io/apimachinery/pkg/api/errors"
47 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
48 utilrand "k8s.io/apimachinery/pkg/util/rand"
49 "k8s.io/apimachinery/pkg/util/wait"
50 "k8s.io/apiserver/pkg/features"
51 genericapiserver "k8s.io/apiserver/pkg/server"
52 authenticationconfigmetrics "k8s.io/apiserver/pkg/server/options/authenticationconfig/metrics"
53 utilfeature "k8s.io/apiserver/pkg/util/feature"
54 "k8s.io/client-go/kubernetes"
55 _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
56 "k8s.io/client-go/rest"
57 "k8s.io/client-go/tools/clientcmd/api"
58 certutil "k8s.io/client-go/util/cert"
59 featuregatetesting "k8s.io/component-base/featuregate/testing"
60 kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
61 "k8s.io/kubernetes/pkg/apis/rbac"
62 "k8s.io/kubernetes/pkg/kubeapiserver/options"
63 "k8s.io/kubernetes/test/integration/framework"
64 utilsoidc "k8s.io/kubernetes/test/utils/oidc"
65 utilsnet "k8s.io/utils/net"
66 )
67
68 const (
69 defaultNamespace = "default"
70 defaultOIDCClientID = "f403b682-603f-4ec9-b3e4-cf111ef36f7c"
71 defaultOIDCClaimedUsername = "john_doe"
72 defaultOIDCUsernamePrefix = "k8s-"
73 defaultRBACRoleName = "developer-role"
74 defaultRBACRoleBindingName = "developer-role-binding"
75
76 defaultStubRefreshToken = "_fake_refresh_token_"
77 defaultStubAccessToken = "_fake_access_token_"
78
79 rsaKeyBitSize = 2048
80 )
81
82 var (
83 defaultRole = &rbacv1.Role{
84 TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
85 ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleName},
86 Rules: []rbacv1.PolicyRule{
87 {
88 Verbs: []string{"list"},
89 Resources: []string{"pods"},
90 APIGroups: []string{""},
91 ResourceNames: []string{},
92 },
93 },
94 }
95 defaultRoleBinding = &rbacv1.RoleBinding{
96 TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"},
97 ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleBindingName},
98 Subjects: []rbacv1.Subject{
99 {
100 APIGroup: rbac.GroupName,
101 Kind: rbacv1.UserKind,
102 Name: defaultOIDCUsernamePrefix + defaultOIDCClaimedUsername,
103 },
104 },
105 RoleRef: rbacv1.RoleRef{
106 APIGroup: rbac.GroupName,
107 Kind: "Role",
108 Name: defaultRBACRoleName,
109 },
110 }
111 )
112
113
114 type authenticationConfigFunc func(t *testing.T, issuerURL, caCert string) string
115
116 type apiServerOIDCConfig struct {
117 oidcURL string
118 oidcClientID string
119 oidcCAFilePath string
120 oidcUsernamePrefix string
121 oidcUsernameClaim string
122 authenticationConfigYAML string
123 }
124
125 func TestOIDC(t *testing.T) {
126 t.Log("Testing OIDC authenticator with --oidc-* flags")
127 runTests(t, false)
128 }
129
130 func TestStructuredAuthenticationConfig(t *testing.T) {
131 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
132
133 t.Log("Testing OIDC authenticator with authentication config")
134 runTests(t, true)
135 }
136
137 func runTests(t *testing.T, useAuthenticationConfig bool) {
138 var tests = []singleTest[*rsa.PrivateKey, *rsa.PublicKey]{
139 {
140 name: "ID token is ok",
141 configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) (
142 oidcServer *utilsoidc.TestServer,
143 apiServer *kubeapiserverapptesting.TestServer,
144 signingPrivateKey *rsa.PrivateKey,
145 caCertContent []byte,
146 caFilePath string,
147 ) {
148 caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
149 signingPrivateKey, publicKey := keyFunc(t)
150 oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
151
152 if useAuthenticationConfig {
153 authenticationConfig := fmt.Sprintf(`
154 apiVersion: apiserver.config.k8s.io/v1beta1
155 kind: AuthenticationConfiguration
156 jwt:
157 - issuer:
158 url: %s
159 audiences:
160 - %s
161 certificateAuthority: |
162 %s
163 claimMappings:
164 username:
165 claim: user
166 prefix: %s
167 `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix)
168 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, &signingPrivateKey.PublicKey)
169 } else {
170 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{oidcURL: oidcServer.URL(), oidcClientID: defaultOIDCClientID,
171 oidcCAFilePath: caFilePath, oidcUsernamePrefix: defaultOIDCUsernamePrefix, oidcUsernameClaim: "user"}, &signingPrivateKey.PublicKey)
172 }
173 oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
174
175 adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
176 configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
177
178 return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
179 }, configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
180 idTokenLifetime := time.Second * 1200
181 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
182 t,
183 signingPrivateKey,
184
185
186 map[string]interface{}{
187 "iss": oidcServer.URL(),
188 "user": defaultOIDCClaimedUsername,
189 "aud": defaultOIDCClientID,
190 "exp": time.Now().Add(idTokenLifetime).Unix(),
191 },
192 defaultStubAccessToken,
193 defaultStubRefreshToken,
194 ))
195 },
196 configureClient: configureClientFetchingOIDCCredentials,
197 assertErrFn: func(t *testing.T, errorToCheck error) {
198 assert.NoError(t, errorToCheck)
199 },
200 },
201 {
202 name: "ID token is expired",
203 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
204 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
205 configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey)
206 },
207 configureClient: configureClientFetchingOIDCCredentials,
208 assertErrFn: func(t *testing.T, errorToCheck error) {
209 assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
210 },
211 },
212 {
213 name: "wrong client ID",
214 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
215 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {
216 oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID)
217 },
218 configureClient: configureClientWithEmptyIDToken,
219 assertErrFn: func(t *testing.T, errorToCheck error) {
220 urlError, ok := errorToCheck.(*url.Error)
221 require.True(t, ok)
222 assert.Equal(
223 t,
224 "failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: client ID is bad\n",
225 urlError.Err.Error(),
226 )
227 },
228 },
229 {
230 name: "client has wrong CA",
231 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
232 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {},
233 configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
234 tempDir := t.TempDir()
235 certFilePath := filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
236
237 _, _, wantErr := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
238 require.NoError(t, wantErr)
239
240 return configureClientWithEmptyIDToken(t, restCfg, caCert, certFilePath, oidcServerURL, oidcServerTokenURL)
241 },
242 assertErrFn: func(t *testing.T, errorToCheck error) {
243 expectedErr := new(x509.UnknownAuthorityError)
244 assert.ErrorAs(t, errorToCheck, expectedErr)
245 },
246 },
247 {
248 name: "refresh flow does not return ID Token",
249 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
250 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
251 configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
252 oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{
253 IDToken: "",
254 AccessToken: defaultStubAccessToken,
255 RefreshToken: defaultStubRefreshToken,
256 ExpiresIn: time.Now().Add(time.Second * 1200).Unix(),
257 }, nil)
258 },
259 configureClient: configureClientFetchingOIDCCredentials,
260 assertErrFn: func(t *testing.T, errorToCheck error) {
261 expectedError := new(apierrors.StatusError)
262 assert.ErrorAs(t, errorToCheck, &expectedError)
263 assert.Equal(
264 t,
265 `pods is forbidden: User "system:anonymous" cannot list resource "pods" in API group "" in the namespace "default"`,
266 errorToCheck.Error(),
267 )
268 },
269 },
270 {
271 name: "ID token signature can not be verified due to wrong JWKs",
272 configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) (
273 oidcServer *utilsoidc.TestServer,
274 apiServer *kubeapiserverapptesting.TestServer,
275 signingPrivateKey *rsa.PrivateKey,
276 caCertContent []byte,
277 caFilePath string,
278 ) {
279 caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
280
281 signingPrivateKey, _ = keyFunc(t)
282
283 oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
284
285 if useAuthenticationConfig {
286 authenticationConfig := fmt.Sprintf(`
287 apiVersion: apiserver.config.k8s.io/v1alpha1
288 kind: AuthenticationConfiguration
289 jwt:
290 - issuer:
291 url: %s
292 audiences:
293 - %s
294 certificateAuthority: |
295 %s
296 claimMappings:
297 username:
298 claim: sub
299 prefix: %s
300 `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix)
301 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, &signingPrivateKey.PublicKey)
302 } else {
303 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{oidcURL: oidcServer.URL(), oidcClientID: defaultOIDCClientID, oidcCAFilePath: caFilePath, oidcUsernamePrefix: defaultOIDCUsernamePrefix}, &signingPrivateKey.PublicKey)
304 }
305
306 adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
307 configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
308
309 anotherSigningPrivateKey, _ := keyFunc(t)
310
311 oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey))
312
313 return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
314 },
315 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
316 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
317 t,
318 signingPrivateKey,
319 map[string]interface{}{
320 "iss": oidcServer.URL(),
321 "sub": defaultOIDCClaimedUsername,
322 "aud": defaultOIDCClientID,
323 "exp": time.Now().Add(time.Second * 1200).Unix(),
324 },
325 defaultStubAccessToken,
326 defaultStubRefreshToken,
327 ))
328 },
329 configureClient: configureClientFetchingOIDCCredentials,
330 assertErrFn: func(t *testing.T, errorToCheck error) {
331 assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
332 },
333 },
334 {
335 name: "ID token is okay but username is empty",
336 configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) (
337 oidcServer *utilsoidc.TestServer,
338 apiServer *kubeapiserverapptesting.TestServer,
339 signingPrivateKey *rsa.PrivateKey,
340 caCertContent []byte,
341 caFilePath string,
342 ) {
343 caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
344
345 signingPrivateKey, _ = keyFunc(t)
346
347 oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
348
349 if useAuthenticationConfig {
350 authenticationConfig := fmt.Sprintf(`
351 apiVersion: apiserver.config.k8s.io/v1alpha1
352 kind: AuthenticationConfiguration
353 jwt:
354 - issuer:
355 url: %s
356 audiences:
357 - %s
358 certificateAuthority: |
359 %s
360 claimMappings:
361 username:
362 expression: claims.sub
363 `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)))
364 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, &signingPrivateKey.PublicKey)
365 } else {
366 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{
367 oidcURL: oidcServer.URL(), oidcClientID: defaultOIDCClientID, oidcCAFilePath: caFilePath, oidcUsernamePrefix: "-",
368 },
369 &signingPrivateKey.PublicKey)
370 }
371
372 oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey))
373
374 return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
375 },
376 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
377 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
378 t,
379 signingPrivateKey,
380 map[string]interface{}{
381 "iss": oidcServer.URL(),
382 "sub": "",
383 "aud": defaultOIDCClientID,
384 "exp": time.Now().Add(time.Second * 1200).Unix(),
385 },
386 defaultStubAccessToken,
387 defaultStubRefreshToken,
388 ))
389 },
390 configureClient: configureClientFetchingOIDCCredentials,
391 assertErrFn: func(t *testing.T, errorToCheck error) {
392 if useAuthenticationConfig {
393 assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
394 } else {
395
396 _ = assert.True(t, apierrors.IsForbidden(errorToCheck), errorToCheck) &&
397 assert.Equal(
398 t,
399 `pods is forbidden: User "" cannot list resource "pods" in API group "" in the namespace "default"`,
400 errorToCheck.Error(),
401 )
402 }
403 },
404 },
405 }
406
407 for _, tt := range tests {
408 t.Run(tt.name, singleTestRunner(useAuthenticationConfig, rsaGenerateKey, tt))
409 }
410
411 for _, tt := range []singleTest[*ecdsa.PrivateKey, *ecdsa.PublicKey]{
412 {
413 name: "ID token is ok",
414 configureInfrastructure: configureTestInfrastructure[*ecdsa.PrivateKey, *ecdsa.PublicKey],
415 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *ecdsa.PrivateKey) {
416 idTokenLifetime := time.Second * 1200
417 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
418 t,
419 signingPrivateKey,
420 map[string]interface{}{
421 "iss": oidcServer.URL(),
422 "sub": defaultOIDCClaimedUsername,
423 "aud": defaultOIDCClientID,
424 "exp": time.Now().Add(idTokenLifetime).Unix(),
425 },
426 defaultStubAccessToken,
427 defaultStubRefreshToken,
428 ))
429 },
430 configureClient: configureClientFetchingOIDCCredentials,
431 assertErrFn: func(t *testing.T, errorToCheck error) {
432 assert.NoError(t, errorToCheck)
433 },
434 },
435 } {
436 t.Run(tt.name, singleTestRunner(useAuthenticationConfig, ecdsaGenerateKey, tt))
437 }
438 }
439
440 type singleTest[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct {
441 name string
442 configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
443 oidcServer *utilsoidc.TestServer,
444 apiServer *kubeapiserverapptesting.TestServer,
445 signingPrivateKey K,
446 caCertContent []byte,
447 caFilePath string,
448 )
449 configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
450 configureClient func(
451 t *testing.T,
452 restCfg *rest.Config,
453 caCert []byte,
454 certPath,
455 oidcServerURL,
456 oidcServerTokenURL string,
457 ) kubernetes.Interface
458 assertErrFn func(t *testing.T, errorToCheck error)
459 }
460
461 func singleTestRunner[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](
462 useAuthenticationConfig bool,
463 keyFunc func(t *testing.T) (K, L),
464 tt singleTest[K, L],
465 ) func(t *testing.T) {
466 return func(t *testing.T) {
467 fn := func(t *testing.T, issuerURL, caCert string) string { return "" }
468 if useAuthenticationConfig {
469 fn = 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 claim: sub
483 prefix: %s
484 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix)
485 }
486 }
487 oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn, keyFunc)
488
489 tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
490
491 tokenURL, err := oidcServer.TokenURL()
492 require.NoError(t, err)
493
494 client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
495
496 ctx := testContext(t)
497 _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
498
499 tt.assertErrFn(t, err)
500 }
501 }
502
503 func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
504 type testRun[K utilsoidc.JosePrivateKey] struct {
505 name string
506 configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
507 assertErrFn func(t *testing.T, errorToCheck error)
508 }
509
510 var tests = []testRun[*rsa.PrivateKey]{
511 {
512 name: "cache returns stale client if refresh token is not updated in config",
513 configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
514 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
515 t,
516 signingPrivateKey,
517 map[string]interface{}{
518 "iss": oidcServer.URL(),
519 "sub": defaultOIDCClaimedUsername,
520 "aud": defaultOIDCClientID,
521 "exp": time.Now().Add(time.Second * 1200).Unix(),
522 },
523 defaultStubAccessToken,
524 defaultStubRefreshToken,
525 ))
526 configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
527 },
528 assertErrFn: func(t *testing.T, errorToCheck error) {
529 urlError, ok := errorToCheck.(*url.Error)
530 require.True(t, ok)
531 assert.Equal(
532 t,
533 "failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: refresh token is expired\n",
534 urlError.Err.Error(),
535 )
536 },
537 },
538 }
539
540 oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" }, rsaGenerateKey)
541
542 tokenURL, err := oidcServer.TokenURL()
543 require.NoError(t, err)
544
545 for _, tt := range tests {
546 t.Run(tt.name, func(t *testing.T) {
547 expiredIDToken, stubRefreshToken := fetchExpiredToken(t, oidcServer, caCert, signingPrivateKey)
548 clientConfig := configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, expiredIDToken, stubRefreshToken, oidcServer.URL())
549 expiredClient := kubernetes.NewForConfigOrDie(clientConfig)
550 configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
551
552 ctx := testContext(t)
553 _, err = expiredClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
554 assert.Error(t, err)
555
556 tt.configureUpdatingTokenBehaviour(t, oidcServer, signingPrivateKey)
557 idToken, stubRefreshToken := fetchOIDCCredentials(t, tokenURL, caCert)
558 clientConfig = configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServer.URL())
559 expectedOkClient := kubernetes.NewForConfigOrDie(clientConfig)
560 _, err = expectedOkClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
561
562 tt.assertErrFn(t, err)
563 })
564 }
565 }
566
567 func TestStructuredAuthenticationConfigCEL(t *testing.T) {
568 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
569
570 type testRun[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct {
571 name string
572 authConfigFn authenticationConfigFunc
573 configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
574 oidcServer *utilsoidc.TestServer,
575 apiServer *kubeapiserverapptesting.TestServer,
576 signingPrivateKey *rsa.PrivateKey,
577 caCertContent []byte,
578 caFilePath string,
579 )
580 configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K)
581 configureClient func(
582 t *testing.T,
583 restCfg *rest.Config,
584 caCert []byte,
585 certPath,
586 oidcServerURL,
587 oidcServerTokenURL string,
588 ) kubernetes.Interface
589 assertErrFn func(t *testing.T, errorToCheck error)
590 wantUser *authenticationv1.UserInfo
591 }
592
593 tests := []testRun[*rsa.PrivateKey, *rsa.PublicKey]{
594 {
595 name: "username CEL expression is ok",
596 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
597 return fmt.Sprintf(`
598 apiVersion: apiserver.config.k8s.io/v1alpha1
599 kind: AuthenticationConfiguration
600 jwt:
601 - issuer:
602 url: %s
603 audiences:
604 - %s
605 - another-audience
606 audienceMatchPolicy: MatchAny
607 certificateAuthority: |
608 %s
609 claimMappings:
610 username:
611 expression: "'k8s-' + claims.sub"
612 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
613 },
614 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
615 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
616 idTokenLifetime := time.Second * 1200
617 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
618 t,
619 signingPrivateKey,
620 map[string]interface{}{
621 "iss": oidcServer.URL(),
622 "sub": defaultOIDCClaimedUsername,
623 "aud": defaultOIDCClientID,
624 "exp": time.Now().Add(idTokenLifetime).Unix(),
625 },
626 defaultStubAccessToken,
627 defaultStubRefreshToken,
628 ))
629 },
630 configureClient: configureClientFetchingOIDCCredentials,
631 assertErrFn: func(t *testing.T, errorToCheck error) {
632 assert.NoError(t, errorToCheck)
633 },
634 wantUser: &authenticationv1.UserInfo{
635 Username: "k8s-john_doe",
636 Groups: []string{"system:authenticated"},
637 },
638 },
639 {
640 name: "groups CEL expression is ok",
641 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
642 return fmt.Sprintf(`
643 apiVersion: apiserver.config.k8s.io/v1alpha1
644 kind: AuthenticationConfiguration
645 jwt:
646 - issuer:
647 url: %s
648 audiences:
649 - %s
650 - another-audience
651 audienceMatchPolicy: MatchAny
652 certificateAuthority: |
653 %s
654 claimMappings:
655 username:
656 expression: "'k8s-' + claims.sub"
657 groups:
658 expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)'
659 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
660 },
661 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
662 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
663 idTokenLifetime := time.Second * 1200
664 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
665 t,
666 signingPrivateKey,
667 map[string]interface{}{
668 "iss": oidcServer.URL(),
669 "sub": defaultOIDCClaimedUsername,
670 "aud": defaultOIDCClientID,
671 "exp": time.Now().Add(idTokenLifetime).Unix(),
672 "roles": "foo,bar",
673 "other_roles": "baz,qux",
674 },
675 defaultStubAccessToken,
676 defaultStubRefreshToken,
677 ))
678 },
679 configureClient: configureClientFetchingOIDCCredentials,
680 assertErrFn: func(t *testing.T, errorToCheck error) {
681 assert.NoError(t, errorToCheck)
682 },
683 wantUser: &authenticationv1.UserInfo{
684 Username: "k8s-john_doe",
685 Groups: []string{"prefix:foo", "prefix:bar", "prefix:baz", "prefix:qux", "system:authenticated"},
686 },
687 },
688 {
689 name: "claim validation rule fails",
690 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
691 return fmt.Sprintf(`
692 apiVersion: apiserver.config.k8s.io/v1alpha1
693 kind: AuthenticationConfiguration
694 jwt:
695 - issuer:
696 url: %s
697 audiences:
698 - %s
699 - another-audience
700 audienceMatchPolicy: MatchAny
701 certificateAuthority: |
702 %s
703 claimMappings:
704 username:
705 expression: "'k8s-' + claims.sub"
706 claimValidationRules:
707 - expression: 'claims.hd == "example.com"'
708 message: "the hd claim must be set to example.com"
709 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
710 },
711 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
712 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
713 idTokenLifetime := time.Second * 1200
714 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
715 t,
716 signingPrivateKey,
717 map[string]interface{}{
718 "iss": oidcServer.URL(),
719 "sub": defaultOIDCClaimedUsername,
720 "aud": defaultOIDCClientID,
721 "exp": time.Now().Add(idTokenLifetime).Unix(),
722 "hd": "notexample.com",
723 },
724 defaultStubAccessToken,
725 defaultStubRefreshToken,
726 ))
727 },
728 configureClient: configureClientFetchingOIDCCredentials,
729 assertErrFn: func(t *testing.T, errorToCheck error) {
730 assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
731 },
732 },
733 {
734 name: "extra mapping CEL expressions are ok",
735 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
736 return fmt.Sprintf(`
737 apiVersion: apiserver.config.k8s.io/v1alpha1
738 kind: AuthenticationConfiguration
739 jwt:
740 - issuer:
741 url: %s
742 audiences:
743 - %s
744 - another-audience
745 audienceMatchPolicy: MatchAny
746 certificateAuthority: |
747 %s
748 claimMappings:
749 username:
750 expression: "'k8s-' + claims.sub"
751 extra:
752 - key: "example.org/foo"
753 valueExpression: "'bar'"
754 - key: "example.org/baz"
755 valueExpression: "claims.baz"
756 userValidationRules:
757 - expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']"
758 message: "example.org/foo must be bar and example.org/baz must be qux"
759 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
760 },
761 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
762 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
763 idTokenLifetime := time.Second * 1200
764 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
765 t,
766 signingPrivateKey,
767 map[string]interface{}{
768 "iss": oidcServer.URL(),
769 "sub": defaultOIDCClaimedUsername,
770 "aud": defaultOIDCClientID,
771 "exp": time.Now().Add(idTokenLifetime).Unix(),
772 "baz": "qux",
773 },
774 defaultStubAccessToken,
775 defaultStubRefreshToken,
776 ))
777 },
778 configureClient: configureClientFetchingOIDCCredentials,
779 assertErrFn: func(t *testing.T, errorToCheck error) {
780 assert.NoError(t, errorToCheck)
781 },
782 wantUser: &authenticationv1.UserInfo{
783 Username: "k8s-john_doe",
784 Groups: []string{"system:authenticated"},
785 Extra: map[string]authenticationv1.ExtraValue{
786 "example.org/foo": {"bar"},
787 "example.org/baz": {"qux"},
788 },
789 },
790 },
791 {
792 name: "uid CEL expression is ok",
793 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
794 return fmt.Sprintf(`
795 apiVersion: apiserver.config.k8s.io/v1alpha1
796 kind: AuthenticationConfiguration
797 jwt:
798 - issuer:
799 url: %s
800 audiences:
801 - %s
802 - another-audience
803 audienceMatchPolicy: MatchAny
804 certificateAuthority: |
805 %s
806 claimMappings:
807 username:
808 expression: "'k8s-' + claims.sub"
809 uid:
810 expression: "claims.uid"
811 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
812 },
813 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
814 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
815 idTokenLifetime := time.Second * 1200
816 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
817 t,
818 signingPrivateKey,
819 map[string]interface{}{
820 "iss": oidcServer.URL(),
821 "sub": defaultOIDCClaimedUsername,
822 "aud": defaultOIDCClientID,
823 "exp": time.Now().Add(idTokenLifetime).Unix(),
824 "uid": "1234",
825 },
826 defaultStubAccessToken,
827 defaultStubRefreshToken,
828 ))
829 },
830 configureClient: configureClientFetchingOIDCCredentials,
831 assertErrFn: func(t *testing.T, errorToCheck error) {
832 assert.NoError(t, errorToCheck)
833 },
834 wantUser: &authenticationv1.UserInfo{
835 Username: "k8s-john_doe",
836 Groups: []string{"system:authenticated"},
837 UID: "1234",
838 },
839 },
840 {
841 name: "user validation rule fails",
842 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
843 return fmt.Sprintf(`
844 apiVersion: apiserver.config.k8s.io/v1alpha1
845 kind: AuthenticationConfiguration
846 jwt:
847 - issuer:
848 url: %s
849 audiences:
850 - %s
851 - another-audience
852 audienceMatchPolicy: MatchAny
853 certificateAuthority: |
854 %s
855 claimMappings:
856 username:
857 expression: "'k8s-' + claims.sub"
858 groups:
859 expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "system:" + role)'
860 userValidationRules:
861 - expression: "user.groups.all(group, !group.startsWith('system:'))"
862 message: "groups cannot used reserved system: prefix"
863 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
864 },
865 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
866 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
867 idTokenLifetime := time.Second * 1200
868 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
869 t,
870 signingPrivateKey,
871 map[string]interface{}{
872 "iss": oidcServer.URL(),
873 "sub": defaultOIDCClaimedUsername,
874 "aud": defaultOIDCClientID,
875 "exp": time.Now().Add(idTokenLifetime).Unix(),
876 "roles": "foo,bar",
877 "other_roles": "baz,qux",
878 },
879 defaultStubAccessToken,
880 defaultStubRefreshToken,
881 ))
882 },
883 configureClient: configureClientFetchingOIDCCredentials,
884 assertErrFn: func(t *testing.T, errorToCheck error) {
885 assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
886 },
887 wantUser: nil,
888 },
889 {
890 name: "multiple audiences check with claim validation rule is ok",
891 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
892 return fmt.Sprintf(`
893 apiVersion: apiserver.config.k8s.io/v1alpha1
894 kind: AuthenticationConfiguration
895 jwt:
896 - issuer:
897 url: %s
898 audiences:
899 - baz
900 - foo
901 audienceMatchPolicy: MatchAny
902 certificateAuthority: |
903 %s
904 claimMappings:
905 username:
906 expression: "'k8s-' + claims.sub"
907 uid:
908 expression: "claims.uid"
909 claimValidationRules:
910 - expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])'
911 message: 'aud claim must be exactly match list ["bar", "foo", "baz"]'
912 `, issuerURL, indentCertificateAuthority(caCert))
913 },
914 configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey],
915 configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
916 idTokenLifetime := time.Second * 1200
917 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
918 t,
919 signingPrivateKey,
920 map[string]interface{}{
921 "iss": oidcServer.URL(),
922 "sub": defaultOIDCClaimedUsername,
923 "aud": []string{"foo", "bar", "baz"},
924 "exp": time.Now().Add(idTokenLifetime).Unix(),
925 "uid": "1234",
926 },
927 defaultStubAccessToken,
928 defaultStubRefreshToken,
929 ))
930 },
931 configureClient: configureClientFetchingOIDCCredentials,
932 assertErrFn: func(t *testing.T, errorToCheck error) {
933 assert.NoError(t, errorToCheck)
934 },
935 wantUser: &authenticationv1.UserInfo{
936 Username: "k8s-john_doe",
937 Groups: []string{"system:authenticated"},
938 UID: "1234",
939 },
940 },
941 }
942
943 for _, tt := range tests {
944 t.Run(tt.name, func(t *testing.T) {
945 oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn, rsaGenerateKey)
946
947 tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
948
949 tokenURL, err := oidcServer.TokenURL()
950 require.NoError(t, err)
951
952 client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
953
954 ctx := testContext(t)
955
956 if tt.wantUser != nil {
957 res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
958 require.NoError(t, err)
959 assert.Equal(t, *tt.wantUser, res.Status.UserInfo)
960 }
961
962 _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
963 tt.assertErrFn(t, err)
964 })
965 }
966 }
967
968 func TestStructuredAuthenticationConfigReload(t *testing.T) {
969 genericapiserver.SetHostnameFuncForTests("testAPIServerID")
970 const hardCodedTokenCacheTTLAndPollInterval = 10 * time.Second
971
972 origUpdateAuthenticationConfigTimeout := options.UpdateAuthenticationConfigTimeout
973 t.Cleanup(func() { options.UpdateAuthenticationConfigTimeout = origUpdateAuthenticationConfigTimeout })
974 options.UpdateAuthenticationConfigTimeout = 2 * hardCodedTokenCacheTTLAndPollInterval
975
976 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
977
978 tests := []struct {
979 name string
980 authConfigFn, newAuthConfigFn authenticationConfigFunc
981 assertErrFn, newAssertErrFn func(t *testing.T, errorToCheck error)
982 wantUser, newWantUser *authenticationv1.UserInfo
983 ignoreTransitionErrFn func(error) bool
984 waitAfterConfigSwap bool
985 wantMetricStrings []string
986 }{
987 {
988 name: "old valid config to new valid config",
989 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
990 return fmt.Sprintf(`
991 apiVersion: apiserver.config.k8s.io/v1alpha1
992 kind: AuthenticationConfiguration
993 jwt:
994 - issuer:
995 url: %s
996 audiences:
997 - %s
998 - another-audience
999 audienceMatchPolicy: MatchAny
1000 certificateAuthority: |
1001 %s
1002 claimMappings:
1003 username:
1004 expression: "'k8s-' + claims.sub"
1005 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1006 },
1007 newAuthConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1008 return fmt.Sprintf(`
1009 apiVersion: apiserver.config.k8s.io/v1alpha1
1010 kind: AuthenticationConfiguration
1011 jwt:
1012 - issuer:
1013 url: %s
1014 audiences:
1015 - %s
1016 - another-audience
1017 audienceMatchPolicy: MatchAny
1018 certificateAuthority: |
1019 %s
1020 claimMappings:
1021 username:
1022 expression: "'panda-' + claims.sub" # this is the only new part of the config
1023 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1024 },
1025 assertErrFn: func(t *testing.T, errorToCheck error) {
1026 assert.NoError(t, errorToCheck)
1027 },
1028 wantUser: &authenticationv1.UserInfo{
1029 Username: "k8s-john_doe",
1030 Groups: []string{"system:authenticated"},
1031 },
1032 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1033 _ = assert.True(t, apierrors.IsForbidden(errorToCheck)) &&
1034 assert.Equal(
1035 t,
1036 `pods is forbidden: User "panda-john_doe" cannot list resource "pods" in API group "" in the namespace "default"`,
1037 errorToCheck.Error(),
1038 )
1039 },
1040 newWantUser: &authenticationv1.UserInfo{
1041 Username: "panda-john_doe",
1042 Groups: []string{"system:authenticated"},
1043 },
1044 wantMetricStrings: []string{
1045 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} FP`,
1046 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} 1`,
1047 },
1048 },
1049 {
1050 name: "old empty config to new valid config",
1051 authConfigFn: func(t *testing.T, _, _ string) string {
1052 return `
1053 apiVersion: apiserver.config.k8s.io/v1alpha1
1054 kind: AuthenticationConfiguration
1055 `
1056 },
1057 newAuthConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1058 return fmt.Sprintf(`
1059 apiVersion: apiserver.config.k8s.io/v1alpha1
1060 kind: AuthenticationConfiguration
1061 jwt:
1062 - issuer:
1063 url: %s
1064 audiences:
1065 - %s
1066 - another-audience
1067 audienceMatchPolicy: MatchAny
1068 certificateAuthority: |
1069 %s
1070 claimMappings:
1071 username:
1072 expression: "'snorlax-' + claims.sub"
1073 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1074 },
1075 assertErrFn: func(t *testing.T, errorToCheck error) {
1076 assert.True(t, apierrors.IsUnauthorized(errorToCheck))
1077 },
1078 wantUser: nil,
1079 ignoreTransitionErrFn: apierrors.IsUnauthorized,
1080 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1081 _ = assert.True(t, apierrors.IsForbidden(errorToCheck)) &&
1082 assert.Equal(
1083 t,
1084 `pods is forbidden: User "snorlax-john_doe" cannot list resource "pods" in API group "" in the namespace "default"`,
1085 errorToCheck.Error(),
1086 )
1087 },
1088 newWantUser: &authenticationv1.UserInfo{
1089 Username: "snorlax-john_doe",
1090 Groups: []string{"system:authenticated"},
1091 },
1092 wantMetricStrings: []string{
1093 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} FP`,
1094 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} 1`,
1095 },
1096 },
1097 {
1098 name: "old invalid config to new valid config",
1099 authConfigFn: func(t *testing.T, issuerURL, _ string) string {
1100 return fmt.Sprintf(`
1101 apiVersion: apiserver.config.k8s.io/v1alpha1
1102 kind: AuthenticationConfiguration
1103 jwt:
1104 - issuer:
1105 url: %s
1106 audiences:
1107 - %s
1108 - another-audience
1109 audienceMatchPolicy: MatchAny
1110 certificateAuthority: "" # missing CA
1111 claimMappings:
1112 username:
1113 expression: "'k8s-' + claims.sub"
1114 `, issuerURL, defaultOIDCClientID)
1115 },
1116 newAuthConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1117 return fmt.Sprintf(`
1118 apiVersion: apiserver.config.k8s.io/v1alpha1
1119 kind: AuthenticationConfiguration
1120 jwt:
1121 - issuer:
1122 url: %s
1123 audiences:
1124 - %s
1125 - another-audience
1126 audienceMatchPolicy: MatchAny
1127 # this is the only new part of the config
1128 certificateAuthority: |
1129 %s
1130 claimMappings:
1131 username:
1132 expression: "'k8s-' + claims.sub"
1133 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1134 },
1135 assertErrFn: func(t *testing.T, errorToCheck error) {
1136 assert.True(t, apierrors.IsUnauthorized(errorToCheck))
1137 },
1138 wantUser: nil,
1139 ignoreTransitionErrFn: apierrors.IsUnauthorized,
1140 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1141 assert.NoError(t, errorToCheck)
1142 },
1143 newWantUser: &authenticationv1.UserInfo{
1144 Username: "k8s-john_doe",
1145 Groups: []string{"system:authenticated"},
1146 },
1147 wantMetricStrings: []string{
1148 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} FP`,
1149 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} 1`,
1150 },
1151 },
1152 {
1153 name: "old valid config to new structurally invalid config (should be ignored)",
1154 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1155 return fmt.Sprintf(`
1156 apiVersion: apiserver.config.k8s.io/v1alpha1
1157 kind: AuthenticationConfiguration
1158 jwt:
1159 - issuer:
1160 url: %s
1161 audiences:
1162 - %s
1163 - another-audience
1164 audienceMatchPolicy: MatchAny
1165 certificateAuthority: |
1166 %s
1167 claimMappings:
1168 username:
1169 expression: "'k8s-' + claims.sub"
1170 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1171 },
1172 newAuthConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1173 return fmt.Sprintf(`
1174 apiVersion: apiserver.config.k8s.io/v1alpha1
1175 kind: AuthenticationConfiguration
1176 jwt:
1177 - issuer:
1178 url: %s
1179 audiences:
1180 - %s
1181 - another-audience
1182 audienceMatchPolicy: MatchAny
1183 certificateAuthority: |
1184 %s
1185 claimMappings:
1186 username:
1187 expression: "'k8s-' + claimss.sub" # has typo
1188 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1189 },
1190 assertErrFn: func(t *testing.T, errorToCheck error) {
1191 assert.NoError(t, errorToCheck)
1192 },
1193 wantUser: &authenticationv1.UserInfo{
1194 Username: "k8s-john_doe",
1195 Groups: []string{"system:authenticated"},
1196 },
1197 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1198 assert.NoError(t, errorToCheck)
1199 },
1200 newWantUser: &authenticationv1.UserInfo{
1201 Username: "k8s-john_doe",
1202 Groups: []string{"system:authenticated"},
1203 },
1204 waitAfterConfigSwap: true,
1205 wantMetricStrings: []string{
1206 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="failure"} FP`,
1207 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="failure"} 1`,
1208 },
1209 },
1210 {
1211 name: "old valid config to new valid empty config (should cause tokens to stop working)",
1212 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1213 return fmt.Sprintf(`
1214 apiVersion: apiserver.config.k8s.io/v1alpha1
1215 kind: AuthenticationConfiguration
1216 jwt:
1217 - issuer:
1218 url: %s
1219 audiences:
1220 - %s
1221 - another-audience
1222 audienceMatchPolicy: MatchAny
1223 certificateAuthority: |
1224 %s
1225 claimMappings:
1226 username:
1227 expression: "'k8s-' + claims.sub"
1228 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1229 },
1230 newAuthConfigFn: func(t *testing.T, _, _ string) string {
1231 return `
1232 apiVersion: apiserver.config.k8s.io/v1alpha1
1233 kind: AuthenticationConfiguration
1234 `
1235 },
1236 assertErrFn: func(t *testing.T, errorToCheck error) {
1237 assert.NoError(t, errorToCheck)
1238 },
1239 wantUser: &authenticationv1.UserInfo{
1240 Username: "k8s-john_doe",
1241 Groups: []string{"system:authenticated"},
1242 },
1243 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1244 assert.True(t, apierrors.IsUnauthorized(errorToCheck))
1245 },
1246 newWantUser: nil,
1247 waitAfterConfigSwap: true,
1248 wantMetricStrings: []string{
1249 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} FP`,
1250 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} 1`,
1251 },
1252 },
1253 {
1254 name: "old valid config to new valid config with typo (should be ignored)",
1255 authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
1256 return fmt.Sprintf(`
1257 apiVersion: apiserver.config.k8s.io/v1alpha1
1258 kind: AuthenticationConfiguration
1259 jwt:
1260 - issuer:
1261 url: %s
1262 audiences:
1263 - %s
1264 - another-audience
1265 audienceMatchPolicy: MatchAny
1266 certificateAuthority: |
1267 %s
1268 claimMappings:
1269 username:
1270 expression: "'k8s-' + claims.sub"
1271 `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
1272 },
1273 newAuthConfigFn: func(t *testing.T, issuerURL, _ string) string {
1274 return fmt.Sprintf(`
1275 apiVersion: apiserver.config.k8s.io/v1alpha1
1276 kind: AuthenticationConfiguration
1277 jwt:
1278 - issuer:
1279 url: %s
1280 audiences:
1281 - %s
1282 - another-audience
1283 audienceMatchPolicy: MatchAny
1284 certificateAuthority: "" # missing CA
1285 claimMappings:
1286 username:
1287 expression: "'k8s-' + claims.sub"
1288 `, issuerURL, defaultOIDCClientID)
1289 },
1290 assertErrFn: func(t *testing.T, errorToCheck error) {
1291 assert.NoError(t, errorToCheck)
1292 },
1293 wantUser: &authenticationv1.UserInfo{
1294 Username: "k8s-john_doe",
1295 Groups: []string{"system:authenticated"},
1296 },
1297 newAssertErrFn: func(t *testing.T, errorToCheck error) {
1298 assert.NoError(t, errorToCheck)
1299 },
1300 newWantUser: &authenticationv1.UserInfo{
1301 Username: "k8s-john_doe",
1302 Groups: []string{"system:authenticated"},
1303 },
1304 waitAfterConfigSwap: true,
1305 wantMetricStrings: []string{
1306 `apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="failure"} FP`,
1307 `apiserver_authentication_config_controller_automatic_reloads_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="failure"} 1`,
1308 },
1309 },
1310 }
1311
1312 for _, tt := range tests {
1313 t.Run(tt.name, func(t *testing.T) {
1314 authenticationconfigmetrics.ResetMetricsForTest()
1315 defer authenticationconfigmetrics.ResetMetricsForTest()
1316
1317 ctx := testContext(t)
1318
1319 oidcServer, apiServer, caCert, certPath := configureBasicTestInfrastructureWithRandomKeyType(t, tt.authConfigFn)
1320
1321 tokenURL, err := oidcServer.TokenURL()
1322 require.NoError(t, err)
1323
1324 client := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
1325
1326 if tt.wantUser != nil {
1327 res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
1328 require.NoError(t, err)
1329 assert.Equal(t, *tt.wantUser, res.Status.UserInfo)
1330 }
1331
1332 _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
1333 tt.assertErrFn(t, err)
1334
1335
1336 tempFile, err := os.CreateTemp("", "tempfile")
1337 require.NoError(t, err)
1338 defer func() {
1339 _ = tempFile.Close()
1340 }()
1341
1342
1343 _, err = tempFile.Write([]byte(tt.newAuthConfigFn(t, oidcServer.URL(), string(caCert))))
1344 require.NoError(t, err)
1345
1346
1347 err = os.Rename(tempFile.Name(), apiServer.ServerOpts.Authentication.AuthenticationConfigFile)
1348 require.NoError(t, err)
1349
1350 if tt.waitAfterConfigSwap {
1351 time.Sleep(options.UpdateAuthenticationConfigTimeout + hardCodedTokenCacheTTLAndPollInterval)
1352 }
1353
1354 if tt.newWantUser != nil {
1355 start := time.Now()
1356 err = wait.PollUntilContextTimeout(ctx, time.Second, 3*hardCodedTokenCacheTTLAndPollInterval, true, func(ctx context.Context) (done bool, err error) {
1357 res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
1358 if err != nil {
1359 if tt.ignoreTransitionErrFn != nil && tt.ignoreTransitionErrFn(err) {
1360 return false, nil
1361 }
1362 return false, err
1363 }
1364
1365 diff := cmp.Diff(*tt.newWantUser, res.Status.UserInfo)
1366 if len(diff) > 0 && time.Since(start) > 2*hardCodedTokenCacheTTLAndPollInterval {
1367 t.Logf("%s saw new user diff:\n%s", t.Name(), diff)
1368 }
1369
1370 return len(diff) == 0, nil
1371 })
1372 require.NoError(t, err, "new authentication config not loaded")
1373 }
1374
1375 _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
1376 tt.newAssertErrFn(t, err)
1377
1378 adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
1379 body, err := adminClient.RESTClient().Get().AbsPath("/metrics").DoRaw(ctx)
1380 require.NoError(t, err)
1381 var gotMetricStrings []string
1382 trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`)
1383 for _, line := range strings.Split(string(body), "\n") {
1384 if strings.HasPrefix(line, "apiserver_authentication_config_controller_") {
1385 if strings.Contains(line, "_seconds") {
1386 line = trimFP.ReplaceAllString(line, `$1`) + "} FP"
1387 }
1388 gotMetricStrings = append(gotMetricStrings, line)
1389 }
1390 }
1391 if diff := cmp.Diff(tt.wantMetricStrings, gotMetricStrings); diff != "" {
1392 t.Errorf("unexpected metrics diff (-want +got): %s", diff)
1393 }
1394 })
1395 }
1396 }
1397
1398 func configureBasicTestInfrastructureWithRandomKeyType(t *testing.T, fn authenticationConfigFunc) (
1399 oidcServer *utilsoidc.TestServer,
1400 apiServer *kubeapiserverapptesting.TestServer,
1401 caCertContent []byte,
1402 caFilePath string,
1403 ) {
1404 t.Helper()
1405
1406 if randomBool() {
1407 return configureBasicTestInfrastructure(t, fn, rsaGenerateKey)
1408 }
1409
1410 return configureBasicTestInfrastructure(t, fn, ecdsaGenerateKey)
1411 }
1412
1413 func configureBasicTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
1414 oidcServer *utilsoidc.TestServer,
1415 apiServer *kubeapiserverapptesting.TestServer,
1416 caCertContent []byte,
1417 caFilePath string,
1418 ) {
1419 t.Helper()
1420
1421 oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath := configureTestInfrastructure(t, fn, keyFunc)
1422
1423 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
1424 t,
1425 signingPrivateKey,
1426 map[string]interface{}{
1427 "iss": oidcServer.URL(),
1428 "sub": defaultOIDCClaimedUsername,
1429 "aud": defaultOIDCClientID,
1430 "exp": time.Now().Add(10 * time.Minute).Unix(),
1431 },
1432 defaultStubAccessToken,
1433 defaultStubRefreshToken,
1434 ))
1435
1436 return oidcServer, apiServer, caCertContent, caFilePath
1437 }
1438
1439
1440
1441 func TestStructuredAuthenticationDiscoveryURL(t *testing.T) {
1442 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
1443
1444 tests := []struct {
1445 name string
1446 issuerURL string
1447 discoveryURL func(baseURL string) string
1448 }{
1449 {
1450 name: "discovery url and issuer url with no path",
1451 issuerURL: "https://example.com",
1452 discoveryURL: func(baseURL string) string { return baseURL },
1453 },
1454 {
1455 name: "discovery url has path, issuer url has no path",
1456 issuerURL: "https://example.com",
1457 discoveryURL: func(baseURL string) string { return fmt.Sprintf("%s/c/d/bar", baseURL) },
1458 },
1459 {
1460 name: "discovery url has no path, issuer url has path",
1461 issuerURL: "https://example.com/a/b/foo",
1462 discoveryURL: func(baseURL string) string { return baseURL },
1463 },
1464 {
1465 name: "discovery url and issuer url have paths",
1466 issuerURL: "https://example.com/a/b/foo",
1467 discoveryURL: func(baseURL string) string {
1468 return fmt.Sprintf("%s/c/d/bar", baseURL)
1469 },
1470 },
1471 }
1472
1473 for _, tt := range tests {
1474 t.Run(tt.name, func(t *testing.T) {
1475 caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
1476 signingPrivateKey, publicKey := rsaGenerateKey(t)
1477
1478
1479
1480 oidcServer := utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, tt.issuerURL)
1481 discoveryURL := strings.TrimSuffix(tt.discoveryURL(oidcServer.URL()), "/") + "/.well-known/openid-configuration"
1482
1483 authenticationConfig := fmt.Sprintf(`
1484 apiVersion: apiserver.config.k8s.io/v1alpha1
1485 kind: AuthenticationConfiguration
1486 jwt:
1487 - issuer:
1488 url: %s
1489 discoveryURL: %s
1490 audiences:
1491 - foo
1492 audienceMatchPolicy: MatchAny
1493 certificateAuthority: |
1494 %s
1495 claimMappings:
1496 username:
1497 expression: "'k8s-' + claims.sub"
1498 claimValidationRules:
1499 - expression: 'claims.hd == "example.com"'
1500 message: "the hd claim must be set to example.com"
1501 `, tt.issuerURL, discoveryURL, indentCertificateAuthority(string(caCertContent)))
1502
1503 oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
1504
1505 apiServer := startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, publicKey)
1506
1507 idTokenLifetime := time.Second * 1200
1508 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
1509 t,
1510 signingPrivateKey,
1511 map[string]interface{}{
1512 "iss": tt.issuerURL,
1513 "sub": defaultOIDCClaimedUsername,
1514 "aud": "foo",
1515 "exp": time.Now().Add(idTokenLifetime).Unix(),
1516 "hd": "example.com",
1517 },
1518 defaultStubAccessToken,
1519 defaultStubRefreshToken,
1520 ))
1521
1522 tokenURL, err := oidcServer.TokenURL()
1523 require.NoError(t, err)
1524
1525 client := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCertContent, caFilePath, oidcServer.URL(), tokenURL)
1526 ctx := testContext(t)
1527 res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
1528 require.NoError(t, err)
1529 assert.Equal(t, authenticationv1.UserInfo{
1530 Username: "k8s-john_doe",
1531 Groups: []string{"system:authenticated"},
1532 }, res.Status.UserInfo)
1533 })
1534 }
1535 }
1536
1537 func TestMultipleJWTAuthenticators(t *testing.T) {
1538 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
1539
1540 caCertContent1, _, caFilePath1, caKeyFilePath1 := generateCert(t)
1541 signingPrivateKey1, publicKey1 := rsaGenerateKey(t)
1542 oidcServer1 := utilsoidc.BuildAndRunTestServer(t, caFilePath1, caKeyFilePath1, "")
1543
1544 caCertContent2, _, caFilePath2, caKeyFilePath2 := generateCert(t)
1545 signingPrivateKey2, publicKey2 := rsaGenerateKey(t)
1546 oidcServer2 := utilsoidc.BuildAndRunTestServer(t, caFilePath2, caKeyFilePath2, "https://example.com")
1547
1548 authenticationConfig := fmt.Sprintf(`
1549 apiVersion: apiserver.config.k8s.io/v1alpha1
1550 kind: AuthenticationConfiguration
1551 jwt:
1552 - issuer:
1553 url: %s
1554 audiences:
1555 - foo
1556 audienceMatchPolicy: MatchAny
1557 certificateAuthority: |
1558 %s
1559 claimMappings:
1560 username:
1561 expression: "'k8s-' + claims.sub"
1562 claimValidationRules:
1563 - expression: 'claims.hd == "example.com"'
1564 message: "the hd claim must be set to example.com"
1565 - issuer:
1566 url: "https://example.com"
1567 discoveryURL: %s/.well-known/openid-configuration
1568 audiences:
1569 - bar
1570 audienceMatchPolicy: MatchAny
1571 certificateAuthority: |
1572 %s
1573 claimMappings:
1574 username:
1575 expression: "'k8s-' + claims.sub"
1576 groups:
1577 expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "system:" + role)'
1578 uid:
1579 expression: "claims.uid"
1580 `, oidcServer1.URL(), indentCertificateAuthority(string(caCertContent1)), oidcServer2.URL(), indentCertificateAuthority(string(caCertContent2)))
1581
1582 oidcServer1.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey1))
1583 oidcServer2.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey2))
1584
1585 apiServer := startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, publicKey1)
1586
1587 idTokenLifetime := time.Second * 1200
1588 oidcServer1.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
1589 t,
1590 signingPrivateKey1,
1591 map[string]interface{}{
1592 "iss": oidcServer1.URL(),
1593 "sub": defaultOIDCClaimedUsername,
1594 "aud": "foo",
1595 "exp": time.Now().Add(idTokenLifetime).Unix(),
1596 "hd": "example.com",
1597 },
1598 defaultStubAccessToken,
1599 defaultStubRefreshToken,
1600 ))
1601
1602 oidcServer2.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
1603 t,
1604 signingPrivateKey2,
1605 map[string]interface{}{
1606 "iss": "https://example.com",
1607 "sub": "not_john_doe",
1608 "aud": "bar",
1609 "roles": "role1,role2",
1610 "other_roles": "role3,role4",
1611 "exp": time.Now().Add(idTokenLifetime).Unix(),
1612 "uid": "1234",
1613 },
1614 defaultStubAccessToken,
1615 defaultStubRefreshToken,
1616 ))
1617
1618 tokenURL1, err := oidcServer1.TokenURL()
1619 require.NoError(t, err)
1620
1621 tokenURL2, err := oidcServer2.TokenURL()
1622 require.NoError(t, err)
1623
1624 client1 := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCertContent1, caFilePath1, oidcServer1.URL(), tokenURL1)
1625 client2 := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCertContent2, caFilePath2, oidcServer2.URL(), tokenURL2)
1626
1627 ctx := testContext(t)
1628 res, err := client1.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
1629 require.NoError(t, err)
1630 assert.Equal(t, authenticationv1.UserInfo{
1631 Username: "k8s-john_doe",
1632 Groups: []string{"system:authenticated"},
1633 }, res.Status.UserInfo)
1634
1635 res, err = client2.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
1636 require.NoError(t, err)
1637 assert.Equal(t, authenticationv1.UserInfo{
1638 Username: "k8s-not_john_doe",
1639 Groups: []string{"system:role1", "system:role2", "system:role3", "system:role4", "system:authenticated"},
1640 UID: "1234",
1641 }, res.Status.UserInfo)
1642 }
1643
1644 func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
1645 t.Helper()
1646
1647 privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
1648 require.NoError(t, err)
1649
1650 return privateKey, &privateKey.PublicKey
1651 }
1652
1653 func ecdsaGenerateKey(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) {
1654 t.Helper()
1655
1656 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
1657 require.NoError(t, err)
1658
1659 return privateKey, &privateKey.PublicKey
1660 }
1661
1662 func configureTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) (
1663 oidcServer *utilsoidc.TestServer,
1664 apiServer *kubeapiserverapptesting.TestServer,
1665 signingPrivateKey K,
1666 caCertContent []byte,
1667 caFilePath string,
1668 ) {
1669 t.Helper()
1670
1671 caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
1672
1673 signingPrivateKey, publicKey := keyFunc(t)
1674
1675 oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
1676
1677 authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
1678 if len(authenticationConfig) > 0 {
1679 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, publicKey)
1680 } else {
1681 apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{oidcURL: oidcServer.URL(), oidcClientID: defaultOIDCClientID, oidcCAFilePath: caFilePath, oidcUsernamePrefix: defaultOIDCUsernamePrefix}, publicKey)
1682 }
1683
1684 oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
1685
1686 adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
1687 configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
1688
1689 return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
1690 }
1691
1692 func configureClientFetchingOIDCCredentials(t *testing.T, restCfg *rest.Config, caCert []byte, certPath, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
1693 idToken, stubRefreshToken := fetchOIDCCredentials(t, oidcServerTokenURL, caCert)
1694 clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServerURL)
1695 return kubernetes.NewForConfigOrDie(clientConfig)
1696 }
1697
1698 func configureClientWithEmptyIDToken(t *testing.T, restCfg *rest.Config, _ []byte, certPath, oidcServerURL, _ string) kubernetes.Interface {
1699 emptyIDToken, stubRefreshToken := "", defaultStubRefreshToken
1700 clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, emptyIDToken, stubRefreshToken, oidcServerURL)
1701 return kubernetes.NewForConfigOrDie(clientConfig)
1702 }
1703
1704 func configureRBAC(t *testing.T, clientset kubernetes.Interface, role *rbacv1.Role, binding *rbacv1.RoleBinding) {
1705 t.Helper()
1706
1707 ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
1708 defer cancel()
1709
1710 _, err := clientset.RbacV1().Roles(defaultNamespace).Create(ctx, role, metav1.CreateOptions{})
1711 require.NoError(t, err)
1712 _, err = clientset.RbacV1().RoleBindings(defaultNamespace).Create(ctx, binding, metav1.CreateOptions{})
1713 require.NoError(t, err)
1714 }
1715
1716 func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, caFilePath, idToken, refreshToken, oidcServerURL string) *rest.Config {
1717 t.Helper()
1718 cfg := rest.AnonymousClientConfig(config)
1719 cfg.AuthProvider = &api.AuthProviderConfig{
1720 Name: "oidc",
1721 Config: map[string]string{
1722 "client-id": clientID,
1723 "id-token": idToken,
1724 "idp-issuer-url": oidcServerURL,
1725 "idp-certificate-authority": caFilePath,
1726 "refresh-token": refreshToken,
1727 },
1728 }
1729
1730 return cfg
1731 }
1732
1733 func startTestAPIServerForOIDC[L utilsoidc.JosePublicKey](t *testing.T, c apiServerOIDCConfig, publicKey L) *kubeapiserverapptesting.TestServer {
1734 t.Helper()
1735
1736 var customFlags []string
1737 if len(c.authenticationConfigYAML) > 0 {
1738 customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, c.authenticationConfigYAML))}
1739 } else {
1740 customFlags = []string{
1741 fmt.Sprintf("--oidc-issuer-url=%s", c.oidcURL),
1742 fmt.Sprintf("--oidc-client-id=%s", c.oidcClientID),
1743 fmt.Sprintf("--oidc-ca-file=%s", c.oidcCAFilePath),
1744 fmt.Sprintf("--oidc-username-prefix=%s", c.oidcUsernamePrefix),
1745 }
1746 if len(c.oidcUsernameClaim) > 0 {
1747 customFlags = append(customFlags, fmt.Sprintf("--oidc-username-claim=%s", c.oidcUsernameClaim))
1748 }
1749 customFlags = append(customFlags, maybeSetSigningAlgs(publicKey)...)
1750 }
1751 customFlags = append(customFlags, "--authorization-mode=RBAC")
1752
1753 server, err := kubeapiserverapptesting.StartTestServer(
1754 t,
1755 kubeapiserverapptesting.NewDefaultTestServerOptions(),
1756 customFlags,
1757 framework.SharedEtcd(),
1758 )
1759 require.NoError(t, err)
1760
1761 t.Cleanup(server.TearDownFn)
1762
1763 return &server
1764 }
1765
1766 func maybeSetSigningAlgs[K utilsoidc.JoseKey](key K) []string {
1767 alg := utilsoidc.GetSignatureAlgorithm(key)
1768 if alg == jose.RS256 && randomBool() {
1769 return nil
1770 }
1771 return []string{
1772 fmt.Sprintf("--oidc-signing-algs=%s", alg),
1773 }
1774 }
1775
1776 func randomBool() bool { return utilrand.Int()%2 == 1 }
1777
1778 func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) {
1779 t.Helper()
1780
1781 req, err := http.NewRequest(http.MethodGet, oidcTokenURL, http.NoBody)
1782 require.NoError(t, err)
1783
1784 caPool := x509.NewCertPool()
1785 ok := caPool.AppendCertsFromPEM(caCertContent)
1786 require.True(t, ok)
1787
1788 client := http.Client{Transport: &http.Transport{
1789 TLSClientConfig: &tls.Config{
1790 RootCAs: caPool,
1791 },
1792 }}
1793
1794 token := new(utilsoidc.Token)
1795
1796 resp, err := client.Do(req)
1797 require.NoError(t, err)
1798
1799 err = json.NewDecoder(resp.Body).Decode(token)
1800 require.NoError(t, err)
1801
1802 return token.IDToken, token.RefreshToken
1803 }
1804
1805 func fetchExpiredToken(t *testing.T, oidcServer *utilsoidc.TestServer, caCertContent []byte, signingPrivateKey *rsa.PrivateKey) (expiredToken, stubRefreshToken string) {
1806 t.Helper()
1807
1808 tokenURL, err := oidcServer.TokenURL()
1809 require.NoError(t, err)
1810
1811 configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
1812 expiredToken, stubRefreshToken = fetchOIDCCredentials(t, tokenURL, caCertContent)
1813
1814 return expiredToken, stubRefreshToken
1815 }
1816
1817 func configureOIDCServerToReturnExpiredIDToken(t *testing.T, returningExpiredTokenTimes int, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
1818 t.Helper()
1819
1820 oidcServer.TokenHandler().EXPECT().Token().Times(returningExpiredTokenTimes).DoAndReturn(func() (utilsoidc.Token, error) {
1821 token, err := utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
1822 t,
1823 signingPrivateKey,
1824 map[string]interface{}{
1825 "iss": oidcServer.URL(),
1826 "sub": defaultOIDCClaimedUsername,
1827 "aud": defaultOIDCClientID,
1828 "exp": time.Now().Add(-time.Millisecond).Unix(),
1829 },
1830 defaultStubAccessToken,
1831 defaultStubRefreshToken,
1832 )()
1833 return token, err
1834 })
1835 }
1836
1837 func configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer *utilsoidc.TestServer) {
1838 oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrRefreshTokenExpired)
1839 }
1840
1841 func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath string) {
1842 t.Helper()
1843
1844 tempDir := t.TempDir()
1845 certFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
1846 keyFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.key")
1847
1848 cert, key, err := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
1849 require.NoError(t, err)
1850
1851 return cert, key, certFilePath, keyFilePath
1852 }
1853
1854 func writeTempFile(t *testing.T, content string) string {
1855 t.Helper()
1856 file, err := os.CreateTemp("", "oidc-test")
1857 if err != nil {
1858 t.Fatal(err)
1859 }
1860 t.Cleanup(func() {
1861 if err := os.Remove(file.Name()); err != nil {
1862 t.Fatal(err)
1863 }
1864 })
1865 if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
1866 t.Fatal(err)
1867 }
1868 return file.Name()
1869 }
1870
1871
1872
1873 func indentCertificateAuthority(caCert string) string {
1874 return strings.ReplaceAll(caCert, "\n", "\n ")
1875 }
1876
1877 func testContext(t *testing.T) context.Context {
1878 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
1879 t.Cleanup(cancel)
1880 return ctx
1881 }
1882
View as plain text