k8s.io/apiserver@v0.31.1/plugin/pkg/authenticator/token/oidc/oidc_test.go (about) 1 /* 2 Copyright 2015 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 "bytes" 21 "context" 22 "crypto" 23 "crypto/x509" 24 "encoding/hex" 25 "encoding/json" 26 "encoding/pem" 27 "fmt" 28 "net/http" 29 "net/http/httptest" 30 "os" 31 "reflect" 32 "strings" 33 "testing" 34 "text/template" 35 "time" 36 37 "gopkg.in/square/go-jose.v2" 38 39 "k8s.io/apimachinery/pkg/util/wait" 40 "k8s.io/apiserver/pkg/apis/apiserver" 41 "k8s.io/apiserver/pkg/authentication/user" 42 "k8s.io/apiserver/pkg/features" 43 "k8s.io/apiserver/pkg/server/dynamiccertificates" 44 utilfeature "k8s.io/apiserver/pkg/util/feature" 45 featuregatetesting "k8s.io/component-base/featuregate/testing" 46 "k8s.io/component-base/metrics/testutil" 47 "k8s.io/klog/v2" 48 "k8s.io/utils/pointer" 49 ) 50 51 // utilities for loading JOSE keys. 52 53 func loadRSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { 54 return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { 55 key, err := x509.ParsePKCS1PrivateKey(b) 56 if err != nil { 57 return nil, err 58 } 59 return key.Public(), nil 60 }) 61 } 62 63 func loadRSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { 64 return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { 65 return x509.ParsePKCS1PrivateKey(b) 66 }) 67 } 68 69 func loadECDSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { 70 return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { 71 key, err := x509.ParseECPrivateKey(b) 72 if err != nil { 73 return nil, err 74 } 75 return key.Public(), nil 76 }) 77 } 78 79 func loadECDSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { 80 return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { 81 return x509.ParseECPrivateKey(b) 82 }) 83 } 84 85 func loadKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm, unmarshal func([]byte) (interface{}, error)) *jose.JSONWebKey { 86 data, err := os.ReadFile(filepath) 87 if err != nil { 88 t.Fatalf("load file: %v", err) 89 } 90 block, _ := pem.Decode(data) 91 if block == nil { 92 t.Fatalf("file contained no PEM encoded data: %s", filepath) 93 } 94 priv, err := unmarshal(block.Bytes) 95 if err != nil { 96 t.Fatalf("unmarshal key: %v", err) 97 } 98 key := &jose.JSONWebKey{Key: priv, Use: "sig", Algorithm: string(alg)} 99 thumbprint, err := key.Thumbprint(crypto.SHA256) 100 if err != nil { 101 t.Fatalf("computing thumbprint: %v", err) 102 } 103 key.KeyID = hex.EncodeToString(thumbprint) 104 return key 105 } 106 107 // staticKeySet implements oidc.KeySet. 108 type staticKeySet struct { 109 keys []*jose.JSONWebKey 110 } 111 112 func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) { 113 jws, err := jose.ParseSigned(jwt) 114 if err != nil { 115 return nil, err 116 } 117 if len(jws.Signatures) == 0 { 118 return nil, fmt.Errorf("jwt contained no signatures") 119 } 120 kid := jws.Signatures[0].Header.KeyID 121 122 for _, key := range s.keys { 123 if key.KeyID == kid { 124 return jws.Verify(key) 125 } 126 } 127 128 return nil, fmt.Errorf("no keys matches jwk keyid") 129 } 130 131 var ( 132 expired, _ = time.Parse(time.RFC3339Nano, "2009-11-10T22:00:00Z") 133 now, _ = time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z") 134 valid, _ = time.Parse(time.RFC3339Nano, "2009-11-11T00:00:00Z") 135 ) 136 137 type claimsTest struct { 138 name string 139 options Options 140 optsFunc func(*Options) 141 signingKey *jose.JSONWebKey 142 pubKeys []*jose.JSONWebKey 143 claims string 144 want *user.DefaultInfo 145 wantSkip bool 146 wantErr string 147 wantInitErr string 148 wantHealthErrPrefix string 149 claimToResponseMap map[string]string 150 openIDConfig string 151 fetchKeysFromRemote bool 152 } 153 154 // Replace formats the contents of v into the provided template. 155 func replace(tmpl string, v interface{}) string { 156 t := template.Must(template.New("test").Parse(tmpl)) 157 buf := bytes.NewBuffer(nil) 158 t.Execute(buf, &v) 159 ret := buf.String() 160 klog.V(4).Infof("Replaced: %v into: %v", tmpl, ret) 161 return ret 162 } 163 164 // newClaimServer returns a new test HTTPS server, which is rigged to return 165 // OIDC responses to requests that resolve distributed claims. signer is the 166 // signer used for the served JWT tokens. claimToResponseMap is a map of 167 // responses that the server will return for each claim it is given. 168 func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server { 169 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 170 klog.V(5).Infof("request: %+v", *r) 171 switch r.URL.Path { 172 case "/.testing/keys": 173 w.Header().Set("Content-Type", "application/json") 174 keyBytes, err := json.Marshal(keys) 175 if err != nil { 176 t.Fatalf("unexpected error while marshaling keys: %v", err) 177 } 178 klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes)) 179 w.Write(keyBytes) 180 181 // /c/d/bar/.well-known/openid-configuration is used to test issuer url and discovery url with a path 182 case "/.well-known/openid-configuration", "/c/d/bar/.well-known/openid-configuration": 183 w.Header().Set("Content-Type", "application/json") 184 klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig) 185 w.Write([]byte(*openIDConfig)) 186 // These claims are tested in the unit tests. 187 case "/groups": 188 fallthrough 189 case "/rabbits": 190 if claimToResponseMap == nil { 191 t.Errorf("no claims specified in response") 192 } 193 claim := r.URL.Path[1:] // "/groups" -> "groups" 194 expectedAuth := fmt.Sprintf("Bearer %v_token", claim) 195 auth := r.Header.Get("Authorization") 196 if auth != expectedAuth { 197 t.Errorf("bearer token expected: %q, was %q", expectedAuth, auth) 198 } 199 jws, err := signer.Sign([]byte(claimToResponseMap[claim])) 200 if err != nil { 201 t.Errorf("while signing response token: %v", err) 202 } 203 token, err := jws.CompactSerialize() 204 if err != nil { 205 t.Errorf("while serializing response token: %v", err) 206 } 207 w.Write([]byte(token)) 208 default: 209 w.WriteHeader(http.StatusNotFound) 210 fmt.Fprintf(w, "unexpected URL: %v", r.URL) 211 } 212 })) 213 klog.V(4).Infof("Serving OIDC at: %v", ts.URL) 214 return ts 215 } 216 217 func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet { 218 ret := jose.JSONWebKeySet{} 219 for _, k := range keys { 220 ret.Keys = append(ret.Keys, *k) 221 } 222 return ret 223 } 224 225 func (c *claimsTest) run(t *testing.T) { 226 var ( 227 signer jose.Signer 228 err error 229 ) 230 if c.signingKey != nil { 231 // Initialize the signer only in the tests that make use of it. We can 232 // not defer this initialization because the test server uses it too. 233 signer, err = jose.NewSigner(jose.SigningKey{ 234 Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm), 235 Key: c.signingKey, 236 }, nil) 237 if err != nil { 238 t.Fatalf("initialize signer: %v", err) 239 } 240 } 241 // The HTTPS server used for requesting distributed groups claims. 242 ts := newClaimServer(t, toKeySet(c.pubKeys), signer, c.claimToResponseMap, &c.openIDConfig) 243 defer ts.Close() 244 245 // Make the certificate of the helper server available to the authenticator 246 caBundle := pem.EncodeToMemory(&pem.Block{ 247 Type: "CERTIFICATE", 248 Bytes: ts.Certificate().Raw, 249 }) 250 caContent, err := dynamiccertificates.NewStaticCAContent("oidc-authenticator", caBundle) 251 if err != nil { 252 t.Fatalf("initialize ca: %v", err) 253 } 254 c.options.CAContentProvider = caContent 255 256 // Allow claims to refer to the serving URL of the test server. For this, 257 // substitute all references to {{.URL}} in appropriate places. 258 // Use {{.Expired}} to handle the token expiry date string with correct timezone handling. 259 v := struct { 260 URL string 261 Expired string 262 }{ 263 URL: ts.URL, 264 Expired: fmt.Sprintf("%v", time.Unix(expired.Unix(), 0)), 265 } 266 c.claims = replace(c.claims, &v) 267 c.openIDConfig = replace(c.openIDConfig, &v) 268 c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v) 269 c.options.JWTAuthenticator.Issuer.DiscoveryURL = replace(c.options.JWTAuthenticator.Issuer.DiscoveryURL, &v) 270 for claim, response := range c.claimToResponseMap { 271 c.claimToResponseMap[claim] = replace(response, &v) 272 } 273 c.wantErr = replace(c.wantErr, &v) 274 c.wantInitErr = replace(c.wantInitErr, &v) 275 276 if !c.fetchKeysFromRemote { 277 // Set the verifier to use the public key set instead of reading from a remote. 278 c.options.KeySet = &staticKeySet{keys: c.pubKeys} 279 } 280 281 if c.optsFunc != nil { 282 c.optsFunc(&c.options) 283 } 284 285 expectInitErr := len(c.wantInitErr) > 0 286 287 ctx := testContext(t) 288 289 // Initialize the authenticator. 290 a, err := New(ctx, c.options) 291 if err != nil { 292 if !expectInitErr { 293 t.Fatalf("initialize authenticator: %v", err) 294 } 295 if got := err.Error(); c.wantInitErr != got { 296 t.Fatalf("expected initialization error %q but got %q", c.wantInitErr, got) 297 } 298 return 299 } 300 if expectInitErr { 301 t.Fatalf("wanted initialization error %q but got none", c.wantInitErr) 302 } 303 304 if len(c.wantHealthErrPrefix) > 0 { 305 if err := wait.PollUntilContextTimeout(ctx, time.Second, time.Minute, true, func(context.Context) (bool, error) { 306 healthErr := a.HealthCheck() 307 if healthErr == nil { 308 return false, fmt.Errorf("authenticator reported healthy when it should not") 309 } 310 311 if strings.HasPrefix(healthErr.Error(), c.wantHealthErrPrefix) { 312 return true, nil 313 } 314 315 t.Logf("saw health error prefix that did not match: want=%q got=%q", c.wantHealthErrPrefix, healthErr.Error()) 316 return false, nil 317 }); err != nil { 318 t.Fatalf("authenticator did not match wanted health error: %v", err) 319 } 320 return 321 } 322 323 claims := struct{}{} 324 if err := json.Unmarshal([]byte(c.claims), &claims); err != nil { 325 t.Fatalf("failed to unmarshal claims: %v", err) 326 } 327 328 // Sign and serialize the claims in a JWT. 329 jws, err := signer.Sign([]byte(c.claims)) 330 if err != nil { 331 t.Fatalf("sign claims: %v", err) 332 } 333 token, err := jws.CompactSerialize() 334 if err != nil { 335 t.Fatalf("serialize token: %v", err) 336 } 337 338 // wait for the authenticator to be healthy 339 err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) { 340 return a.HealthCheck() == nil, nil 341 }) 342 if err != nil { 343 t.Fatalf("failed to initialize the authenticator: %v", err) 344 } 345 346 got, ok, err := a.AuthenticateToken(ctx, token) 347 348 expectErr := len(c.wantErr) > 0 349 350 if err != nil { 351 if !expectErr { 352 t.Fatalf("authenticate token: %v", err) 353 } 354 if got := err.Error(); c.wantErr != got { 355 t.Fatalf("expected error %q when authenticating token but got %q", c.wantErr, got) 356 } 357 return 358 } 359 360 if expectErr { 361 t.Fatalf("expected error %q when authenticating token but got none", c.wantErr) 362 } 363 if !ok { 364 if !c.wantSkip { 365 // We don't have any cases where we return (nil, false, nil) 366 t.Fatalf("no error but token not authenticated") 367 } 368 return 369 } 370 if c.wantSkip { 371 t.Fatalf("expected authenticator to skip token") 372 } 373 374 gotUser := got.User.(*user.DefaultInfo) 375 if !reflect.DeepEqual(gotUser, c.want) { 376 t.Fatalf("wanted user=%#v, got=%#v", c.want, gotUser) 377 } 378 } 379 380 func TestToken(t *testing.T) { 381 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true) 382 383 synchronizeTokenIDVerifierForTest = true 384 tests := []claimsTest{ 385 { 386 name: "token", 387 options: Options{ 388 JWTAuthenticator: apiserver.JWTAuthenticator{ 389 Issuer: apiserver.Issuer{ 390 URL: "https://auth.example.com", 391 Audiences: []string{"my-client"}, 392 }, 393 ClaimMappings: apiserver.ClaimMappings{ 394 Username: apiserver.PrefixedClaimOrExpression{ 395 Claim: "username", 396 Prefix: pointer.String(""), 397 }, 398 }, 399 }, 400 now: func() time.Time { return now }, 401 }, 402 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 403 pubKeys: []*jose.JSONWebKey{ 404 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 405 }, 406 claims: fmt.Sprintf(`{ 407 "iss": "https://auth.example.com", 408 "aud": "my-client", 409 "username": "jane", 410 "exp": %d 411 }`, valid.Unix()), 412 want: &user.DefaultInfo{ 413 Name: "jane", 414 }, 415 }, 416 { 417 name: "no-username", 418 options: Options{ 419 JWTAuthenticator: apiserver.JWTAuthenticator{ 420 Issuer: apiserver.Issuer{ 421 URL: "https://auth.example.com", 422 Audiences: []string{"my-client"}, 423 }, 424 ClaimMappings: apiserver.ClaimMappings{ 425 Username: apiserver.PrefixedClaimOrExpression{ 426 Claim: "username", 427 Prefix: pointer.String("prefix:"), 428 }, 429 }, 430 }, 431 now: func() time.Time { return now }, 432 }, 433 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 434 pubKeys: []*jose.JSONWebKey{ 435 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 436 }, 437 claims: fmt.Sprintf(`{ 438 "iss": "https://auth.example.com", 439 "aud": "my-client", 440 "exp": %d 441 }`, valid.Unix()), 442 wantErr: `oidc: parse username claims "username": claim not present`, 443 }, 444 { 445 name: "email", 446 options: Options{ 447 JWTAuthenticator: apiserver.JWTAuthenticator{ 448 Issuer: apiserver.Issuer{ 449 URL: "https://auth.example.com", 450 Audiences: []string{"my-client"}, 451 }, 452 ClaimMappings: apiserver.ClaimMappings{ 453 Username: apiserver.PrefixedClaimOrExpression{ 454 Claim: "email", 455 Prefix: pointer.String(""), 456 }, 457 }, 458 }, 459 now: func() time.Time { return now }, 460 }, 461 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 462 pubKeys: []*jose.JSONWebKey{ 463 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 464 }, 465 claims: fmt.Sprintf(`{ 466 "iss": "https://auth.example.com", 467 "aud": "my-client", 468 "email": "jane@example.com", 469 "email_verified": true, 470 "exp": %d 471 }`, valid.Unix()), 472 want: &user.DefaultInfo{ 473 Name: "jane@example.com", 474 }, 475 }, 476 { 477 name: "email-not-verified", 478 options: Options{ 479 JWTAuthenticator: apiserver.JWTAuthenticator{ 480 Issuer: apiserver.Issuer{ 481 URL: "https://auth.example.com", 482 Audiences: []string{"my-client"}, 483 }, 484 ClaimMappings: apiserver.ClaimMappings{ 485 Username: apiserver.PrefixedClaimOrExpression{ 486 Claim: "email", 487 Prefix: pointer.String(""), 488 }, 489 }, 490 }, 491 now: func() time.Time { return now }, 492 }, 493 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 494 pubKeys: []*jose.JSONWebKey{ 495 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 496 }, 497 claims: fmt.Sprintf(`{ 498 "iss": "https://auth.example.com", 499 "aud": "my-client", 500 "email": "jane@example.com", 501 "email_verified": false, 502 "exp": %d 503 }`, valid.Unix()), 504 wantErr: "oidc: email not verified", 505 }, 506 { 507 // If "email_verified" isn't present, assume true 508 name: "no-email-verified-claim", 509 options: Options{ 510 JWTAuthenticator: apiserver.JWTAuthenticator{ 511 Issuer: apiserver.Issuer{ 512 URL: "https://auth.example.com", 513 Audiences: []string{"my-client"}, 514 }, 515 ClaimMappings: apiserver.ClaimMappings{ 516 Username: apiserver.PrefixedClaimOrExpression{ 517 Claim: "email", 518 Prefix: pointer.String(""), 519 }, 520 }, 521 }, 522 now: func() time.Time { return now }, 523 }, 524 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 525 pubKeys: []*jose.JSONWebKey{ 526 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 527 }, 528 claims: fmt.Sprintf(`{ 529 "iss": "https://auth.example.com", 530 "aud": "my-client", 531 "email": "jane@example.com", 532 "exp": %d 533 }`, valid.Unix()), 534 want: &user.DefaultInfo{ 535 Name: "jane@example.com", 536 }, 537 }, 538 { 539 name: "invalid-email-verified-claim", 540 options: Options{ 541 JWTAuthenticator: apiserver.JWTAuthenticator{ 542 Issuer: apiserver.Issuer{ 543 URL: "https://auth.example.com", 544 Audiences: []string{"my-client"}, 545 }, 546 ClaimMappings: apiserver.ClaimMappings{ 547 Username: apiserver.PrefixedClaimOrExpression{ 548 Claim: "email", 549 Prefix: pointer.String(""), 550 }, 551 }, 552 }, 553 now: func() time.Time { return now }, 554 }, 555 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 556 pubKeys: []*jose.JSONWebKey{ 557 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 558 }, 559 // string value for "email_verified" 560 claims: fmt.Sprintf(`{ 561 "iss": "https://auth.example.com", 562 "aud": "my-client", 563 "email": "jane@example.com", 564 "email_verified": "false", 565 "exp": %d 566 }`, valid.Unix()), 567 wantErr: "oidc: parse 'email_verified' claim: json: cannot unmarshal string into Go value of type bool", 568 }, 569 { 570 name: "groups", 571 options: Options{ 572 JWTAuthenticator: apiserver.JWTAuthenticator{ 573 Issuer: apiserver.Issuer{ 574 URL: "https://auth.example.com", 575 Audiences: []string{"my-client"}, 576 }, 577 ClaimMappings: apiserver.ClaimMappings{ 578 Username: apiserver.PrefixedClaimOrExpression{ 579 Claim: "username", 580 Prefix: pointer.String(""), 581 }, 582 Groups: apiserver.PrefixedClaimOrExpression{ 583 Claim: "groups", 584 Prefix: pointer.String(""), 585 }, 586 }, 587 }, 588 now: func() time.Time { return now }, 589 }, 590 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 591 pubKeys: []*jose.JSONWebKey{ 592 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 593 }, 594 claims: fmt.Sprintf(`{ 595 "iss": "https://auth.example.com", 596 "aud": "my-client", 597 "username": "jane", 598 "groups": ["team1", "team2"], 599 "exp": %d 600 }`, valid.Unix()), 601 want: &user.DefaultInfo{ 602 Name: "jane", 603 Groups: []string{"team1", "team2"}, 604 }, 605 }, 606 { 607 name: "groups-distributed", 608 options: Options{ 609 JWTAuthenticator: apiserver.JWTAuthenticator{ 610 Issuer: apiserver.Issuer{ 611 URL: "{{.URL}}", 612 Audiences: []string{"my-client"}, 613 }, 614 ClaimMappings: apiserver.ClaimMappings{ 615 Username: apiserver.PrefixedClaimOrExpression{ 616 Claim: "username", 617 Prefix: pointer.String(""), 618 }, 619 Groups: apiserver.PrefixedClaimOrExpression{ 620 Claim: "groups", 621 Prefix: pointer.String(""), 622 }, 623 }, 624 }, 625 now: func() time.Time { return now }, 626 }, 627 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 628 pubKeys: []*jose.JSONWebKey{ 629 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 630 }, 631 claims: fmt.Sprintf(`{ 632 "iss": "{{.URL}}", 633 "aud": "my-client", 634 "username": "jane", 635 "_claim_names": { 636 "groups": "src1" 637 }, 638 "_claim_sources": { 639 "src1": { 640 "endpoint": "{{.URL}}/groups", 641 "access_token": "groups_token" 642 } 643 }, 644 "exp": %d 645 }`, valid.Unix()), 646 claimToResponseMap: map[string]string{ 647 "groups": fmt.Sprintf(`{ 648 "iss": "{{.URL}}", 649 "aud": "my-client", 650 "groups": ["team1", "team2"], 651 "exp": %d 652 }`, valid.Unix()), 653 }, 654 openIDConfig: `{ 655 "issuer": "{{.URL}}", 656 "jwks_uri": "{{.URL}}/.testing/keys" 657 }`, 658 want: &user.DefaultInfo{ 659 Name: "jane", 660 Groups: []string{"team1", "team2"}, 661 }, 662 }, 663 { 664 name: "groups-distributed invalid client", 665 options: Options{ 666 JWTAuthenticator: apiserver.JWTAuthenticator{ 667 Issuer: apiserver.Issuer{ 668 URL: "{{.URL}}", 669 Audiences: []string{"my-client"}, 670 }, 671 ClaimMappings: apiserver.ClaimMappings{ 672 Username: apiserver.PrefixedClaimOrExpression{ 673 Claim: "username", 674 Prefix: pointer.String(""), 675 }, 676 Groups: apiserver.PrefixedClaimOrExpression{ 677 Claim: "groups", 678 Prefix: pointer.String(""), 679 }, 680 }, 681 }, 682 Client: &http.Client{Transport: errTransport("some unexpected oidc error")}, // return an error that we can assert against 683 now: func() time.Time { return now }, 684 }, 685 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 686 pubKeys: []*jose.JSONWebKey{ 687 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 688 }, 689 claims: fmt.Sprintf(`{ 690 "iss": "{{.URL}}", 691 "aud": "my-client", 692 "username": "jane", 693 "_claim_names": { 694 "groups": "src1" 695 }, 696 "_claim_sources": { 697 "src1": { 698 "endpoint": "{{.URL}}/groups", 699 "access_token": "groups_token" 700 } 701 }, 702 "exp": %d 703 }`, valid.Unix()), 704 claimToResponseMap: map[string]string{ 705 "groups": fmt.Sprintf(`{ 706 "iss": "{{.URL}}", 707 "aud": "my-client", 708 "groups": ["team1", "team2"], 709 "exp": %d 710 }`, valid.Unix()), 711 }, 712 openIDConfig: `{ 713 "issuer": "{{.URL}}", 714 "jwks_uri": "{{.URL}}/.testing/keys" 715 }`, 716 optsFunc: func(opts *Options) { 717 opts.CAContentProvider = nil // unset CA automatically set by the test to allow us to use a custom client 718 }, 719 wantErr: `oidc: could not expand distributed claims: while getting distributed claim "groups": Get "{{.URL}}/groups": some unexpected oidc error`, 720 }, 721 { 722 name: "groups-distributed-malformed-claim-names", 723 options: Options{ 724 JWTAuthenticator: apiserver.JWTAuthenticator{ 725 Issuer: apiserver.Issuer{ 726 URL: "{{.URL}}", 727 Audiences: []string{"my-client"}, 728 }, 729 ClaimMappings: apiserver.ClaimMappings{ 730 Username: apiserver.PrefixedClaimOrExpression{ 731 Claim: "username", 732 Prefix: pointer.String(""), 733 }, 734 Groups: apiserver.PrefixedClaimOrExpression{ 735 Claim: "groups", 736 Prefix: pointer.String(""), 737 }, 738 }, 739 }, 740 now: func() time.Time { return now }, 741 }, 742 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 743 pubKeys: []*jose.JSONWebKey{ 744 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 745 }, 746 claims: fmt.Sprintf(`{ 747 "iss": "{{.URL}}", 748 "aud": "my-client", 749 "username": "jane", 750 "_claim_names": { 751 "groups": "nonexistent-claim-source" 752 }, 753 "_claim_sources": { 754 "src1": { 755 "endpoint": "{{.URL}}/groups", 756 "access_token": "groups_token" 757 } 758 }, 759 "exp": %d 760 }`, valid.Unix()), 761 claimToResponseMap: map[string]string{ 762 "groups": fmt.Sprintf(`{ 763 "iss": "{{.URL}}", 764 "aud": "my-client", 765 "groups": ["team1", "team2"], 766 "exp": %d 767 }`, valid.Unix()), 768 }, 769 openIDConfig: `{ 770 "issuer": "{{.URL}}", 771 "jwks_uri": "{{.URL}}/.testing/keys" 772 }`, 773 wantErr: "oidc: verify token: oidc: source does not exist", 774 }, 775 { 776 name: "groups-distributed-malformed-names-and-sources", 777 options: Options{ 778 JWTAuthenticator: apiserver.JWTAuthenticator{ 779 Issuer: apiserver.Issuer{ 780 URL: "{{.URL}}", 781 Audiences: []string{"my-client"}, 782 }, 783 ClaimMappings: apiserver.ClaimMappings{ 784 Username: apiserver.PrefixedClaimOrExpression{ 785 Claim: "username", 786 Prefix: pointer.String(""), 787 }, 788 Groups: apiserver.PrefixedClaimOrExpression{ 789 Claim: "groups", 790 Prefix: pointer.String(""), 791 }, 792 }, 793 }, 794 now: func() time.Time { return now }, 795 }, 796 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 797 pubKeys: []*jose.JSONWebKey{ 798 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 799 }, 800 claims: fmt.Sprintf(`{ 801 "iss": "{{.URL}}", 802 "aud": "my-client", 803 "username": "jane", 804 "_claim_names": { 805 "groups": "src1" 806 }, 807 "exp": %d 808 }`, valid.Unix()), 809 claimToResponseMap: map[string]string{ 810 "groups": fmt.Sprintf(`{ 811 "iss": "{{.URL}}", 812 "aud": "my-client", 813 "groups": ["team1", "team2"], 814 "exp": %d 815 }`, valid.Unix()), 816 }, 817 openIDConfig: `{ 818 "issuer": "{{.URL}}", 819 "jwks_uri": "{{.URL}}/.testing/keys" 820 }`, 821 wantErr: "oidc: verify token: oidc: source does not exist", 822 }, 823 { 824 name: "groups-distributed-malformed-distributed-claim", 825 options: Options{ 826 JWTAuthenticator: apiserver.JWTAuthenticator{ 827 Issuer: apiserver.Issuer{ 828 URL: "{{.URL}}", 829 Audiences: []string{"my-client"}, 830 }, 831 ClaimMappings: apiserver.ClaimMappings{ 832 Username: apiserver.PrefixedClaimOrExpression{ 833 Claim: "username", 834 Prefix: pointer.String(""), 835 }, 836 Groups: apiserver.PrefixedClaimOrExpression{ 837 Claim: "groups", 838 Prefix: pointer.String(""), 839 }, 840 }, 841 }, 842 now: func() time.Time { return now }, 843 }, 844 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 845 pubKeys: []*jose.JSONWebKey{ 846 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 847 }, 848 claims: fmt.Sprintf(`{ 849 "iss": "{{.URL}}", 850 "aud": "my-client", 851 "username": "jane", 852 "_claim_names": { 853 "groups": "src1" 854 }, 855 "_claim_sources": { 856 "src1": { 857 "endpoint": "{{.URL}}/groups", 858 "access_token": "groups_token" 859 } 860 }, 861 "exp": %d 862 }`, valid.Unix()), 863 claimToResponseMap: map[string]string{ 864 // Doesn't contain the "groups" claim as it promises. 865 "groups": fmt.Sprintf(`{ 866 "iss": "{{.URL}}", 867 "aud": "my-client", 868 "exp": %d 869 }`, valid.Unix()), 870 }, 871 openIDConfig: `{ 872 "issuer": "{{.URL}}", 873 "jwks_uri": "{{.URL}}/.testing/keys" 874 }`, 875 wantErr: `oidc: could not expand distributed claims: jwt returned by distributed claim endpoint "{{.URL}}/groups" did not contain claim: groups`, 876 }, 877 { 878 name: "groups-distributed-unusual-name", 879 options: Options{ 880 JWTAuthenticator: apiserver.JWTAuthenticator{ 881 Issuer: apiserver.Issuer{ 882 URL: "{{.URL}}", 883 Audiences: []string{"my-client"}, 884 }, 885 ClaimMappings: apiserver.ClaimMappings{ 886 Username: apiserver.PrefixedClaimOrExpression{ 887 Claim: "username", 888 Prefix: pointer.String(""), 889 }, 890 Groups: apiserver.PrefixedClaimOrExpression{ 891 Claim: "rabbits", 892 Prefix: pointer.String(""), 893 }, 894 }, 895 }, 896 now: func() time.Time { return now }, 897 }, 898 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 899 pubKeys: []*jose.JSONWebKey{ 900 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 901 }, 902 claims: fmt.Sprintf(`{ 903 "iss": "{{.URL}}", 904 "aud": "my-client", 905 "username": "jane", 906 "_claim_names": { 907 "rabbits": "src1" 908 }, 909 "_claim_sources": { 910 "src1": { 911 "endpoint": "{{.URL}}/rabbits", 912 "access_token": "rabbits_token" 913 } 914 }, 915 "exp": %d 916 }`, valid.Unix()), 917 claimToResponseMap: map[string]string{ 918 "rabbits": fmt.Sprintf(`{ 919 "iss": "{{.URL}}", 920 "aud": "my-client", 921 "rabbits": ["team1", "team2"], 922 "exp": %d 923 }`, valid.Unix()), 924 }, 925 openIDConfig: `{ 926 "issuer": "{{.URL}}", 927 "jwks_uri": "{{.URL}}/.testing/keys" 928 }`, 929 want: &user.DefaultInfo{ 930 Name: "jane", 931 Groups: []string{"team1", "team2"}, 932 }, 933 }, 934 { 935 name: "groups-distributed-wrong-audience", 936 options: Options{ 937 JWTAuthenticator: apiserver.JWTAuthenticator{ 938 Issuer: apiserver.Issuer{ 939 URL: "{{.URL}}", 940 Audiences: []string{"my-client"}, 941 }, 942 ClaimMappings: apiserver.ClaimMappings{ 943 Username: apiserver.PrefixedClaimOrExpression{ 944 Claim: "username", 945 Prefix: pointer.String(""), 946 }, 947 Groups: apiserver.PrefixedClaimOrExpression{ 948 Claim: "groups", 949 Prefix: pointer.String(""), 950 }, 951 }, 952 }, 953 now: func() time.Time { return now }, 954 }, 955 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 956 pubKeys: []*jose.JSONWebKey{ 957 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 958 }, 959 claims: fmt.Sprintf(`{ 960 "iss": "{{.URL}}", 961 "aud": "my-client", 962 "username": "jane", 963 "_claim_names": { 964 "groups": "src1" 965 }, 966 "_claim_sources": { 967 "src1": { 968 "endpoint": "{{.URL}}/groups", 969 "access_token": "groups_token" 970 } 971 }, 972 "exp": %d 973 }`, valid.Unix()), 974 claimToResponseMap: map[string]string{ 975 // Note mismatching "aud" 976 "groups": fmt.Sprintf(`{ 977 "iss": "{{.URL}}", 978 "aud": "your-client", 979 "groups": ["team1", "team2"], 980 "exp": %d 981 }`, valid.Unix()), 982 }, 983 openIDConfig: `{ 984 "issuer": "{{.URL}}", 985 "jwks_uri": "{{.URL}}/.testing/keys" 986 }`, 987 wantErr: `oidc: could not expand distributed claims: verify distributed claim token: oidc: expected audience "my-client" got ["your-client"]`, 988 }, 989 { 990 name: "groups-distributed-expired-token", 991 options: Options{ 992 JWTAuthenticator: apiserver.JWTAuthenticator{ 993 Issuer: apiserver.Issuer{ 994 URL: "{{.URL}}", 995 Audiences: []string{"my-client"}, 996 }, 997 ClaimMappings: apiserver.ClaimMappings{ 998 Username: apiserver.PrefixedClaimOrExpression{ 999 Claim: "username", 1000 Prefix: pointer.String(""), 1001 }, 1002 Groups: apiserver.PrefixedClaimOrExpression{ 1003 Claim: "groups", 1004 Prefix: pointer.String(""), 1005 }, 1006 }, 1007 }, 1008 now: func() time.Time { return now }, 1009 }, 1010 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1011 pubKeys: []*jose.JSONWebKey{ 1012 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1013 }, 1014 claims: fmt.Sprintf(`{ 1015 "iss": "{{.URL}}", 1016 "aud": "my-client", 1017 "username": "jane", 1018 "_claim_names": { 1019 "groups": "src1" 1020 }, 1021 "_claim_sources": { 1022 "src1": { 1023 "endpoint": "{{.URL}}/groups", 1024 "access_token": "groups_token" 1025 } 1026 }, 1027 "exp": %d 1028 }`, valid.Unix()), 1029 claimToResponseMap: map[string]string{ 1030 // Note expired timestamp. 1031 "groups": fmt.Sprintf(`{ 1032 "iss": "{{.URL}}", 1033 "aud": "my-client", 1034 "groups": ["team1", "team2"], 1035 "exp": %d 1036 }`, expired.Unix()), 1037 }, 1038 openIDConfig: `{ 1039 "issuer": "{{.URL}}", 1040 "jwks_uri": "{{.URL}}/.testing/keys" 1041 }`, 1042 wantErr: "oidc: could not expand distributed claims: verify distributed claim token: oidc: token is expired (Token Expiry: {{.Expired}})", 1043 }, 1044 { 1045 // Specs are unclear about this behavior. We adopt a behavior where 1046 // normal claim wins over a distributed claim by the same name. 1047 name: "groups-distributed-normal-claim-wins", 1048 options: Options{ 1049 JWTAuthenticator: apiserver.JWTAuthenticator{ 1050 Issuer: apiserver.Issuer{ 1051 URL: "{{.URL}}", 1052 Audiences: []string{"my-client"}, 1053 }, 1054 ClaimMappings: apiserver.ClaimMappings{ 1055 Username: apiserver.PrefixedClaimOrExpression{ 1056 Claim: "username", 1057 Prefix: pointer.String(""), 1058 }, 1059 Groups: apiserver.PrefixedClaimOrExpression{ 1060 Claim: "groups", 1061 Prefix: pointer.String(""), 1062 }, 1063 }, 1064 }, 1065 now: func() time.Time { return now }, 1066 }, 1067 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1068 pubKeys: []*jose.JSONWebKey{ 1069 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1070 }, 1071 claims: fmt.Sprintf(`{ 1072 "iss": "{{.URL}}", 1073 "aud": "my-client", 1074 "username": "jane", 1075 "groups": "team1", 1076 "_claim_names": { 1077 "groups": "src1" 1078 }, 1079 "_claim_sources": { 1080 "src1": { 1081 "endpoint": "{{.URL}}/groups", 1082 "access_token": "groups_token" 1083 } 1084 }, 1085 "exp": %d 1086 }`, valid.Unix()), 1087 claimToResponseMap: map[string]string{ 1088 "groups": fmt.Sprintf(`{ 1089 "iss": "{{.URL}}", 1090 "aud": "my-client", 1091 "groups": ["team2"], 1092 "exp": %d 1093 }`, valid.Unix()), 1094 }, 1095 openIDConfig: `{ 1096 "issuer": "{{.URL}}", 1097 "jwks_uri": "{{.URL}}/.testing/keys" 1098 }`, 1099 want: &user.DefaultInfo{ 1100 Name: "jane", 1101 // "team1" is from the normal "groups" claim. 1102 Groups: []string{"team1"}, 1103 }, 1104 }, 1105 { 1106 // Groups should be able to be a single string, not just a slice. 1107 name: "group-string-claim", 1108 options: Options{ 1109 JWTAuthenticator: apiserver.JWTAuthenticator{ 1110 Issuer: apiserver.Issuer{ 1111 URL: "https://auth.example.com", 1112 Audiences: []string{"my-client"}, 1113 }, 1114 ClaimMappings: apiserver.ClaimMappings{ 1115 Username: apiserver.PrefixedClaimOrExpression{ 1116 Claim: "username", 1117 Prefix: pointer.String(""), 1118 }, 1119 Groups: apiserver.PrefixedClaimOrExpression{ 1120 Claim: "groups", 1121 Prefix: pointer.String(""), 1122 }, 1123 }, 1124 }, 1125 now: func() time.Time { return now }, 1126 }, 1127 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1128 pubKeys: []*jose.JSONWebKey{ 1129 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1130 }, 1131 claims: fmt.Sprintf(`{ 1132 "iss": "https://auth.example.com", 1133 "aud": "my-client", 1134 "username": "jane", 1135 "groups": "team1", 1136 "exp": %d 1137 }`, valid.Unix()), 1138 want: &user.DefaultInfo{ 1139 Name: "jane", 1140 Groups: []string{"team1"}, 1141 }, 1142 }, 1143 { 1144 // Groups should be able to be a single string, not just a slice. 1145 name: "group-string-claim-distributed", 1146 options: Options{ 1147 JWTAuthenticator: apiserver.JWTAuthenticator{ 1148 Issuer: apiserver.Issuer{ 1149 URL: "{{.URL}}", 1150 Audiences: []string{"my-client"}, 1151 }, 1152 ClaimMappings: apiserver.ClaimMappings{ 1153 Username: apiserver.PrefixedClaimOrExpression{ 1154 Claim: "username", 1155 Prefix: pointer.String(""), 1156 }, 1157 Groups: apiserver.PrefixedClaimOrExpression{ 1158 Claim: "groups", 1159 Prefix: pointer.String(""), 1160 }, 1161 }, 1162 }, 1163 now: func() time.Time { return now }, 1164 }, 1165 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1166 pubKeys: []*jose.JSONWebKey{ 1167 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1168 }, 1169 claims: fmt.Sprintf(`{ 1170 "iss": "{{.URL}}", 1171 "aud": "my-client", 1172 "username": "jane", 1173 "_claim_names": { 1174 "groups": "src1" 1175 }, 1176 "_claim_sources": { 1177 "src1": { 1178 "endpoint": "{{.URL}}/groups", 1179 "access_token": "groups_token" 1180 } 1181 }, 1182 "exp": %d 1183 }`, valid.Unix()), 1184 claimToResponseMap: map[string]string{ 1185 "groups": fmt.Sprintf(`{ 1186 "iss": "{{.URL}}", 1187 "aud": "my-client", 1188 "groups": "team1", 1189 "exp": %d 1190 }`, valid.Unix()), 1191 }, 1192 openIDConfig: `{ 1193 "issuer": "{{.URL}}", 1194 "jwks_uri": "{{.URL}}/.testing/keys" 1195 }`, 1196 want: &user.DefaultInfo{ 1197 Name: "jane", 1198 Groups: []string{"team1"}, 1199 }, 1200 }, 1201 { 1202 name: "group-string-claim-aggregated-not-supported", 1203 options: Options{ 1204 JWTAuthenticator: apiserver.JWTAuthenticator{ 1205 Issuer: apiserver.Issuer{ 1206 URL: "https://auth.example.com", 1207 Audiences: []string{"my-client"}, 1208 }, 1209 ClaimMappings: apiserver.ClaimMappings{ 1210 Username: apiserver.PrefixedClaimOrExpression{ 1211 Claim: "username", 1212 Prefix: pointer.String(""), 1213 }, 1214 Groups: apiserver.PrefixedClaimOrExpression{ 1215 Claim: "groups", 1216 Prefix: pointer.String(""), 1217 }, 1218 }, 1219 }, 1220 now: func() time.Time { return now }, 1221 }, 1222 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1223 pubKeys: []*jose.JSONWebKey{ 1224 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1225 }, 1226 claims: fmt.Sprintf(`{ 1227 "iss": "https://auth.example.com", 1228 "aud": "my-client", 1229 "username": "jane", 1230 "_claim_names": { 1231 "groups": "src1" 1232 }, 1233 "_claim_sources": { 1234 "src1": { 1235 "JWT": "some.jwt.token" 1236 } 1237 }, 1238 "exp": %d 1239 }`, valid.Unix()), 1240 want: &user.DefaultInfo{ 1241 Name: "jane", 1242 }, 1243 }, 1244 { 1245 // if the groups claim isn't provided, this shouldn't error out 1246 name: "no-groups-claim", 1247 options: Options{ 1248 JWTAuthenticator: apiserver.JWTAuthenticator{ 1249 Issuer: apiserver.Issuer{ 1250 URL: "https://auth.example.com", 1251 Audiences: []string{"my-client"}, 1252 }, 1253 ClaimMappings: apiserver.ClaimMappings{ 1254 Username: apiserver.PrefixedClaimOrExpression{ 1255 Claim: "username", 1256 Prefix: pointer.String(""), 1257 }, 1258 Groups: apiserver.PrefixedClaimOrExpression{ 1259 Claim: "groups", 1260 Prefix: pointer.String(""), 1261 }, 1262 }, 1263 }, 1264 now: func() time.Time { return now }, 1265 }, 1266 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1267 pubKeys: []*jose.JSONWebKey{ 1268 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1269 }, 1270 claims: fmt.Sprintf(`{ 1271 "iss": "https://auth.example.com", 1272 "aud": "my-client", 1273 "username": "jane", 1274 "exp": %d 1275 }`, valid.Unix()), 1276 want: &user.DefaultInfo{ 1277 Name: "jane", 1278 }, 1279 }, 1280 { 1281 name: "invalid-groups-claim", 1282 options: Options{ 1283 JWTAuthenticator: apiserver.JWTAuthenticator{ 1284 Issuer: apiserver.Issuer{ 1285 URL: "https://auth.example.com", 1286 Audiences: []string{"my-client"}, 1287 }, 1288 ClaimMappings: apiserver.ClaimMappings{ 1289 Username: apiserver.PrefixedClaimOrExpression{ 1290 Claim: "username", 1291 Prefix: pointer.String(""), 1292 }, 1293 Groups: apiserver.PrefixedClaimOrExpression{ 1294 Claim: "groups", 1295 Prefix: pointer.String(""), 1296 }, 1297 }, 1298 }, 1299 now: func() time.Time { return now }, 1300 }, 1301 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1302 pubKeys: []*jose.JSONWebKey{ 1303 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1304 }, 1305 claims: fmt.Sprintf(`{ 1306 "iss": "https://auth.example.com", 1307 "aud": "my-client", 1308 "username": "jane", 1309 "groups": 42, 1310 "exp": %d 1311 }`, valid.Unix()), 1312 wantErr: `oidc: parse groups claim "groups": json: cannot unmarshal number into Go value of type string`, 1313 }, 1314 { 1315 name: "required-claim", 1316 options: Options{ 1317 JWTAuthenticator: apiserver.JWTAuthenticator{ 1318 Issuer: apiserver.Issuer{ 1319 URL: "https://auth.example.com", 1320 Audiences: []string{"my-client"}, 1321 }, 1322 ClaimMappings: apiserver.ClaimMappings{ 1323 Username: apiserver.PrefixedClaimOrExpression{ 1324 Claim: "username", 1325 Prefix: pointer.String(""), 1326 }, 1327 Groups: apiserver.PrefixedClaimOrExpression{ 1328 Claim: "groups", 1329 Prefix: pointer.String(""), 1330 }, 1331 }, 1332 ClaimValidationRules: []apiserver.ClaimValidationRule{ 1333 { 1334 Claim: "hd", 1335 RequiredValue: "example.com", 1336 }, 1337 { 1338 Claim: "sub", 1339 RequiredValue: "test", 1340 }, 1341 }, 1342 }, 1343 now: func() time.Time { return now }, 1344 }, 1345 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1346 pubKeys: []*jose.JSONWebKey{ 1347 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1348 }, 1349 claims: fmt.Sprintf(`{ 1350 "iss": "https://auth.example.com", 1351 "aud": "my-client", 1352 "username": "jane", 1353 "hd": "example.com", 1354 "sub": "test", 1355 "exp": %d 1356 }`, valid.Unix()), 1357 want: &user.DefaultInfo{ 1358 Name: "jane", 1359 }, 1360 }, 1361 { 1362 name: "no-required-claim", 1363 options: Options{ 1364 JWTAuthenticator: apiserver.JWTAuthenticator{ 1365 Issuer: apiserver.Issuer{ 1366 URL: "https://auth.example.com", 1367 Audiences: []string{"my-client"}, 1368 }, 1369 ClaimMappings: apiserver.ClaimMappings{ 1370 Username: apiserver.PrefixedClaimOrExpression{ 1371 Claim: "username", 1372 Prefix: pointer.String(""), 1373 }, 1374 Groups: apiserver.PrefixedClaimOrExpression{ 1375 Claim: "groups", 1376 Prefix: pointer.String(""), 1377 }, 1378 }, 1379 ClaimValidationRules: []apiserver.ClaimValidationRule{ 1380 { 1381 Claim: "hd", 1382 RequiredValue: "example.com", 1383 }, 1384 }, 1385 }, 1386 now: func() time.Time { return now }, 1387 }, 1388 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1389 pubKeys: []*jose.JSONWebKey{ 1390 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1391 }, 1392 claims: fmt.Sprintf(`{ 1393 "iss": "https://auth.example.com", 1394 "aud": "my-client", 1395 "username": "jane", 1396 "exp": %d 1397 }`, valid.Unix()), 1398 wantErr: "oidc: required claim hd not present in ID token", 1399 }, 1400 { 1401 name: "invalid-required-claim", 1402 options: Options{ 1403 JWTAuthenticator: apiserver.JWTAuthenticator{ 1404 Issuer: apiserver.Issuer{ 1405 URL: "https://auth.example.com", 1406 Audiences: []string{"my-client"}, 1407 }, 1408 ClaimMappings: apiserver.ClaimMappings{ 1409 Username: apiserver.PrefixedClaimOrExpression{ 1410 Claim: "username", 1411 Prefix: pointer.String(""), 1412 }, 1413 Groups: apiserver.PrefixedClaimOrExpression{ 1414 Claim: "groups", 1415 Prefix: pointer.String(""), 1416 }, 1417 }, 1418 ClaimValidationRules: []apiserver.ClaimValidationRule{ 1419 { 1420 Claim: "hd", 1421 RequiredValue: "example.com", 1422 }, 1423 }, 1424 }, 1425 now: func() time.Time { return now }, 1426 }, 1427 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1428 pubKeys: []*jose.JSONWebKey{ 1429 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1430 }, 1431 claims: fmt.Sprintf(`{ 1432 "iss": "https://auth.example.com", 1433 "aud": "my-client", 1434 "username": "jane", 1435 "hd": "example.org", 1436 "exp": %d 1437 }`, valid.Unix()), 1438 wantErr: "oidc: required claim hd value does not match. Got = example.org, want = example.com", 1439 }, 1440 { 1441 name: "invalid-signature", 1442 options: Options{ 1443 JWTAuthenticator: apiserver.JWTAuthenticator{ 1444 Issuer: apiserver.Issuer{ 1445 URL: "https://auth.example.com", 1446 Audiences: []string{"my-client"}, 1447 }, 1448 ClaimMappings: apiserver.ClaimMappings{ 1449 Username: apiserver.PrefixedClaimOrExpression{ 1450 Claim: "username", 1451 Prefix: pointer.String("prefix:"), 1452 }, 1453 }, 1454 }, 1455 now: func() time.Time { return now }, 1456 }, 1457 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1458 pubKeys: []*jose.JSONWebKey{ 1459 loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256), 1460 }, 1461 claims: fmt.Sprintf(`{ 1462 "iss": "https://auth.example.com", 1463 "aud": "my-client", 1464 "username": "jane", 1465 "exp": %d 1466 }`, valid.Unix()), 1467 wantErr: "oidc: verify token: failed to verify signature: no keys matches jwk keyid", 1468 }, 1469 { 1470 name: "expired", 1471 options: Options{ 1472 JWTAuthenticator: apiserver.JWTAuthenticator{ 1473 Issuer: apiserver.Issuer{ 1474 URL: "https://auth.example.com", 1475 Audiences: []string{"my-client"}, 1476 }, 1477 ClaimMappings: apiserver.ClaimMappings{ 1478 Username: apiserver.PrefixedClaimOrExpression{ 1479 Claim: "username", 1480 Prefix: pointer.String("prefix:"), 1481 }, 1482 }, 1483 }, 1484 now: func() time.Time { return now }, 1485 }, 1486 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1487 pubKeys: []*jose.JSONWebKey{ 1488 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1489 }, 1490 claims: fmt.Sprintf(`{ 1491 "iss": "https://auth.example.com", 1492 "aud": "my-client", 1493 "username": "jane", 1494 "exp": %d 1495 }`, expired.Unix()), 1496 wantErr: `oidc: verify token: oidc: token is expired (Token Expiry: {{.Expired}})`, 1497 }, 1498 { 1499 name: "invalid-aud", 1500 options: Options{ 1501 JWTAuthenticator: apiserver.JWTAuthenticator{ 1502 Issuer: apiserver.Issuer{ 1503 URL: "https://auth.example.com", 1504 Audiences: []string{"my-client"}, 1505 }, 1506 ClaimMappings: apiserver.ClaimMappings{ 1507 Username: apiserver.PrefixedClaimOrExpression{ 1508 Claim: "username", 1509 Prefix: pointer.String("prefix:"), 1510 }, 1511 }, 1512 }, 1513 now: func() time.Time { return now }, 1514 }, 1515 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1516 pubKeys: []*jose.JSONWebKey{ 1517 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1518 }, 1519 claims: fmt.Sprintf(`{ 1520 "iss": "https://auth.example.com", 1521 "aud": "not-my-client", 1522 "username": "jane", 1523 "exp": %d 1524 }`, valid.Unix()), 1525 wantErr: `oidc: verify token: oidc: expected audience "my-client" got ["not-my-client"]`, 1526 }, 1527 { 1528 // ID tokens may contain multiple audiences: 1529 // https://openid.net/specs/openid-connect-core-1_0.html#IDToken 1530 name: "multiple-audiences", 1531 options: Options{ 1532 JWTAuthenticator: apiserver.JWTAuthenticator{ 1533 Issuer: apiserver.Issuer{ 1534 URL: "https://auth.example.com", 1535 Audiences: []string{"my-client"}, 1536 }, 1537 ClaimMappings: apiserver.ClaimMappings{ 1538 Username: apiserver.PrefixedClaimOrExpression{ 1539 Claim: "username", 1540 Prefix: pointer.String(""), 1541 }, 1542 }, 1543 }, 1544 now: func() time.Time { return now }, 1545 }, 1546 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1547 pubKeys: []*jose.JSONWebKey{ 1548 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1549 }, 1550 claims: fmt.Sprintf(`{ 1551 "iss": "https://auth.example.com", 1552 "aud": ["not-my-client", "my-client"], 1553 "azp": "not-my-client", 1554 "username": "jane", 1555 "exp": %d 1556 }`, valid.Unix()), 1557 want: &user.DefaultInfo{ 1558 Name: "jane", 1559 }, 1560 }, 1561 { 1562 name: "multiple-audiences in authentication config", 1563 options: Options{ 1564 JWTAuthenticator: apiserver.JWTAuthenticator{ 1565 Issuer: apiserver.Issuer{ 1566 URL: "https://auth.example.com", 1567 Audiences: []string{"random-client", "my-client"}, 1568 AudienceMatchPolicy: "MatchAny", 1569 }, 1570 ClaimMappings: apiserver.ClaimMappings{ 1571 Username: apiserver.PrefixedClaimOrExpression{ 1572 Claim: "username", 1573 Prefix: pointer.String(""), 1574 }, 1575 }, 1576 }, 1577 now: func() time.Time { return now }, 1578 }, 1579 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1580 pubKeys: []*jose.JSONWebKey{ 1581 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1582 }, 1583 claims: fmt.Sprintf(`{ 1584 "iss": "https://auth.example.com", 1585 "aud": ["not-my-client", "my-client"], 1586 "azp": "not-my-client", 1587 "username": "jane", 1588 "exp": %d 1589 }`, valid.Unix()), 1590 want: &user.DefaultInfo{ 1591 Name: "jane", 1592 }, 1593 }, 1594 { 1595 name: "multiple-audiences in authentication config, multiple matches", 1596 options: Options{ 1597 JWTAuthenticator: apiserver.JWTAuthenticator{ 1598 Issuer: apiserver.Issuer{ 1599 URL: "https://auth.example.com", 1600 Audiences: []string{"random-client", "my-client", "other-client"}, 1601 AudienceMatchPolicy: "MatchAny", 1602 }, 1603 ClaimMappings: apiserver.ClaimMappings{ 1604 Username: apiserver.PrefixedClaimOrExpression{ 1605 Claim: "username", 1606 Prefix: pointer.String(""), 1607 }, 1608 }, 1609 }, 1610 now: func() time.Time { return now }, 1611 }, 1612 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1613 pubKeys: []*jose.JSONWebKey{ 1614 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1615 }, 1616 claims: fmt.Sprintf(`{ 1617 "iss": "https://auth.example.com", 1618 "aud": ["not-my-client", "my-client", "other-client"], 1619 "azp": "not-my-client", 1620 "username": "jane", 1621 "exp": %d 1622 }`, valid.Unix()), 1623 want: &user.DefaultInfo{ 1624 Name: "jane", 1625 }, 1626 }, 1627 { 1628 name: "multiple-audiences in authentication config, no match", 1629 options: Options{ 1630 JWTAuthenticator: apiserver.JWTAuthenticator{ 1631 Issuer: apiserver.Issuer{ 1632 URL: "https://auth.example.com", 1633 Audiences: []string{"random-client", "my-client"}, 1634 AudienceMatchPolicy: "MatchAny", 1635 }, 1636 ClaimMappings: apiserver.ClaimMappings{ 1637 Username: apiserver.PrefixedClaimOrExpression{ 1638 Claim: "username", 1639 Prefix: pointer.String(""), 1640 }, 1641 }, 1642 }, 1643 now: func() time.Time { return now }, 1644 }, 1645 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1646 pubKeys: []*jose.JSONWebKey{ 1647 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1648 }, 1649 claims: fmt.Sprintf(`{ 1650 "iss": "https://auth.example.com", 1651 "aud": ["not-my-client"], 1652 "azp": "not-my-client", 1653 "username": "jane", 1654 "exp": %d 1655 }`, valid.Unix()), 1656 wantErr: `oidc: verify token: oidc: expected audience in ["my-client" "random-client"] got ["not-my-client"]`, 1657 }, 1658 { 1659 name: "nuanced audience validation using claim validation rules", 1660 options: Options{ 1661 JWTAuthenticator: apiserver.JWTAuthenticator{ 1662 Issuer: apiserver.Issuer{ 1663 URL: "https://auth.example.com", 1664 Audiences: []string{"bar", "foo", "baz"}, 1665 AudienceMatchPolicy: "MatchAny", 1666 }, 1667 ClaimMappings: apiserver.ClaimMappings{ 1668 Username: apiserver.PrefixedClaimOrExpression{ 1669 Claim: "username", 1670 Prefix: pointer.String(""), 1671 }, 1672 }, 1673 ClaimValidationRules: []apiserver.ClaimValidationRule{ 1674 { 1675 Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`, 1676 Message: "audience must exactly contain [bar, foo, baz]", 1677 }, 1678 }, 1679 }, 1680 now: func() time.Time { return now }, 1681 }, 1682 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1683 pubKeys: []*jose.JSONWebKey{ 1684 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1685 }, 1686 claims: fmt.Sprintf(`{ 1687 "iss": "https://auth.example.com", 1688 "aud": ["foo", "bar", "baz"], 1689 "azp": "not-my-client", 1690 "username": "jane", 1691 "exp": %d 1692 }`, valid.Unix()), 1693 want: &user.DefaultInfo{ 1694 Name: "jane", 1695 }, 1696 }, 1697 { 1698 name: "audience validation using claim validation rules fails", 1699 options: Options{ 1700 JWTAuthenticator: apiserver.JWTAuthenticator{ 1701 Issuer: apiserver.Issuer{ 1702 URL: "https://auth.example.com", 1703 Audiences: []string{"bar", "foo", "baz"}, 1704 AudienceMatchPolicy: "MatchAny", 1705 }, 1706 ClaimMappings: apiserver.ClaimMappings{ 1707 Username: apiserver.PrefixedClaimOrExpression{ 1708 Claim: "username", 1709 Prefix: pointer.String(""), 1710 }, 1711 }, 1712 ClaimValidationRules: []apiserver.ClaimValidationRule{ 1713 { 1714 Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`, 1715 Message: "audience must exactly contain [bar, foo, baz]", 1716 }, 1717 }, 1718 }, 1719 now: func() time.Time { return now }, 1720 }, 1721 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1722 pubKeys: []*jose.JSONWebKey{ 1723 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1724 }, 1725 claims: fmt.Sprintf(`{ 1726 "iss": "https://auth.example.com", 1727 "aud": ["foo", "baz"], 1728 "azp": "not-my-client", 1729 "username": "jane", 1730 "exp": %d 1731 }`, valid.Unix()), 1732 wantErr: `oidc: error evaluating claim validation expression: validation expression 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' failed: audience must exactly contain [bar, foo, baz]`, 1733 }, 1734 { 1735 name: "invalid-issuer", 1736 options: Options{ 1737 JWTAuthenticator: apiserver.JWTAuthenticator{ 1738 Issuer: apiserver.Issuer{ 1739 URL: "https://auth.example.com", 1740 Audiences: []string{"my-client"}, 1741 }, 1742 ClaimMappings: apiserver.ClaimMappings{ 1743 Username: apiserver.PrefixedClaimOrExpression{ 1744 Claim: "username", 1745 Prefix: pointer.String("prefix:"), 1746 }, 1747 }, 1748 }, 1749 now: func() time.Time { return now }, 1750 }, 1751 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1752 pubKeys: []*jose.JSONWebKey{ 1753 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1754 }, 1755 claims: fmt.Sprintf(`{ 1756 "iss": "https://example.com", 1757 "aud": "my-client", 1758 "username": "jane", 1759 "exp": %d 1760 }`, valid.Unix()), 1761 wantSkip: true, 1762 }, 1763 { 1764 name: "username-prefix", 1765 options: Options{ 1766 JWTAuthenticator: apiserver.JWTAuthenticator{ 1767 Issuer: apiserver.Issuer{ 1768 URL: "https://auth.example.com", 1769 Audiences: []string{"my-client"}, 1770 }, 1771 ClaimMappings: apiserver.ClaimMappings{ 1772 Username: apiserver.PrefixedClaimOrExpression{ 1773 Claim: "username", 1774 Prefix: pointer.String("oidc:"), 1775 }, 1776 }, 1777 }, 1778 now: func() time.Time { return now }, 1779 }, 1780 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1781 pubKeys: []*jose.JSONWebKey{ 1782 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1783 }, 1784 claims: fmt.Sprintf(`{ 1785 "iss": "https://auth.example.com", 1786 "aud": "my-client", 1787 "username": "jane", 1788 "exp": %d 1789 }`, valid.Unix()), 1790 want: &user.DefaultInfo{ 1791 Name: "oidc:jane", 1792 }, 1793 }, 1794 { 1795 name: "groups-prefix", 1796 options: Options{ 1797 JWTAuthenticator: apiserver.JWTAuthenticator{ 1798 Issuer: apiserver.Issuer{ 1799 URL: "https://auth.example.com", 1800 Audiences: []string{"my-client"}, 1801 }, 1802 ClaimMappings: apiserver.ClaimMappings{ 1803 Username: apiserver.PrefixedClaimOrExpression{ 1804 Claim: "username", 1805 Prefix: pointer.String("oidc:"), 1806 }, 1807 Groups: apiserver.PrefixedClaimOrExpression{ 1808 Claim: "groups", 1809 Prefix: pointer.String("groups:"), 1810 }, 1811 }, 1812 }, 1813 now: func() time.Time { return now }, 1814 }, 1815 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1816 pubKeys: []*jose.JSONWebKey{ 1817 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1818 }, 1819 claims: fmt.Sprintf(`{ 1820 "iss": "https://auth.example.com", 1821 "aud": "my-client", 1822 "username": "jane", 1823 "groups": ["team1", "team2"], 1824 "exp": %d 1825 }`, valid.Unix()), 1826 want: &user.DefaultInfo{ 1827 Name: "oidc:jane", 1828 Groups: []string{"groups:team1", "groups:team2"}, 1829 }, 1830 }, 1831 { 1832 name: "groups-prefix-distributed", 1833 options: Options{ 1834 JWTAuthenticator: apiserver.JWTAuthenticator{ 1835 Issuer: apiserver.Issuer{ 1836 URL: "{{.URL}}", 1837 Audiences: []string{"my-client"}, 1838 }, 1839 ClaimMappings: apiserver.ClaimMappings{ 1840 Username: apiserver.PrefixedClaimOrExpression{ 1841 Claim: "username", 1842 Prefix: pointer.String("oidc:"), 1843 }, 1844 Groups: apiserver.PrefixedClaimOrExpression{ 1845 Claim: "groups", 1846 Prefix: pointer.String("groups:"), 1847 }, 1848 }, 1849 }, 1850 now: func() time.Time { return now }, 1851 }, 1852 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 1853 pubKeys: []*jose.JSONWebKey{ 1854 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1855 }, 1856 claims: fmt.Sprintf(`{ 1857 "iss": "{{.URL}}", 1858 "aud": "my-client", 1859 "username": "jane", 1860 "_claim_names": { 1861 "groups": "src1" 1862 }, 1863 "_claim_sources": { 1864 "src1": { 1865 "endpoint": "{{.URL}}/groups", 1866 "access_token": "groups_token" 1867 } 1868 }, 1869 "exp": %d 1870 }`, valid.Unix()), 1871 claimToResponseMap: map[string]string{ 1872 "groups": fmt.Sprintf(`{ 1873 "iss": "{{.URL}}", 1874 "aud": "my-client", 1875 "groups": ["team1", "team2"], 1876 "exp": %d 1877 }`, valid.Unix()), 1878 }, 1879 openIDConfig: `{ 1880 "issuer": "{{.URL}}", 1881 "jwks_uri": "{{.URL}}/.testing/keys" 1882 }`, 1883 want: &user.DefaultInfo{ 1884 Name: "oidc:jane", 1885 Groups: []string{"groups:team1", "groups:team2"}, 1886 }, 1887 }, 1888 { 1889 name: "invalid-signing-alg", 1890 options: Options{ 1891 JWTAuthenticator: apiserver.JWTAuthenticator{ 1892 Issuer: apiserver.Issuer{ 1893 URL: "https://auth.example.com", 1894 Audiences: []string{"my-client"}, 1895 }, 1896 ClaimMappings: apiserver.ClaimMappings{ 1897 Username: apiserver.PrefixedClaimOrExpression{ 1898 Claim: "username", 1899 Prefix: pointer.String("prefix:"), 1900 }, 1901 }, 1902 }, 1903 now: func() time.Time { return now }, 1904 }, 1905 // Correct key but invalid signature algorithm "PS256" 1906 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256), 1907 pubKeys: []*jose.JSONWebKey{ 1908 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 1909 }, 1910 claims: fmt.Sprintf(`{ 1911 "iss": "https://auth.example.com", 1912 "aud": "my-client", 1913 "username": "jane", 1914 "exp": %d 1915 }`, valid.Unix()), 1916 wantErr: `oidc: verify token: oidc: id token signed with unsupported algorithm, expected ["RS256"] got "PS256"`, 1917 }, 1918 { 1919 name: "ps256", 1920 options: Options{ 1921 JWTAuthenticator: apiserver.JWTAuthenticator{ 1922 Issuer: apiserver.Issuer{ 1923 URL: "https://auth.example.com", 1924 Audiences: []string{"my-client"}, 1925 }, 1926 ClaimMappings: apiserver.ClaimMappings{ 1927 Username: apiserver.PrefixedClaimOrExpression{ 1928 Claim: "username", 1929 Prefix: pointer.String(""), 1930 }, 1931 }, 1932 }, 1933 SupportedSigningAlgs: []string{"PS256"}, 1934 now: func() time.Time { return now }, 1935 }, 1936 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256), 1937 pubKeys: []*jose.JSONWebKey{ 1938 loadRSAKey(t, "testdata/rsa_1.pem", jose.PS256), 1939 }, 1940 claims: fmt.Sprintf(`{ 1941 "iss": "https://auth.example.com", 1942 "aud": "my-client", 1943 "username": "jane", 1944 "exp": %d 1945 }`, valid.Unix()), 1946 want: &user.DefaultInfo{ 1947 Name: "jane", 1948 }, 1949 }, 1950 { 1951 name: "es512", 1952 options: Options{ 1953 JWTAuthenticator: apiserver.JWTAuthenticator{ 1954 Issuer: apiserver.Issuer{ 1955 URL: "https://auth.example.com", 1956 Audiences: []string{"my-client"}, 1957 }, 1958 ClaimMappings: apiserver.ClaimMappings{ 1959 Username: apiserver.PrefixedClaimOrExpression{ 1960 Claim: "username", 1961 Prefix: pointer.String(""), 1962 }, 1963 }, 1964 }, 1965 SupportedSigningAlgs: []string{"ES512"}, 1966 now: func() time.Time { return now }, 1967 }, 1968 signingKey: loadECDSAPrivKey(t, "testdata/ecdsa_2.pem", jose.ES512), 1969 pubKeys: []*jose.JSONWebKey{ 1970 loadECDSAKey(t, "testdata/ecdsa_1.pem", jose.ES512), 1971 loadECDSAKey(t, "testdata/ecdsa_2.pem", jose.ES512), 1972 }, 1973 claims: fmt.Sprintf(`{ 1974 "iss": "https://auth.example.com", 1975 "aud": "my-client", 1976 "username": "jane", 1977 "exp": %d 1978 }`, valid.Unix()), 1979 want: &user.DefaultInfo{ 1980 Name: "jane", 1981 }, 1982 }, 1983 { 1984 name: "not-https", 1985 options: Options{ 1986 JWTAuthenticator: apiserver.JWTAuthenticator{ 1987 Issuer: apiserver.Issuer{ 1988 URL: "http://auth.example.com", 1989 Audiences: []string{"my-client"}, 1990 }, 1991 ClaimMappings: apiserver.ClaimMappings{ 1992 Username: apiserver.PrefixedClaimOrExpression{ 1993 Claim: "username", 1994 Prefix: pointer.String("prefix:"), 1995 }, 1996 }, 1997 }, 1998 now: func() time.Time { return now }, 1999 }, 2000 pubKeys: []*jose.JSONWebKey{ 2001 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2002 }, 2003 wantInitErr: `issuer.url: Invalid value: "http://auth.example.com": URL scheme must be https`, 2004 }, 2005 { 2006 name: "no-username-claim", 2007 options: Options{ 2008 JWTAuthenticator: apiserver.JWTAuthenticator{ 2009 Issuer: apiserver.Issuer{ 2010 URL: "https://auth.example.com", 2011 Audiences: []string{"my-client"}, 2012 }, 2013 ClaimMappings: apiserver.ClaimMappings{ 2014 Username: apiserver.PrefixedClaimOrExpression{ 2015 Prefix: pointer.String(""), 2016 }, 2017 }, 2018 }, 2019 now: func() time.Time { return now }, 2020 }, 2021 pubKeys: []*jose.JSONWebKey{ 2022 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2023 }, 2024 wantInitErr: `claimMappings.username: Required value: claim or expression is required`, 2025 }, 2026 { 2027 name: "invalid-sig-alg", 2028 options: Options{ 2029 JWTAuthenticator: apiserver.JWTAuthenticator{ 2030 Issuer: apiserver.Issuer{ 2031 URL: "https://auth.example.com", 2032 Audiences: []string{"my-client"}, 2033 }, 2034 ClaimMappings: apiserver.ClaimMappings{ 2035 Username: apiserver.PrefixedClaimOrExpression{ 2036 Claim: "username", 2037 Prefix: pointer.String("prefix:"), 2038 }, 2039 }, 2040 }, 2041 SupportedSigningAlgs: []string{"HS256"}, 2042 now: func() time.Time { return now }, 2043 }, 2044 pubKeys: []*jose.JSONWebKey{ 2045 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2046 }, 2047 wantInitErr: `oidc: unsupported signing alg: "HS256"`, 2048 }, 2049 { 2050 name: "client and ca mutually exclusive", 2051 options: Options{ 2052 JWTAuthenticator: apiserver.JWTAuthenticator{ 2053 Issuer: apiserver.Issuer{ 2054 URL: "https://auth.example.com", 2055 Audiences: []string{"my-client"}, 2056 }, 2057 ClaimMappings: apiserver.ClaimMappings{ 2058 Username: apiserver.PrefixedClaimOrExpression{ 2059 Claim: "username", 2060 Prefix: pointer.String("prefix:"), 2061 }, 2062 }, 2063 }, 2064 SupportedSigningAlgs: []string{"RS256"}, 2065 now: func() time.Time { return now }, 2066 Client: http.DefaultClient, // test automatically sets CAContentProvider 2067 }, 2068 pubKeys: []*jose.JSONWebKey{ 2069 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2070 }, 2071 wantInitErr: "oidc: Client and CAContentProvider are mutually exclusive", 2072 }, 2073 { 2074 name: "keyset and discovery URL mutually exclusive", 2075 options: Options{ 2076 JWTAuthenticator: apiserver.JWTAuthenticator{ 2077 Issuer: apiserver.Issuer{ 2078 URL: "https://auth.example.com", 2079 DiscoveryURL: "https://auth.example.com/foo", 2080 Audiences: []string{"my-client"}, 2081 }, 2082 ClaimMappings: apiserver.ClaimMappings{ 2083 Username: apiserver.PrefixedClaimOrExpression{ 2084 Claim: "username", 2085 Prefix: pointer.String("prefix:"), 2086 }, 2087 }, 2088 }, 2089 SupportedSigningAlgs: []string{"RS256"}, 2090 now: func() time.Time { return now }, 2091 KeySet: &staticKeySet{}, 2092 }, 2093 pubKeys: []*jose.JSONWebKey{ 2094 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2095 }, 2096 wantInitErr: "oidc: KeySet and DiscoveryURL are mutually exclusive", 2097 }, 2098 { 2099 name: "health check failure", 2100 options: Options{ 2101 JWTAuthenticator: apiserver.JWTAuthenticator{ 2102 Issuer: apiserver.Issuer{ 2103 URL: "https://this-will-not-work.notatld", 2104 Audiences: []string{"my-client"}, 2105 }, 2106 ClaimMappings: apiserver.ClaimMappings{ 2107 Username: apiserver.PrefixedClaimOrExpression{ 2108 Claim: "username", 2109 Prefix: pointer.String("prefix:"), 2110 }, 2111 }, 2112 }, 2113 SupportedSigningAlgs: []string{"RS256"}, 2114 }, 2115 fetchKeysFromRemote: true, 2116 wantHealthErrPrefix: `oidc: authenticator for issuer "https://this-will-not-work.notatld" is not healthy: Get "https://this-will-not-work.notatld/.well-known/openid-configuration": dial tcp: lookup this-will-not-work.notatld`, 2117 }, 2118 { 2119 name: "accounts.google.com issuer", 2120 options: Options{ 2121 JWTAuthenticator: apiserver.JWTAuthenticator{ 2122 Issuer: apiserver.Issuer{ 2123 URL: "https://accounts.google.com", 2124 Audiences: []string{"my-client"}, 2125 }, 2126 ClaimMappings: apiserver.ClaimMappings{ 2127 Username: apiserver.PrefixedClaimOrExpression{ 2128 Claim: "email", 2129 Prefix: pointer.String(""), 2130 }, 2131 }, 2132 }, 2133 now: func() time.Time { return now }, 2134 }, 2135 claims: fmt.Sprintf(`{ 2136 "iss": "accounts.google.com", 2137 "email": "thomas.jefferson@gmail.com", 2138 "aud": "my-client", 2139 "exp": %d 2140 }`, valid.Unix()), 2141 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2142 pubKeys: []*jose.JSONWebKey{ 2143 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2144 }, 2145 want: &user.DefaultInfo{ 2146 Name: "thomas.jefferson@gmail.com", 2147 }, 2148 }, 2149 { 2150 name: "good token with bad client id", 2151 options: Options{ 2152 JWTAuthenticator: apiserver.JWTAuthenticator{ 2153 Issuer: apiserver.Issuer{ 2154 URL: "https://auth.example.com", 2155 Audiences: []string{"my-client"}, 2156 }, 2157 ClaimMappings: apiserver.ClaimMappings{ 2158 Username: apiserver.PrefixedClaimOrExpression{ 2159 Claim: "username", 2160 Prefix: pointer.String("prefix:"), 2161 }, 2162 }, 2163 }, 2164 now: func() time.Time { return now }, 2165 }, 2166 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2167 pubKeys: []*jose.JSONWebKey{ 2168 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2169 }, 2170 claims: fmt.Sprintf(`{ 2171 "iss": "https://auth.example.com", 2172 "aud": "my-wrong-client", 2173 "username": "jane", 2174 "exp": %d 2175 }`, valid.Unix()), 2176 wantErr: `oidc: verify token: oidc: expected audience "my-client" got ["my-wrong-client"]`, 2177 }, 2178 { 2179 name: "user validation rule fails for user.username", 2180 options: Options{ 2181 JWTAuthenticator: apiserver.JWTAuthenticator{ 2182 Issuer: apiserver.Issuer{ 2183 URL: "https://auth.example.com", 2184 Audiences: []string{"my-client"}, 2185 }, 2186 ClaimMappings: apiserver.ClaimMappings{ 2187 Username: apiserver.PrefixedClaimOrExpression{ 2188 Claim: "username", 2189 Prefix: pointer.String("system:"), 2190 }, 2191 }, 2192 UserValidationRules: []apiserver.UserValidationRule{ 2193 { 2194 Expression: "!user.username.startsWith('system:')", 2195 Message: "username cannot used reserved system: prefix", 2196 }, 2197 }, 2198 }, 2199 now: func() time.Time { return now }, 2200 }, 2201 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2202 pubKeys: []*jose.JSONWebKey{ 2203 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2204 }, 2205 claims: fmt.Sprintf(`{ 2206 "iss": "https://auth.example.com", 2207 "aud": "my-client", 2208 "username": "jane", 2209 "exp": %d 2210 }`, valid.Unix()), 2211 wantErr: `oidc: error evaluating user info validation rule: validation expression '!user.username.startsWith('system:')' failed: username cannot used reserved system: prefix`, 2212 }, 2213 { 2214 name: "user validation rule fails for user.groups", 2215 options: Options{ 2216 JWTAuthenticator: apiserver.JWTAuthenticator{ 2217 Issuer: apiserver.Issuer{ 2218 URL: "https://auth.example.com", 2219 Audiences: []string{"my-client"}, 2220 }, 2221 ClaimMappings: apiserver.ClaimMappings{ 2222 Username: apiserver.PrefixedClaimOrExpression{ 2223 Expression: "claims.username", 2224 }, 2225 Groups: apiserver.PrefixedClaimOrExpression{ 2226 Claim: "groups", 2227 Prefix: pointer.String("system:"), 2228 }, 2229 }, 2230 UserValidationRules: []apiserver.UserValidationRule{ 2231 { 2232 Expression: "user.groups.all(group, !group.startsWith('system:'))", 2233 Message: "groups cannot used reserved system: prefix", 2234 }, 2235 }, 2236 }, 2237 now: func() time.Time { return now }, 2238 }, 2239 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2240 pubKeys: []*jose.JSONWebKey{ 2241 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2242 }, 2243 claims: fmt.Sprintf(`{ 2244 "iss": "https://auth.example.com", 2245 "aud": "my-client", 2246 "username": "jane", 2247 "exp": %d, 2248 "groups": ["team1", "team2"] 2249 }`, valid.Unix()), 2250 wantErr: `oidc: error evaluating user info validation rule: validation expression 'user.groups.all(group, !group.startsWith('system:'))' failed: groups cannot used reserved system: prefix`, 2251 }, 2252 { 2253 name: "claim validation rule with expression fails", 2254 options: Options{ 2255 JWTAuthenticator: apiserver.JWTAuthenticator{ 2256 Issuer: apiserver.Issuer{ 2257 URL: "https://auth.example.com", 2258 Audiences: []string{"my-client"}, 2259 }, 2260 ClaimMappings: apiserver.ClaimMappings{ 2261 Username: apiserver.PrefixedClaimOrExpression{ 2262 Claim: "username", 2263 Prefix: pointer.String(""), 2264 }, 2265 }, 2266 ClaimValidationRules: []apiserver.ClaimValidationRule{ 2267 { 2268 Expression: `claims.hd == "example.com"`, 2269 Message: "hd claim must be example.com", 2270 }, 2271 }, 2272 }, 2273 now: func() time.Time { return now }, 2274 }, 2275 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2276 pubKeys: []*jose.JSONWebKey{ 2277 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2278 }, 2279 claims: fmt.Sprintf(`{ 2280 "iss": "https://auth.example.com", 2281 "aud": "my-client", 2282 "username": "jane", 2283 "exp": %d 2284 }`, valid.Unix()), 2285 wantErr: `oidc: error evaluating claim validation expression: expression 'claims.hd == "example.com"' resulted in error: no such key: hd`, 2286 }, 2287 { 2288 name: "claim validation rule with expression", 2289 options: Options{ 2290 JWTAuthenticator: apiserver.JWTAuthenticator{ 2291 Issuer: apiserver.Issuer{ 2292 URL: "https://auth.example.com", 2293 Audiences: []string{"my-client"}, 2294 }, 2295 ClaimMappings: apiserver.ClaimMappings{ 2296 Username: apiserver.PrefixedClaimOrExpression{ 2297 Claim: "username", 2298 Prefix: pointer.String(""), 2299 }, 2300 }, 2301 ClaimValidationRules: []apiserver.ClaimValidationRule{ 2302 { 2303 Expression: `claims.hd == "example.com"`, 2304 Message: "hd claim must be example.com", 2305 }, 2306 }, 2307 }, 2308 now: func() time.Time { return now }, 2309 }, 2310 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2311 pubKeys: []*jose.JSONWebKey{ 2312 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2313 }, 2314 claims: fmt.Sprintf(`{ 2315 "iss": "https://auth.example.com", 2316 "aud": "my-client", 2317 "username": "jane", 2318 "exp": %d, 2319 "hd": "example.com" 2320 }`, valid.Unix()), 2321 want: &user.DefaultInfo{ 2322 Name: "jane", 2323 }, 2324 }, 2325 { 2326 name: "claim validation rule with expression and nested claims", 2327 options: Options{ 2328 JWTAuthenticator: apiserver.JWTAuthenticator{ 2329 Issuer: apiserver.Issuer{ 2330 URL: "https://auth.example.com", 2331 Audiences: []string{"my-client"}, 2332 }, 2333 ClaimMappings: apiserver.ClaimMappings{ 2334 Username: apiserver.PrefixedClaimOrExpression{ 2335 Claim: "username", 2336 Prefix: pointer.String(""), 2337 }, 2338 }, 2339 ClaimValidationRules: []apiserver.ClaimValidationRule{ 2340 { 2341 Expression: `claims.foo.bar == "baz"`, 2342 Message: "foo.bar claim must be baz", 2343 }, 2344 }, 2345 }, 2346 now: func() time.Time { return now }, 2347 }, 2348 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2349 pubKeys: []*jose.JSONWebKey{ 2350 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2351 }, 2352 claims: fmt.Sprintf(`{ 2353 "iss": "https://auth.example.com", 2354 "aud": "my-client", 2355 "username": "jane", 2356 "exp": %d, 2357 "hd": "example.com", 2358 "foo": { 2359 "bar": "baz" 2360 } 2361 }`, valid.Unix()), 2362 want: &user.DefaultInfo{ 2363 Name: "jane", 2364 }, 2365 }, 2366 { 2367 name: "claim validation rule with mix of expression and claim", 2368 options: Options{ 2369 JWTAuthenticator: apiserver.JWTAuthenticator{ 2370 Issuer: apiserver.Issuer{ 2371 URL: "https://auth.example.com", 2372 Audiences: []string{"my-client"}, 2373 }, 2374 ClaimMappings: apiserver.ClaimMappings{ 2375 Username: apiserver.PrefixedClaimOrExpression{ 2376 Claim: "username", 2377 Prefix: pointer.String(""), 2378 }, 2379 }, 2380 ClaimValidationRules: []apiserver.ClaimValidationRule{ 2381 { 2382 Expression: `claims.foo.bar == "baz"`, 2383 Message: "foo.bar claim must be baz", 2384 }, 2385 { 2386 Claim: "hd", 2387 RequiredValue: "example.com", 2388 }, 2389 }, 2390 }, 2391 now: func() time.Time { return now }, 2392 }, 2393 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2394 pubKeys: []*jose.JSONWebKey{ 2395 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2396 }, 2397 claims: fmt.Sprintf(`{ 2398 "iss": "https://auth.example.com", 2399 "aud": "my-client", 2400 "username": "jane", 2401 "exp": %d, 2402 "hd": "example.com", 2403 "foo": { 2404 "bar": "baz" 2405 } 2406 }`, valid.Unix()), 2407 want: &user.DefaultInfo{ 2408 Name: "jane", 2409 }, 2410 }, 2411 { 2412 name: "username claim mapping with expression", 2413 options: Options{ 2414 JWTAuthenticator: apiserver.JWTAuthenticator{ 2415 Issuer: apiserver.Issuer{ 2416 URL: "https://auth.example.com", 2417 Audiences: []string{"my-client"}, 2418 }, 2419 ClaimMappings: apiserver.ClaimMappings{ 2420 Username: apiserver.PrefixedClaimOrExpression{ 2421 Expression: "claims.username", 2422 }, 2423 }, 2424 }, 2425 now: func() time.Time { return now }, 2426 }, 2427 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2428 pubKeys: []*jose.JSONWebKey{ 2429 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2430 }, 2431 claims: fmt.Sprintf(`{ 2432 "iss": "https://auth.example.com", 2433 "aud": "my-client", 2434 "username": "jane", 2435 "exp": %d 2436 }`, valid.Unix()), 2437 want: &user.DefaultInfo{ 2438 Name: "jane", 2439 }, 2440 }, 2441 { 2442 name: "username claim mapping with expression and nested claim", 2443 options: Options{ 2444 JWTAuthenticator: apiserver.JWTAuthenticator{ 2445 Issuer: apiserver.Issuer{ 2446 URL: "https://auth.example.com", 2447 Audiences: []string{"my-client"}, 2448 }, 2449 ClaimMappings: apiserver.ClaimMappings{ 2450 Username: apiserver.PrefixedClaimOrExpression{ 2451 Expression: "claims.foo.username", 2452 }, 2453 }, 2454 }, 2455 now: func() time.Time { return now }, 2456 }, 2457 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2458 pubKeys: []*jose.JSONWebKey{ 2459 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2460 }, 2461 claims: fmt.Sprintf(`{ 2462 "iss": "https://auth.example.com", 2463 "aud": "my-client", 2464 "username": "jane", 2465 "exp": %d, 2466 "foo": { 2467 "username": "jane" 2468 } 2469 }`, valid.Unix()), 2470 want: &user.DefaultInfo{ 2471 Name: "jane", 2472 }, 2473 }, 2474 { 2475 name: "groups claim mapping with expression", 2476 options: Options{ 2477 JWTAuthenticator: apiserver.JWTAuthenticator{ 2478 Issuer: apiserver.Issuer{ 2479 URL: "https://auth.example.com", 2480 Audiences: []string{"my-client"}, 2481 }, 2482 ClaimMappings: apiserver.ClaimMappings{ 2483 Username: apiserver.PrefixedClaimOrExpression{ 2484 Expression: "claims.username", 2485 }, 2486 Groups: apiserver.PrefixedClaimOrExpression{ 2487 Expression: "claims.groups", 2488 }, 2489 }, 2490 }, 2491 now: func() time.Time { return now }, 2492 }, 2493 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2494 pubKeys: []*jose.JSONWebKey{ 2495 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2496 }, 2497 claims: fmt.Sprintf(`{ 2498 "iss": "https://auth.example.com", 2499 "aud": "my-client", 2500 "username": "jane", 2501 "groups": ["team1", "team2"], 2502 "exp": %d 2503 }`, valid.Unix()), 2504 want: &user.DefaultInfo{ 2505 Name: "jane", 2506 Groups: []string{"team1", "team2"}, 2507 }, 2508 }, 2509 { 2510 name: "groups claim with expression", 2511 options: Options{ 2512 JWTAuthenticator: apiserver.JWTAuthenticator{ 2513 Issuer: apiserver.Issuer{ 2514 URL: "https://auth.example.com", 2515 Audiences: []string{"my-client"}, 2516 }, 2517 ClaimMappings: apiserver.ClaimMappings{ 2518 Username: apiserver.PrefixedClaimOrExpression{ 2519 Claim: "username", 2520 Prefix: pointer.String("oidc:"), 2521 }, 2522 Groups: apiserver.PrefixedClaimOrExpression{ 2523 Expression: `(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "groups:" + role)`, 2524 }, 2525 }, 2526 }, 2527 now: func() time.Time { return now }, 2528 }, 2529 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2530 pubKeys: []*jose.JSONWebKey{ 2531 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2532 }, 2533 claims: fmt.Sprintf(`{ 2534 "iss": "https://auth.example.com", 2535 "aud": "my-client", 2536 "username": "jane", 2537 "roles": "foo,bar", 2538 "other_roles": "baz,qux", 2539 "exp": %d 2540 }`, valid.Unix()), 2541 want: &user.DefaultInfo{ 2542 Name: "oidc:jane", 2543 Groups: []string{"groups:foo", "groups:bar", "groups:baz", "groups:qux"}, 2544 }, 2545 }, 2546 { 2547 name: "uid claim mapping with expression", 2548 options: Options{ 2549 JWTAuthenticator: apiserver.JWTAuthenticator{ 2550 Issuer: apiserver.Issuer{ 2551 URL: "https://auth.example.com", 2552 Audiences: []string{"my-client"}, 2553 }, 2554 ClaimMappings: apiserver.ClaimMappings{ 2555 Username: apiserver.PrefixedClaimOrExpression{ 2556 Expression: "claims.username", 2557 }, 2558 Groups: apiserver.PrefixedClaimOrExpression{ 2559 Expression: "claims.groups", 2560 }, 2561 UID: apiserver.ClaimOrExpression{ 2562 Expression: "claims.uid", 2563 }, 2564 }, 2565 }, 2566 now: func() time.Time { return now }, 2567 }, 2568 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2569 pubKeys: []*jose.JSONWebKey{ 2570 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2571 }, 2572 claims: fmt.Sprintf(`{ 2573 "iss": "https://auth.example.com", 2574 "aud": "my-client", 2575 "username": "jane", 2576 "groups": ["team1", "team2"], 2577 "exp": %d, 2578 "uid": "1234" 2579 }`, valid.Unix()), 2580 want: &user.DefaultInfo{ 2581 Name: "jane", 2582 Groups: []string{"team1", "team2"}, 2583 UID: "1234", 2584 }, 2585 }, 2586 { 2587 name: "uid claim mapping with claim", 2588 options: Options{ 2589 JWTAuthenticator: apiserver.JWTAuthenticator{ 2590 Issuer: apiserver.Issuer{ 2591 URL: "https://auth.example.com", 2592 Audiences: []string{"my-client"}, 2593 }, 2594 ClaimMappings: apiserver.ClaimMappings{ 2595 Username: apiserver.PrefixedClaimOrExpression{ 2596 Expression: "claims.username", 2597 }, 2598 Groups: apiserver.PrefixedClaimOrExpression{ 2599 Expression: "claims.groups", 2600 }, 2601 UID: apiserver.ClaimOrExpression{ 2602 Claim: "uid", 2603 }, 2604 }, 2605 }, 2606 now: func() time.Time { return now }, 2607 }, 2608 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2609 pubKeys: []*jose.JSONWebKey{ 2610 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2611 }, 2612 claims: fmt.Sprintf(`{ 2613 "iss": "https://auth.example.com", 2614 "aud": "my-client", 2615 "username": "jane", 2616 "groups": ["team1", "team2"], 2617 "exp": %d, 2618 "uid": "1234" 2619 }`, valid.Unix()), 2620 want: &user.DefaultInfo{ 2621 Name: "jane", 2622 Groups: []string{"team1", "team2"}, 2623 UID: "1234", 2624 }, 2625 }, 2626 { 2627 name: "extra claim mapping with expression", 2628 options: Options{ 2629 JWTAuthenticator: apiserver.JWTAuthenticator{ 2630 Issuer: apiserver.Issuer{ 2631 URL: "https://auth.example.com", 2632 Audiences: []string{"my-client"}, 2633 }, 2634 ClaimMappings: apiserver.ClaimMappings{ 2635 Username: apiserver.PrefixedClaimOrExpression{ 2636 Expression: "claims.username", 2637 }, 2638 Groups: apiserver.PrefixedClaimOrExpression{ 2639 Expression: "claims.groups", 2640 }, 2641 UID: apiserver.ClaimOrExpression{ 2642 Expression: "claims.uid", 2643 }, 2644 Extra: []apiserver.ExtraMapping{ 2645 { 2646 Key: "example.org/foo", 2647 ValueExpression: "claims.foo", 2648 }, 2649 { 2650 Key: "example.org/bar", 2651 ValueExpression: "claims.bar", 2652 }, 2653 }, 2654 }, 2655 }, 2656 now: func() time.Time { return now }, 2657 }, 2658 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2659 pubKeys: []*jose.JSONWebKey{ 2660 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2661 }, 2662 claims: fmt.Sprintf(`{ 2663 "iss": "https://auth.example.com", 2664 "aud": "my-client", 2665 "username": "jane", 2666 "groups": ["team1", "team2"], 2667 "exp": %d, 2668 "uid": "1234", 2669 "foo": "bar", 2670 "bar": [ 2671 "baz", 2672 "qux" 2673 ] 2674 }`, valid.Unix()), 2675 want: &user.DefaultInfo{ 2676 Name: "jane", 2677 Groups: []string{"team1", "team2"}, 2678 UID: "1234", 2679 Extra: map[string][]string{ 2680 "example.org/foo": {"bar"}, 2681 "example.org/bar": {"baz", "qux"}, 2682 }, 2683 }, 2684 }, 2685 { 2686 name: "extra claim mapping, value derived from claim value", 2687 options: Options{ 2688 JWTAuthenticator: apiserver.JWTAuthenticator{ 2689 Issuer: apiserver.Issuer{ 2690 URL: "https://auth.example.com", 2691 Audiences: []string{"my-client"}, 2692 }, 2693 ClaimMappings: apiserver.ClaimMappings{ 2694 Username: apiserver.PrefixedClaimOrExpression{ 2695 Expression: "claims.username", 2696 }, 2697 Extra: []apiserver.ExtraMapping{ 2698 { 2699 Key: "example.org/admin", 2700 ValueExpression: `(has(claims.is_admin) && claims.is_admin) ? "true":""`, 2701 }, 2702 { 2703 Key: "example.org/admin_1", 2704 ValueExpression: `claims.?is_admin.orValue(false) == true ? "true":""`, 2705 }, 2706 { 2707 Key: "example.org/non_existent", 2708 ValueExpression: `claims.?non_existent.orValue("default") == "default" ? "true":""`, 2709 }, 2710 }, 2711 }, 2712 }, 2713 now: func() time.Time { return now }, 2714 }, 2715 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2716 pubKeys: []*jose.JSONWebKey{ 2717 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2718 }, 2719 claims: fmt.Sprintf(`{ 2720 "iss": "https://auth.example.com", 2721 "aud": "my-client", 2722 "username": "jane", 2723 "exp": %d, 2724 "is_admin": true 2725 }`, valid.Unix()), 2726 want: &user.DefaultInfo{ 2727 Name: "jane", 2728 Extra: map[string][]string{ 2729 "example.org/admin": {"true"}, 2730 "example.org/admin_1": {"true"}, 2731 "example.org/non_existent": {"true"}, 2732 }, 2733 }, 2734 }, 2735 { 2736 name: "hardcoded extra claim mapping", 2737 options: Options{ 2738 JWTAuthenticator: apiserver.JWTAuthenticator{ 2739 Issuer: apiserver.Issuer{ 2740 URL: "https://auth.example.com", 2741 Audiences: []string{"my-client"}, 2742 }, 2743 ClaimMappings: apiserver.ClaimMappings{ 2744 Username: apiserver.PrefixedClaimOrExpression{ 2745 Expression: "claims.username", 2746 }, 2747 Extra: []apiserver.ExtraMapping{ 2748 { 2749 Key: "example.org/admin", 2750 ValueExpression: `"true"`, 2751 }, 2752 }, 2753 }, 2754 }, 2755 now: func() time.Time { return now }, 2756 }, 2757 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2758 pubKeys: []*jose.JSONWebKey{ 2759 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2760 }, 2761 claims: fmt.Sprintf(`{ 2762 "iss": "https://auth.example.com", 2763 "aud": "my-client", 2764 "username": "jane", 2765 "exp": %d, 2766 "is_admin": true 2767 }`, valid.Unix()), 2768 want: &user.DefaultInfo{ 2769 Name: "jane", 2770 Extra: map[string][]string{ 2771 "example.org/admin": {"true"}, 2772 }, 2773 }, 2774 }, 2775 { 2776 name: "extra claim mapping, multiple expressions for same key", 2777 options: Options{ 2778 JWTAuthenticator: apiserver.JWTAuthenticator{ 2779 Issuer: apiserver.Issuer{ 2780 URL: "https://auth.example.com", 2781 Audiences: []string{"my-client"}, 2782 }, 2783 ClaimMappings: apiserver.ClaimMappings{ 2784 Username: apiserver.PrefixedClaimOrExpression{ 2785 Expression: "claims.username", 2786 }, 2787 Groups: apiserver.PrefixedClaimOrExpression{ 2788 Expression: "claims.groups", 2789 }, 2790 UID: apiserver.ClaimOrExpression{ 2791 Expression: "claims.uid", 2792 }, 2793 Extra: []apiserver.ExtraMapping{ 2794 { 2795 Key: "example.org/foo", 2796 ValueExpression: "claims.foo", 2797 }, 2798 { 2799 Key: "example.org/bar", 2800 ValueExpression: "claims.bar", 2801 }, 2802 { 2803 Key: "example.org/foo", 2804 ValueExpression: "claims.bar", 2805 }, 2806 }, 2807 }, 2808 }, 2809 now: func() time.Time { return now }, 2810 }, 2811 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2812 pubKeys: []*jose.JSONWebKey{ 2813 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2814 }, 2815 claims: fmt.Sprintf(`{ 2816 "iss": "https://auth.example.com", 2817 "aud": "my-client", 2818 "username": "jane", 2819 "groups": ["team1", "team2"], 2820 "exp": %d, 2821 "uid": "1234", 2822 "foo": "bar", 2823 "bar": [ 2824 "baz", 2825 "qux" 2826 ] 2827 }`, valid.Unix()), 2828 wantInitErr: `claimMappings.extra[2].key: Duplicate value: "example.org/foo"`, 2829 }, 2830 { 2831 name: "disallowed issuer via configured value", 2832 options: Options{ 2833 JWTAuthenticator: apiserver.JWTAuthenticator{ 2834 Issuer: apiserver.Issuer{ 2835 URL: "https://auth.example.com", 2836 Audiences: []string{"my-client"}, 2837 }, 2838 ClaimMappings: apiserver.ClaimMappings{ 2839 Username: apiserver.PrefixedClaimOrExpression{ 2840 Expression: "claims.username", 2841 }, 2842 Groups: apiserver.PrefixedClaimOrExpression{ 2843 Expression: "claims.groups", 2844 }, 2845 UID: apiserver.ClaimOrExpression{ 2846 Expression: "claims.uid", 2847 }, 2848 Extra: []apiserver.ExtraMapping{ 2849 { 2850 Key: "example.org/foo", 2851 ValueExpression: "claims.foo", 2852 }, 2853 { 2854 Key: "example.org/bar", 2855 ValueExpression: "claims.bar", 2856 }, 2857 }, 2858 }, 2859 }, 2860 DisallowedIssuers: []string{"https://auth.example.com"}, 2861 now: func() time.Time { return now }, 2862 }, 2863 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2864 pubKeys: []*jose.JSONWebKey{ 2865 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2866 }, 2867 claims: fmt.Sprintf(`{ 2868 "iss": "https://auth.example.com", 2869 "aud": "my-client", 2870 "username": "jane", 2871 "groups": ["team1", "team2"], 2872 "exp": %d, 2873 "uid": "1234", 2874 "foo": "bar", 2875 "bar": [ 2876 "baz", 2877 "qux" 2878 ] 2879 }`, valid.Unix()), 2880 wantInitErr: `issuer.url: Invalid value: "https://auth.example.com": URL must not overlap with disallowed issuers: [https://auth.example.com]`, 2881 }, 2882 { 2883 name: "extra claim mapping, empty string value for key", 2884 options: Options{ 2885 JWTAuthenticator: apiserver.JWTAuthenticator{ 2886 Issuer: apiserver.Issuer{ 2887 URL: "https://auth.example.com", 2888 Audiences: []string{"my-client"}, 2889 }, 2890 ClaimMappings: apiserver.ClaimMappings{ 2891 Username: apiserver.PrefixedClaimOrExpression{ 2892 Expression: "claims.username", 2893 }, 2894 Groups: apiserver.PrefixedClaimOrExpression{ 2895 Expression: "claims.groups", 2896 }, 2897 UID: apiserver.ClaimOrExpression{ 2898 Expression: "claims.uid", 2899 }, 2900 Extra: []apiserver.ExtraMapping{ 2901 { 2902 Key: "example.org/foo", 2903 ValueExpression: "claims.foo", 2904 }, 2905 { 2906 Key: "example.org/bar", 2907 ValueExpression: "claims.bar", 2908 }, 2909 }, 2910 }, 2911 }, 2912 now: func() time.Time { return now }, 2913 }, 2914 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2915 pubKeys: []*jose.JSONWebKey{ 2916 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2917 }, 2918 claims: fmt.Sprintf(`{ 2919 "iss": "https://auth.example.com", 2920 "aud": "my-client", 2921 "username": "jane", 2922 "groups": ["team1", "team2"], 2923 "exp": %d, 2924 "uid": "1234", 2925 "foo": "", 2926 "bar": [ 2927 "baz", 2928 "qux" 2929 ] 2930 }`, valid.Unix()), 2931 want: &user.DefaultInfo{ 2932 Name: "jane", 2933 Groups: []string{"team1", "team2"}, 2934 UID: "1234", 2935 Extra: map[string][]string{ 2936 "example.org/bar": {"baz", "qux"}, 2937 }, 2938 }, 2939 }, 2940 { 2941 name: "extra claim mapping with user validation rule succeeds", 2942 options: Options{ 2943 JWTAuthenticator: apiserver.JWTAuthenticator{ 2944 Issuer: apiserver.Issuer{ 2945 URL: "https://auth.example.com", 2946 Audiences: []string{"my-client"}, 2947 }, 2948 ClaimMappings: apiserver.ClaimMappings{ 2949 Username: apiserver.PrefixedClaimOrExpression{ 2950 Expression: "claims.username", 2951 }, 2952 Groups: apiserver.PrefixedClaimOrExpression{ 2953 Expression: "claims.groups", 2954 }, 2955 UID: apiserver.ClaimOrExpression{ 2956 Expression: "claims.uid", 2957 }, 2958 Extra: []apiserver.ExtraMapping{ 2959 { 2960 Key: "example.org/foo", 2961 ValueExpression: "'bar'", 2962 }, 2963 { 2964 Key: "example.org/baz", 2965 ValueExpression: "claims.baz", 2966 }, 2967 }, 2968 }, 2969 UserValidationRules: []apiserver.UserValidationRule{ 2970 { 2971 Expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']", 2972 Message: "example.org/foo must be bar and example.org/baz must be qux", 2973 }, 2974 }, 2975 }, 2976 now: func() time.Time { return now }, 2977 }, 2978 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 2979 pubKeys: []*jose.JSONWebKey{ 2980 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 2981 }, 2982 claims: fmt.Sprintf(`{ 2983 "iss": "https://auth.example.com", 2984 "aud": "my-client", 2985 "username": "jane", 2986 "groups": ["team1", "team2"], 2987 "exp": %d, 2988 "uid": "1234", 2989 "baz": "qux" 2990 }`, valid.Unix()), 2991 want: &user.DefaultInfo{ 2992 Name: "jane", 2993 Groups: []string{"team1", "team2"}, 2994 UID: "1234", 2995 Extra: map[string][]string{ 2996 "example.org/foo": {"bar"}, 2997 "example.org/baz": {"qux"}, 2998 }, 2999 }, 3000 }, 3001 { 3002 name: "groups expression returns null", 3003 options: Options{ 3004 JWTAuthenticator: apiserver.JWTAuthenticator{ 3005 Issuer: apiserver.Issuer{ 3006 URL: "https://auth.example.com", 3007 Audiences: []string{"my-client"}, 3008 }, 3009 ClaimMappings: apiserver.ClaimMappings{ 3010 Username: apiserver.PrefixedClaimOrExpression{ 3011 Expression: "claims.username", 3012 }, 3013 Groups: apiserver.PrefixedClaimOrExpression{ 3014 Expression: "claims.groups", 3015 }, 3016 }, 3017 }, 3018 now: func() time.Time { return now }, 3019 }, 3020 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3021 pubKeys: []*jose.JSONWebKey{ 3022 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3023 }, 3024 claims: fmt.Sprintf(`{ 3025 "iss": "https://auth.example.com", 3026 "aud": "my-client", 3027 "username": "jane", 3028 "groups": null, 3029 "exp": %d, 3030 "uid": "1234", 3031 "baz": "qux" 3032 }`, valid.Unix()), 3033 want: &user.DefaultInfo{ 3034 Name: "jane", 3035 }, 3036 }, 3037 // test to ensure omitempty fields not included in user info 3038 // are set and accessible for CEL evaluation. 3039 { 3040 name: "test user validation rule doesn't fail when user info is empty except username", 3041 options: Options{ 3042 JWTAuthenticator: apiserver.JWTAuthenticator{ 3043 Issuer: apiserver.Issuer{ 3044 URL: "https://auth.example.com", 3045 Audiences: []string{"my-client"}, 3046 }, 3047 ClaimMappings: apiserver.ClaimMappings{ 3048 Username: apiserver.PrefixedClaimOrExpression{ 3049 Expression: "claims.username", 3050 }, 3051 Groups: apiserver.PrefixedClaimOrExpression{ 3052 Expression: "claims.groups", 3053 }, 3054 }, 3055 UserValidationRules: []apiserver.UserValidationRule{ 3056 { 3057 Expression: `user.username == " "`, 3058 Message: "username must be single space", 3059 }, 3060 { 3061 Expression: `user.uid == ""`, 3062 Message: "uid must be empty string", 3063 }, 3064 { 3065 Expression: `!('bar' in user.groups)`, 3066 Message: "groups must not contain bar", 3067 }, 3068 { 3069 Expression: `!('bar' in user.extra)`, 3070 Message: "extra must not contain bar", 3071 }, 3072 }, 3073 }, 3074 now: func() time.Time { return now }, 3075 }, 3076 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3077 pubKeys: []*jose.JSONWebKey{ 3078 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3079 }, 3080 claims: fmt.Sprintf(`{ 3081 "iss": "https://auth.example.com", 3082 "aud": "my-client", 3083 "username": " ", 3084 "groups": null, 3085 "exp": %d, 3086 "baz": "qux" 3087 }`, valid.Unix()), 3088 want: &user.DefaultInfo{Name: " "}, 3089 }, 3090 { 3091 name: "empty username is allowed via claim", 3092 options: Options{ 3093 JWTAuthenticator: apiserver.JWTAuthenticator{ 3094 Issuer: apiserver.Issuer{ 3095 URL: "https://auth.example.com", 3096 Audiences: []string{"my-client"}, 3097 }, 3098 ClaimMappings: apiserver.ClaimMappings{ 3099 Username: apiserver.PrefixedClaimOrExpression{ 3100 Claim: "username", 3101 Prefix: pointer.String(""), 3102 }, 3103 Groups: apiserver.PrefixedClaimOrExpression{ 3104 Expression: "claims.groups", 3105 }, 3106 }, 3107 UserValidationRules: []apiserver.UserValidationRule{ 3108 { 3109 Expression: `user.username == ""`, 3110 Message: "username must be empty string", 3111 }, 3112 { 3113 Expression: `user.uid == ""`, 3114 Message: "uid must be empty string", 3115 }, 3116 { 3117 Expression: `!('bar' in user.groups)`, 3118 Message: "groups must not contain bar", 3119 }, 3120 { 3121 Expression: `!('bar' in user.extra)`, 3122 Message: "extra must not contain bar", 3123 }, 3124 }, 3125 }, 3126 now: func() time.Time { return now }, 3127 }, 3128 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3129 pubKeys: []*jose.JSONWebKey{ 3130 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3131 }, 3132 claims: fmt.Sprintf(`{ 3133 "iss": "https://auth.example.com", 3134 "aud": "my-client", 3135 "username": "", 3136 "groups": null, 3137 "exp": %d, 3138 "baz": "qux" 3139 }`, valid.Unix()), 3140 want: &user.DefaultInfo{}, 3141 }, 3142 // test to assert the minimum valid jwt payload 3143 // the required claims are iss, aud, exp and <claimMappings.Username> (in this case user). 3144 { 3145 name: "minimum valid jwt payload", 3146 options: Options{ 3147 JWTAuthenticator: apiserver.JWTAuthenticator{ 3148 Issuer: apiserver.Issuer{ 3149 URL: "https://auth.example.com", 3150 Audiences: []string{"my-client"}, 3151 }, 3152 ClaimMappings: apiserver.ClaimMappings{ 3153 Username: apiserver.PrefixedClaimOrExpression{ 3154 Expression: "claims.user", 3155 }, 3156 }, 3157 }, 3158 now: func() time.Time { return now }, 3159 }, 3160 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3161 pubKeys: []*jose.JSONWebKey{ 3162 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3163 }, 3164 claims: fmt.Sprintf(`{ 3165 "iss": "https://auth.example.com", 3166 "aud": "my-client", 3167 "user": "jane", 3168 "exp": %d 3169 }`, valid.Unix()), 3170 want: &user.DefaultInfo{ 3171 Name: "jane", 3172 }, 3173 }, 3174 { 3175 name: "discovery-url", 3176 options: Options{ 3177 JWTAuthenticator: apiserver.JWTAuthenticator{ 3178 Issuer: apiserver.Issuer{ 3179 URL: "https://auth.example.com", 3180 DiscoveryURL: "{{.URL}}/.well-known/openid-configuration", 3181 Audiences: []string{"my-client"}, 3182 }, 3183 ClaimMappings: apiserver.ClaimMappings{ 3184 Username: apiserver.PrefixedClaimOrExpression{ 3185 Claim: "username", 3186 Prefix: pointer.String(""), 3187 }, 3188 }, 3189 }, 3190 now: func() time.Time { return now }, 3191 }, 3192 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3193 pubKeys: []*jose.JSONWebKey{ 3194 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3195 }, 3196 claims: fmt.Sprintf(`{ 3197 "iss": "https://auth.example.com", 3198 "aud": "my-client", 3199 "username": "jane", 3200 "exp": %d 3201 }`, valid.Unix()), 3202 openIDConfig: `{ 3203 "issuer": "https://auth.example.com", 3204 "jwks_uri": "{{.URL}}/.testing/keys" 3205 }`, 3206 fetchKeysFromRemote: true, 3207 want: &user.DefaultInfo{ 3208 Name: "jane", 3209 }, 3210 }, 3211 { 3212 name: "discovery url, issuer has a path", 3213 options: Options{ 3214 JWTAuthenticator: apiserver.JWTAuthenticator{ 3215 Issuer: apiserver.Issuer{ 3216 URL: "https://auth.example.com/a/b/foo", 3217 DiscoveryURL: "{{.URL}}/.well-known/openid-configuration", 3218 Audiences: []string{"my-client"}, 3219 }, 3220 ClaimMappings: apiserver.ClaimMappings{ 3221 Username: apiserver.PrefixedClaimOrExpression{ 3222 Claim: "username", 3223 Prefix: pointer.String(""), 3224 }, 3225 }, 3226 }, 3227 now: func() time.Time { return now }, 3228 }, 3229 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3230 pubKeys: []*jose.JSONWebKey{ 3231 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3232 }, 3233 claims: fmt.Sprintf(`{ 3234 "iss": "https://auth.example.com/a/b/foo", 3235 "aud": "my-client", 3236 "username": "jane", 3237 "exp": %d 3238 }`, valid.Unix()), 3239 openIDConfig: `{ 3240 "issuer": "https://auth.example.com/a/b/foo", 3241 "jwks_uri": "{{.URL}}/.testing/keys" 3242 }`, 3243 fetchKeysFromRemote: true, 3244 want: &user.DefaultInfo{ 3245 Name: "jane", 3246 }, 3247 }, 3248 { 3249 name: "discovery url has a path, issuer url has no path", 3250 options: Options{ 3251 JWTAuthenticator: apiserver.JWTAuthenticator{ 3252 Issuer: apiserver.Issuer{ 3253 URL: "https://auth.example.com", 3254 DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration", 3255 Audiences: []string{"my-client"}, 3256 }, 3257 ClaimMappings: apiserver.ClaimMappings{ 3258 Username: apiserver.PrefixedClaimOrExpression{ 3259 Claim: "username", 3260 Prefix: pointer.String(""), 3261 }, 3262 }, 3263 }, 3264 now: func() time.Time { return now }, 3265 }, 3266 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3267 pubKeys: []*jose.JSONWebKey{ 3268 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3269 }, 3270 claims: fmt.Sprintf(`{ 3271 "iss": "https://auth.example.com", 3272 "aud": "my-client", 3273 "username": "jane", 3274 "exp": %d 3275 }`, valid.Unix()), 3276 openIDConfig: `{ 3277 "issuer": "https://auth.example.com", 3278 "jwks_uri": "{{.URL}}/.testing/keys" 3279 }`, 3280 fetchKeysFromRemote: true, 3281 want: &user.DefaultInfo{ 3282 Name: "jane", 3283 }, 3284 }, 3285 { 3286 name: "discovery url and issuer url have paths", 3287 options: Options{ 3288 JWTAuthenticator: apiserver.JWTAuthenticator{ 3289 Issuer: apiserver.Issuer{ 3290 URL: "https://auth.example.com/a/b/foo", 3291 DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration", 3292 Audiences: []string{"my-client"}, 3293 }, 3294 ClaimMappings: apiserver.ClaimMappings{ 3295 Username: apiserver.PrefixedClaimOrExpression{ 3296 Claim: "username", 3297 Prefix: pointer.String(""), 3298 }, 3299 }, 3300 }, 3301 now: func() time.Time { return now }, 3302 }, 3303 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3304 pubKeys: []*jose.JSONWebKey{ 3305 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3306 }, 3307 claims: fmt.Sprintf(`{ 3308 "iss": "https://auth.example.com/a/b/foo", 3309 "aud": "my-client", 3310 "username": "jane", 3311 "exp": %d 3312 }`, valid.Unix()), 3313 openIDConfig: `{ 3314 "issuer": "https://auth.example.com/a/b/foo", 3315 "jwks_uri": "{{.URL}}/.testing/keys" 3316 }`, 3317 fetchKeysFromRemote: true, 3318 want: &user.DefaultInfo{ 3319 Name: "jane", 3320 }, 3321 }, 3322 { 3323 name: "discovery url and issuer url have paths, issuer url has trailing slash", 3324 options: Options{ 3325 JWTAuthenticator: apiserver.JWTAuthenticator{ 3326 Issuer: apiserver.Issuer{ 3327 URL: "https://auth.example.com/a/b/foo/", 3328 DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration", 3329 Audiences: []string{"my-client"}, 3330 }, 3331 ClaimMappings: apiserver.ClaimMappings{ 3332 Username: apiserver.PrefixedClaimOrExpression{ 3333 Claim: "username", 3334 Prefix: pointer.String(""), 3335 }, 3336 }, 3337 }, 3338 now: func() time.Time { return now }, 3339 }, 3340 signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), 3341 pubKeys: []*jose.JSONWebKey{ 3342 loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), 3343 }, 3344 claims: fmt.Sprintf(`{ 3345 "iss": "https://auth.example.com/a/b/foo/", 3346 "aud": "my-client", 3347 "username": "jane", 3348 "exp": %d 3349 }`, valid.Unix()), 3350 openIDConfig: `{ 3351 "issuer": "https://auth.example.com/a/b/foo/", 3352 "jwks_uri": "{{.URL}}/.testing/keys" 3353 }`, 3354 fetchKeysFromRemote: true, 3355 want: &user.DefaultInfo{ 3356 Name: "jane", 3357 }, 3358 }, 3359 } 3360 3361 var successTestCount, failureTestCount int 3362 for _, test := range tests { 3363 t.Run(test.name, test.run) 3364 if test.wantSkip || len(test.wantInitErr) > 0 || len(test.wantHealthErrPrefix) > 0 { 3365 continue 3366 } 3367 // check metrics for success and failure 3368 if test.wantErr == "" { 3369 successTestCount++ 3370 testutil.AssertHistogramTotalCount(t, "apiserver_authentication_jwt_authenticator_latency_seconds", map[string]string{"result": "success"}, successTestCount) 3371 } else { 3372 failureTestCount++ 3373 testutil.AssertHistogramTotalCount(t, "apiserver_authentication_jwt_authenticator_latency_seconds", map[string]string{"result": "failure"}, failureTestCount) 3374 } 3375 } 3376 } 3377 3378 func TestUnmarshalClaimError(t *testing.T) { 3379 // Ensure error strings returned by unmarshaling claims don't include the claim. 3380 const token = "96bb299a-02e9-11e8-8673-54ee7553240e" // Fake token for testing. 3381 payload := fmt.Sprintf(`{ 3382 "token": "%s" 3383 }`, token) 3384 3385 var c claims 3386 if err := json.Unmarshal([]byte(payload), &c); err != nil { 3387 t.Fatal(err) 3388 } 3389 var n int 3390 err := c.unmarshalClaim("token", &n) 3391 if err == nil { 3392 t.Fatal("expected error") 3393 } 3394 3395 if strings.Contains(err.Error(), token) { 3396 t.Fatalf("unmarshal error included token") 3397 } 3398 } 3399 3400 func TestUnmarshalClaim(t *testing.T) { 3401 tests := []struct { 3402 name string 3403 claims string 3404 do func(claims) (interface{}, error) 3405 want interface{} 3406 wantErr bool 3407 }{ 3408 { 3409 name: "string claim", 3410 claims: `{"aud":"foo"}`, 3411 do: func(c claims) (interface{}, error) { 3412 var s string 3413 err := c.unmarshalClaim("aud", &s) 3414 return s, err 3415 }, 3416 want: "foo", 3417 }, 3418 { 3419 name: "mismatched types", 3420 claims: `{"aud":"foo"}`, 3421 do: func(c claims) (interface{}, error) { 3422 var n int 3423 err := c.unmarshalClaim("aud", &n) 3424 return n, err 3425 3426 }, 3427 wantErr: true, 3428 }, 3429 { 3430 name: "bool claim", 3431 claims: `{"email":"foo@coreos.com","email_verified":true}`, 3432 do: func(c claims) (interface{}, error) { 3433 var verified bool 3434 err := c.unmarshalClaim("email_verified", &verified) 3435 return verified, err 3436 }, 3437 want: true, 3438 }, 3439 { 3440 name: "strings claim", 3441 claims: `{"groups":["a","b","c"]}`, 3442 do: func(c claims) (interface{}, error) { 3443 var groups []string 3444 err := c.unmarshalClaim("groups", &groups) 3445 return groups, err 3446 }, 3447 want: []string{"a", "b", "c"}, 3448 }, 3449 } 3450 3451 for _, test := range tests { 3452 t.Run(test.name, func(t *testing.T) { 3453 var c claims 3454 if err := json.Unmarshal([]byte(test.claims), &c); err != nil { 3455 t.Fatal(err) 3456 } 3457 3458 got, err := test.do(c) 3459 if err != nil { 3460 if test.wantErr { 3461 return 3462 } 3463 t.Fatalf("unexpected error: %v", err) 3464 } 3465 if test.wantErr { 3466 t.Fatalf("expected error") 3467 } 3468 3469 if !reflect.DeepEqual(got, test.want) { 3470 t.Errorf("wanted=%#v, got=%#v", test.want, got) 3471 } 3472 }) 3473 } 3474 } 3475 3476 type errTransport string 3477 3478 func (e errTransport) RoundTrip(_ *http.Request) (*http.Response, error) { 3479 return nil, fmt.Errorf("%s", e) 3480 } 3481 3482 func testContext(t *testing.T) context.Context { 3483 ctx, cancel := context.WithCancel(context.Background()) 3484 t.Cleanup(cancel) 3485 return ctx 3486 }