k8s.io/apiserver@v0.31.1/pkg/authentication/token/cache/cached_token_authenticator_test.go (about) 1 /* 2 Copyright 2017 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 cache 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/hmac" 23 "crypto/rand" 24 "crypto/sha256" 25 "errors" 26 "fmt" 27 mathrand "math/rand" 28 "reflect" 29 "sync" 30 "sync/atomic" 31 "testing" 32 "time" 33 34 "github.com/google/go-cmp/cmp" 35 36 utilrand "k8s.io/apimachinery/pkg/util/rand" 37 "k8s.io/apimachinery/pkg/util/uuid" 38 auditinternal "k8s.io/apiserver/pkg/apis/audit" 39 "k8s.io/apiserver/pkg/audit" 40 "k8s.io/apiserver/pkg/authentication/authenticator" 41 "k8s.io/apiserver/pkg/authentication/user" 42 "k8s.io/utils/clock" 43 testingclock "k8s.io/utils/clock/testing" 44 ) 45 46 func TestCachedTokenAuthenticator(t *testing.T) { 47 var ( 48 calledWithToken []string 49 50 resultUsers map[string]user.Info 51 resultOk bool 52 resultErr error 53 ) 54 fakeAuth := authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 55 calledWithToken = append(calledWithToken, token) 56 return &authenticator.Response{User: resultUsers[token]}, resultOk, resultErr 57 }) 58 fakeClock := testingclock.NewFakeClock(time.Now()) 59 60 a := newWithClock(fakeAuth, true, time.Minute, 0, fakeClock) 61 62 calledWithToken, resultUsers, resultOk, resultErr = []string{}, nil, false, nil 63 a.AuthenticateToken(context.Background(), "bad1") 64 a.AuthenticateToken(context.Background(), "bad2") 65 a.AuthenticateToken(context.Background(), "bad3") 66 fakeClock.Step(2 * time.Microsecond) 67 a.AuthenticateToken(context.Background(), "bad1") 68 a.AuthenticateToken(context.Background(), "bad2") 69 a.AuthenticateToken(context.Background(), "bad3") 70 fakeClock.Step(2 * time.Microsecond) 71 if !reflect.DeepEqual(calledWithToken, []string{"bad1", "bad2", "bad3", "bad1", "bad2", "bad3"}) { 72 t.Errorf("Expected failing calls to not stay in the cache, got %v", calledWithToken) 73 } 74 75 // reset calls, make the backend return success for three user tokens 76 calledWithToken = []string{} 77 resultUsers, resultOk, resultErr = map[string]user.Info{}, true, nil 78 resultUsers["usertoken1"] = &user.DefaultInfo{Name: "user1"} 79 resultUsers["usertoken2"] = &user.DefaultInfo{Name: "user2"} 80 resultUsers["usertoken3"] = &user.DefaultInfo{Name: "user3"} 81 82 // populate cache 83 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken1"); err != nil || !ok || resp.User.GetName() != "user1" { 84 t.Errorf("Expected user1") 85 } 86 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken2"); err != nil || !ok || resp.User.GetName() != "user2" { 87 t.Errorf("Expected user2") 88 } 89 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken3"); err != nil || !ok || resp.User.GetName() != "user3" { 90 t.Errorf("Expected user3") 91 } 92 if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { 93 t.Errorf("Expected token calls, got %v", calledWithToken) 94 } 95 96 // reset calls, make the backend return failures 97 calledWithToken = []string{} 98 resultUsers, resultOk, resultErr = nil, false, nil 99 100 // authenticate calls still succeed and backend is not hit 101 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken1"); err != nil || !ok || resp.User.GetName() != "user1" { 102 t.Errorf("Expected user1") 103 } 104 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken2"); err != nil || !ok || resp.User.GetName() != "user2" { 105 t.Errorf("Expected user2") 106 } 107 if resp, ok, err := a.AuthenticateToken(context.Background(), "usertoken3"); err != nil || !ok || resp.User.GetName() != "user3" { 108 t.Errorf("Expected user3") 109 } 110 if !reflect.DeepEqual(calledWithToken, []string{}) { 111 t.Errorf("Expected no token calls, got %v", calledWithToken) 112 } 113 114 // skip forward in time 115 fakeClock.Step(2 * time.Minute) 116 117 // backend is consulted again and fails 118 a.AuthenticateToken(context.Background(), "usertoken1") 119 a.AuthenticateToken(context.Background(), "usertoken2") 120 a.AuthenticateToken(context.Background(), "usertoken3") 121 if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { 122 t.Errorf("Expected token calls, got %v", calledWithToken) 123 } 124 } 125 126 func TestCachedTokenAuthenticatorWithAudiences(t *testing.T) { 127 resultUsers := make(map[string]user.Info) 128 fakeAuth := authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 129 auds, _ := authenticator.AudiencesFrom(ctx) 130 return &authenticator.Response{User: resultUsers[auds[0]+token]}, true, nil 131 }) 132 fakeClock := testingclock.NewFakeClock(time.Now()) 133 134 a := newWithClock(fakeAuth, true, time.Minute, 0, fakeClock) 135 136 resultUsers["audAusertoken1"] = &user.DefaultInfo{Name: "user1"} 137 resultUsers["audBusertoken1"] = &user.DefaultInfo{Name: "user1-different"} 138 139 if u, ok, _ := a.AuthenticateToken(authenticator.WithAudiences(context.Background(), []string{"audA"}), "usertoken1"); !ok || u.User.GetName() != "user1" { 140 t.Errorf("Expected user1") 141 } 142 if u, ok, _ := a.AuthenticateToken(authenticator.WithAudiences(context.Background(), []string{"audB"}), "usertoken1"); !ok || u.User.GetName() != "user1-different" { 143 t.Errorf("Expected user1-different") 144 } 145 } 146 147 var bKey string 148 149 // use a realistic token for benchmarking 150 const jwtToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJvcGVuc2hpZnQtc2RuIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNkbi10b2tlbi1nNndtYyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJzZG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzYzM5YzNhYS1kM2Q5LTExZTktYTVkMC0wMmI3YjllODg1OWUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b3BlbnNoaWZ0LXNkbjpzZG4ifQ.PIs0rsUTekj5AX8yJeLDyW4vQB17YS4IOgO026yjEvsCY7Wv_2TD0lwyZWqyQh639q3jPh2_3LTQq2Cp0cReBP1PYOIGgprNm3C-3OFZRnkls-GH09kvPYE8J_-a1YwjxucOwytzJvEM5QTC9iXfEJNSTBfLge-HMYT1y0AGKs8DWTSC4rtd_2PedK3OYiAyDg_xHA8qNpG9pRNM8vfjV9VsmqJtlbnTVlTngqC0t5vyMaWrmLNRxN0rTbN2W9L3diXRnYqI8BUfgPQb7uhYcPuXGeypaFrN4d3yNN4NbgVxnkgdd2IXQ8elSJuQn6ynrvLgG0JPMmThOHnwvsZDeA` 151 152 func BenchmarkKeyFunc(b *testing.B) { 153 randomCacheKey := make([]byte, 32) 154 if _, err := rand.Read(randomCacheKey); err != nil { 155 b.Fatal(err) // rand should never fail 156 } 157 hashPool := &sync.Pool{ 158 New: func() interface{} { 159 return hmac.New(sha256.New, randomCacheKey) 160 }, 161 } 162 163 // use realistic audiences for benchmarking 164 auds := []string{"7daf30b7-a85c-429b-8b21-e666aecbb235", "c22aa267-bdde-4acb-8505-998be7818400", "44f9b4f3-7125-4333-b04c-1446a16c6113"} 165 166 b.Run("has audiences", func(b *testing.B) { 167 var key string 168 for n := 0; n < b.N; n++ { 169 key = keyFunc(hashPool, auds, jwtToken) 170 } 171 bKey = key 172 }) 173 174 b.Run("nil audiences", func(b *testing.B) { 175 var key string 176 for n := 0; n < b.N; n++ { 177 key = keyFunc(hashPool, nil, jwtToken) 178 } 179 bKey = key 180 }) 181 } 182 183 func TestSharedLookup(t *testing.T) { 184 var chewie = &authenticator.Response{User: &user.DefaultInfo{Name: "chewbacca"}} 185 186 t.Run("actually shared", func(t *testing.T) { 187 var lookups uint32 188 c := make(chan struct{}) 189 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 190 <-c 191 atomic.AddUint32(&lookups, 1) 192 return chewie, true, nil 193 }), true, time.Minute, 0) 194 195 var wg sync.WaitGroup 196 for i := 0; i < 10; i++ { 197 wg.Add(1) 198 go func() { 199 defer wg.Done() 200 a.AuthenticateToken(context.Background(), "") 201 }() 202 } 203 204 // no good way to make sure that all the callers are queued so we sleep. 205 time.Sleep(1 * time.Second) 206 close(c) 207 wg.Wait() 208 209 if lookups > 3 { 210 t.Fatalf("unexpected number of lookups: got=%d, wanted less than 3", lookups) 211 } 212 }) 213 214 t.Run("first caller bails, second caller gets result", func(t *testing.T) { 215 c := make(chan struct{}) 216 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 217 <-c 218 return chewie, true, nil 219 }), true, time.Minute, 0) 220 221 var wg sync.WaitGroup 222 wg.Add(2) 223 224 ctx1, cancel1 := context.WithCancel(context.Background()) 225 go func() { 226 defer wg.Done() 227 a.AuthenticateToken(ctx1, "") 228 }() 229 230 ctx2 := context.Background() 231 232 var ( 233 resp *authenticator.Response 234 ok bool 235 err error 236 ) 237 go func() { 238 defer wg.Done() 239 resp, ok, err = a.AuthenticateToken(ctx2, "") 240 }() 241 242 time.Sleep(1 * time.Second) 243 cancel1() 244 close(c) 245 wg.Wait() 246 247 if want := chewie; !cmp.Equal(resp, want) { 248 t.Errorf("Unexpected diff: %v", cmp.Diff(resp, want)) 249 } 250 if !ok { 251 t.Errorf("Expected ok response") 252 } 253 if err != nil { 254 t.Errorf("Unexpected error: %v", err) 255 } 256 }) 257 258 t.Run("lookup panics", func(t *testing.T) { 259 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 260 panic("uh oh") 261 }), true, time.Minute, 0) 262 263 _, _, err := a.AuthenticateToken(context.Background(), "") 264 if err != errAuthnCrash { 265 t.Errorf("expected error: %v", err) 266 } 267 }) 268 269 t.Run("audiences are forwarded", func(t *testing.T) { 270 ctx := authenticator.WithAudiences(context.Background(), []string{"a"}) 271 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 272 auds, _ := authenticator.AudiencesFrom(ctx) 273 if got, want := auds, []string{"a"}; cmp.Equal(got, want) { 274 t.Fatalf("unexpeced audiences: %v", cmp.Diff(got, want)) 275 } 276 return nil, false, nil 277 }), true, time.Minute, 0) 278 279 a.AuthenticateToken(ctx, "") 280 }) 281 } 282 283 func TestCachedAuditAnnotations(t *testing.T) { 284 snorlax := &authenticator.Response{User: &user.DefaultInfo{Name: "snorlax"}} 285 286 t.Run("annotations from cache", func(t *testing.T) { 287 var lookups uint32 288 c := make(chan struct{}) 289 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 290 <-c 291 atomic.AddUint32(&lookups, 1) 292 audit.AddAuditAnnotation(ctx, "snorlax", "rocks") 293 audit.AddAuditAnnotation(ctx, "pandas", "are amazing") 294 return snorlax, true, nil 295 }), false, time.Minute, 0) 296 297 allAnnotations := make(chan map[string]string, 10) 298 defer close(allAnnotations) 299 300 var wg sync.WaitGroup 301 for i := 0; i < cap(allAnnotations); i++ { 302 wg.Add(1) 303 go func() { 304 defer wg.Done() 305 306 ctx := withAudit(context.Background()) 307 _, _, _ = a.AuthenticateToken(ctx, "token") 308 309 allAnnotations <- audit.AuditEventFrom(ctx).Annotations 310 }() 311 } 312 313 // no good way to make sure that all the callers are queued so we sleep. 314 time.Sleep(1 * time.Second) 315 close(c) 316 wg.Wait() 317 318 want := map[string]string{"snorlax": "rocks", "pandas": "are amazing"} 319 for i := 0; i < cap(allAnnotations); i++ { 320 annotations := <-allAnnotations 321 if diff := cmp.Diff(want, annotations); diff != "" { 322 t.Errorf("%d: unexpected annotations (-want +got): %s", i, diff) 323 } 324 } 325 326 if queued := len(allAnnotations); queued != 0 { 327 t.Errorf("expected all annoations to be processed: %d", queued) 328 } 329 330 if lookups > 3 { 331 t.Errorf("unexpected number of lookups: got=%d, wanted less than 3", lookups) 332 } 333 }) 334 335 t.Run("annotations do not change during cache TTL", func(t *testing.T) { 336 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 337 audit.AddAuditAnnotation(ctx, "timestamp", time.Now().String()) 338 return snorlax, true, nil 339 }), false, time.Minute, 0) 340 341 allAnnotations := make([]map[string]string, 0, 10) 342 343 for i := 0; i < cap(allAnnotations); i++ { 344 ctx := withAudit(context.Background()) 345 _, _, _ = a.AuthenticateToken(ctx, "token") 346 allAnnotations = append(allAnnotations, audit.AuditEventFrom(ctx).Annotations) 347 } 348 349 if len(allAnnotations) != cap(allAnnotations) { 350 t.Errorf("failed to process all annotations") 351 } 352 353 want := allAnnotations[0] 354 if ok := len(want) == 1 && len(want["timestamp"]) > 0; !ok { 355 t.Errorf("invalid annotations: %v", want) 356 } 357 358 for i, annotations := range allAnnotations[1:] { 359 if diff := cmp.Diff(want, annotations); diff != "" { 360 t.Errorf("%d: unexpected annotations (-want +got): %s", i, diff) 361 } 362 } 363 }) 364 365 t.Run("different tokens can have different annotations", func(t *testing.T) { 366 a := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 367 audit.AddAuditAnnotation(ctx, "timestamp", time.Now().String()) 368 return snorlax, true, nil 369 }), false, time.Minute, 0) 370 371 ctx1 := withAudit(context.Background()) 372 _, _, _ = a.AuthenticateToken(ctx1, "token1") 373 annotations1 := audit.AuditEventFrom(ctx1).Annotations 374 375 // guarantee different now times 376 time.Sleep(time.Second) 377 378 ctx2 := withAudit(context.Background()) 379 _, _, _ = a.AuthenticateToken(ctx2, "token2") 380 annotations2 := audit.AuditEventFrom(ctx2).Annotations 381 382 if ok := len(annotations1) == 1 && len(annotations1["timestamp"]) > 0; !ok { 383 t.Errorf("invalid annotations 1: %v", annotations1) 384 } 385 if ok := len(annotations2) == 1 && len(annotations2["timestamp"]) > 0; !ok { 386 t.Errorf("invalid annotations 2: %v", annotations2) 387 } 388 389 if annotations1["timestamp"] == annotations2["timestamp"] { 390 t.Errorf("annotations should have different timestamp value: %v", annotations1) 391 } 392 }) 393 } 394 395 func BenchmarkCachedTokenAuthenticator(b *testing.B) { 396 tokenCount := []int{100, 500, 2500, 12500, 62500} 397 threadCount := []int{1, 16, 256} 398 for _, tokens := range tokenCount { 399 for _, threads := range threadCount { 400 newSingleBenchmark(tokens, threads).run(b) 401 } 402 } 403 } 404 405 func newSingleBenchmark(tokens, threads int) *singleBenchmark { 406 s := &singleBenchmark{ 407 threadCount: threads, 408 tokenCount: tokens, 409 } 410 s.makeTokens() 411 return s 412 } 413 414 // singleBenchmark collects all the state needed to run a benchmark. The 415 // question this benchmark answers is, "what's the average latency added by the 416 // cache for N concurrent tokens?" 417 // 418 // Given the size of the key range constructed by this test, the default go 419 // benchtime of 1 second is often inadequate to test caching and expiration 420 // behavior. A benchtime of 10 to 30 seconds is adequate to stress these 421 // code paths. 422 type singleBenchmark struct { 423 threadCount int 424 // These token.* variables are set by makeTokens() 425 tokenCount int 426 // pre-computed response for a token 427 tokenToResponse map[string]*cacheRecord 428 // include audiences for some 429 tokenToAuds map[string]authenticator.Audiences 430 // a list makes it easy to select a random one 431 tokens []string 432 } 433 434 func (s *singleBenchmark) makeTokens() { 435 s.tokenToResponse = map[string]*cacheRecord{} 436 s.tokenToAuds = map[string]authenticator.Audiences{} 437 s.tokens = []string{} 438 439 rr := mathrand.New(mathrand.NewSource(mathrand.Int63())) 440 441 for i := 0; i < s.tokenCount; i++ { 442 tok := fmt.Sprintf("%v-%v", jwtToken, i) 443 r := cacheRecord{ 444 resp: &authenticator.Response{ 445 User: &user.DefaultInfo{Name: fmt.Sprintf("holder of token %v", i)}, 446 }, 447 } 448 // make different combinations of audience, failures, denies for the tokens. 449 auds := []string{} 450 for i := 0; i < rr.Intn(4); i++ { 451 auds = append(auds, string(uuid.NewUUID())) 452 } 453 choice := rr.Float64() 454 switch { 455 case choice < 0.9: 456 r.ok = true 457 r.err = nil 458 459 // add some realistic annotations on ~20% of successful authentications 460 if f := rr.Float64(); f < 0.2 { 461 r.annotations = map[string]string{ 462 "audience.authentication.kubernetes.io": "e8357258-88b1-11ea-bc55-0242ac130003", 463 "namespace.authentication.kubernetes.io": "kube-system", 464 "float.authentication.kubernetes.io": fmt.Sprint(f), 465 } 466 } 467 case choice < 0.99: 468 r.ok = false 469 r.err = nil 470 default: 471 r.ok = false 472 r.err = errors.New("I can't think of a clever error name right now") 473 } 474 s.tokens = append(s.tokens, tok) 475 s.tokenToResponse[tok] = &r 476 if len(auds) > 0 { 477 s.tokenToAuds[tok] = auds 478 } 479 } 480 } 481 482 func (s *singleBenchmark) lookup(ctx context.Context, token string) (*authenticator.Response, bool, error) { 483 r, ok := s.tokenToResponse[token] 484 if !ok { 485 panic("test setup problem") 486 } 487 for key, val := range r.annotations { 488 audit.AddAuditAnnotation(ctx, key, val) 489 } 490 return r.resp, r.ok, r.err 491 } 492 493 func (s *singleBenchmark) doAuthForTokenN(n int, a authenticator.Token) { 494 tok := s.tokens[n] 495 auds := s.tokenToAuds[tok] 496 ctx := context.Background() 497 ctx = authenticator.WithAudiences(ctx, auds) 498 a.AuthenticateToken(ctx, tok) 499 } 500 501 func (s *singleBenchmark) run(b *testing.B) { 502 b.Run(fmt.Sprintf("tokens=%d threads=%d", s.tokenCount, s.threadCount), s.bench) 503 } 504 505 func (s *singleBenchmark) bench(b *testing.B) { 506 // Simulate slowness, qps limit, external service limitation, etc 507 const maxInFlight = 40 508 chokepoint := make(chan struct{}, maxInFlight) 509 // lookup count 510 var lookups uint64 511 512 a := newWithClock( 513 authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 514 atomic.AddUint64(&lookups, 1) 515 516 chokepoint <- struct{}{} 517 defer func() { <-chokepoint }() 518 519 time.Sleep(1 * time.Millisecond) 520 521 return s.lookup(ctx, token) 522 }), 523 true, 524 4*time.Second, 525 500*time.Millisecond, 526 clock.RealClock{}, 527 ) 528 529 b.ResetTimer() 530 b.SetParallelism(s.threadCount) 531 b.RunParallel(func(pb *testing.PB) { 532 r := mathrand.New(mathrand.NewSource(mathrand.Int63())) 533 for pb.Next() { 534 // some problems appear with random access, some appear with many 535 // requests for a single entry, so we do both. 536 s.doAuthForTokenN(r.Intn(s.tokenCount), a) 537 s.doAuthForTokenN(0, a) 538 } 539 }) 540 b.StopTimer() 541 542 b.ReportMetric(float64(lookups)/float64(b.N), "lookups/op") 543 } 544 545 // Add a test version of the audit context with a pre-populated event for easy annotation 546 // extraction. 547 func withAudit(ctx context.Context) context.Context { 548 ctx = audit.WithAuditContext(ctx) 549 ac := audit.AuditContextFrom(ctx) 550 ac.Event.Level = auditinternal.LevelMetadata 551 return ctx 552 } 553 554 func TestUnsafeConversions(t *testing.T) { 555 t.Parallel() 556 557 // needs to be large to force allocations so we pick a random value between [1024, 2048] 558 size := utilrand.IntnRange(1024, 2048+1) 559 560 t.Run("toBytes semantics", func(t *testing.T) { 561 t.Parallel() 562 563 s := utilrand.String(size) 564 b := toBytes(s) 565 if len(b) != size { 566 t.Errorf("unexpected length: %d", len(b)) 567 } 568 if cap(b) != size { 569 t.Errorf("unexpected capacity: %d", cap(b)) 570 } 571 if !bytes.Equal(b, []byte(s)) { 572 t.Errorf("unexpected equality failure: %#v", b) 573 } 574 }) 575 576 t.Run("toBytes allocations", func(t *testing.T) { 577 t.Parallel() 578 579 s := utilrand.String(size) 580 f := func() { 581 b := toBytes(s) 582 if len(b) != size { 583 t.Errorf("invalid length: %d", len(b)) 584 } 585 } 586 allocs := testing.AllocsPerRun(100, f) 587 if allocs > 0 { 588 t.Errorf("expected zero allocations, got %v", allocs) 589 } 590 }) 591 592 t.Run("toString semantics", func(t *testing.T) { 593 t.Parallel() 594 595 b := make([]byte, size) 596 if _, err := rand.Read(b); err != nil { 597 t.Fatal(err) 598 } 599 s := toString(b) 600 if len(s) != size { 601 t.Errorf("unexpected length: %d", len(s)) 602 } 603 if s != string(b) { 604 t.Errorf("unexpected equality failure: %#v", s) 605 } 606 }) 607 608 t.Run("toString allocations", func(t *testing.T) { 609 t.Parallel() 610 611 b := make([]byte, size) 612 if _, err := rand.Read(b); err != nil { 613 t.Fatal(err) 614 } 615 f := func() { 616 s := toString(b) 617 if len(s) != size { 618 t.Errorf("invalid length: %d", len(s)) 619 } 620 } 621 allocs := testing.AllocsPerRun(100, f) 622 if allocs > 0 { 623 t.Errorf("expected zero allocations, got %v", allocs) 624 } 625 }) 626 } 627 628 func TestKeyFunc(t *testing.T) { 629 t.Parallel() 630 631 hashPool := &sync.Pool{ 632 New: func() interface{} { 633 return hmac.New(sha256.New, []byte("098c9e46-b7f4-4358-bb3c-35cb7495b836")) // deterministic HMAC for testing 634 }, 635 } 636 637 // use realistic audiences 638 auds := []string{"7daf30b7-a85c-429b-8b21-e666aecbb235", "c22aa267-bdde-4acb-8505-998be7818400", "44f9b4f3-7125-4333-b04c-1446a16c6113"} 639 640 keyWithAuds := "\"\xf7\xac\xcd\x12\xf5\x83l\xa9;@\n\xa13a;\nd\x1f\xdelL\xd1\xe1!\x8a\xdahٛ\xbb\xf0" 641 642 keyWithoutAuds := "\x054a \xa5\x8e\xea\xb2?\x8c\x88\xb9,e\n5\xe7ȵ>\xfdK\x0e\x93+\x02˿&\xf98\x1e" 643 644 t.Run("has audiences", func(t *testing.T) { 645 t.Parallel() 646 647 key := keyFunc(hashPool, auds, jwtToken) 648 if key != keyWithAuds { 649 t.Errorf("unexpected equality failure: %#v", key) 650 } 651 }) 652 653 t.Run("nil audiences", func(t *testing.T) { 654 t.Parallel() 655 656 key := keyFunc(hashPool, nil, jwtToken) 657 if key != keyWithoutAuds { 658 t.Errorf("unexpected equality failure: %#v", key) 659 } 660 }) 661 }