go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/auth_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package auth 16 17 import ( 18 "context" 19 "io" 20 "math/rand" 21 "net/http" 22 "net/http/httptest" 23 "testing" 24 "time" 25 26 "golang.org/x/oauth2" 27 28 "go.chromium.org/luci/auth/internal" 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/clock/testclock" 31 "go.chromium.org/luci/common/data/rand/mathrand" 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/retry/transient" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 var ( 40 now = time.Date(2015, time.January, 1, 0, 0, 0, 0, time.UTC) 41 past = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) 42 future = now.Add(24 * time.Hour) 43 ) 44 45 func TestTransportFactory(t *testing.T) { 46 t.Parallel() 47 48 Convey("InteractiveLogin + interactive provider: invokes Login", t, func() { 49 provider := &fakeTokenProvider{ 50 interactive: true, 51 } 52 auth, _ := newAuth(InteractiveLogin, provider, nil, "") 53 54 // Returns "hooked" transport, not default. 55 // 56 // Note: we don't use ShouldNotEqual because it tries to read guts of 57 // http.DefaultTransport and it sometimes triggers race detector. 58 t, err := auth.Transport() 59 So(err, ShouldBeNil) 60 So(t != http.DefaultTransport, ShouldBeTrue) 61 62 // MintToken is called by Login. 63 So(provider.mintTokenCalled, ShouldBeTrue) 64 }) 65 66 Convey("SilentLogin + interactive provider: ErrLoginRequired", t, func() { 67 auth, _ := newAuth(SilentLogin, &fakeTokenProvider{ 68 interactive: true, 69 }, nil, "") 70 _, err := auth.Transport() 71 So(err, ShouldEqual, ErrLoginRequired) 72 }) 73 74 Convey("OptionalLogin + interactive provider: Fallback to non-auth", t, func() { 75 auth, _ := newAuth(OptionalLogin, &fakeTokenProvider{ 76 interactive: true, 77 }, nil, "") 78 t, err := auth.Transport() 79 So(err, ShouldBeNil) 80 So(t == http.DefaultTransport, ShouldBeTrue) 81 }) 82 83 Convey("Always uses authenticating transport for non-interactive provider", t, func() { 84 modes := []LoginMode{InteractiveLogin, SilentLogin, OptionalLogin} 85 for _, mode := range modes { 86 auth, _ := newAuth(mode, &fakeTokenProvider{}, nil, "") 87 So(auth.Login(), ShouldBeNil) // noop 88 t, err := auth.Transport() 89 So(err, ShouldBeNil) 90 So(t != http.DefaultTransport, ShouldBeTrue) 91 } 92 }) 93 } 94 95 func TestRefreshToken(t *testing.T) { 96 t.Parallel() 97 98 Convey("Test non-interactive auth (no cache)", t, func() { 99 tokenProvider := &fakeTokenProvider{ 100 interactive: false, 101 tokenToMint: &internal.Token{ 102 Token: oauth2.Token{AccessToken: "minted"}, 103 Email: "freshly-minted@example.com", 104 }, 105 } 106 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 107 So(auth.CheckLoginRequired(), ShouldBeNil) 108 109 // No token yet, it is is lazily loaded below. 110 tok, err := auth.currentToken() 111 So(err, ShouldBeNil) 112 So(tok, ShouldBeNil) 113 114 // The token is minted on first request. 115 oauthTok, err := auth.GetAccessToken(time.Minute) 116 So(err, ShouldBeNil) 117 So(oauthTok.AccessToken, ShouldEqual, "minted") 118 119 // And we also get an email straight from MintToken call. 120 email, err := auth.GetEmail() 121 So(err, ShouldBeNil) 122 So(email, ShouldEqual, "freshly-minted@example.com") 123 }) 124 125 Convey("Test non-interactive auth (with non-expired cache)", t, func() { 126 tokenProvider := &fakeTokenProvider{ 127 interactive: false, 128 } 129 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 130 cacheToken(auth, tokenProvider, &internal.Token{ 131 Token: oauth2.Token{ 132 AccessToken: "cached", 133 Expiry: future, 134 }, 135 Email: "cached-email@example.com", 136 }) 137 138 So(auth.CheckLoginRequired(), ShouldBeNil) 139 140 // Cached token is used. 141 oauthTok, err := auth.GetAccessToken(time.Minute) 142 So(err, ShouldBeNil) 143 So(oauthTok.AccessToken, ShouldEqual, "cached") 144 145 // Cached email is used. 146 email, err := auth.GetEmail() 147 So(err, ShouldBeNil) 148 So(email, ShouldEqual, "cached-email@example.com") 149 }) 150 151 Convey("Test non-interactive auth (with expired cache)", t, func() { 152 tokenProvider := &fakeTokenProvider{ 153 interactive: false, 154 tokenToRefresh: &internal.Token{ 155 Token: oauth2.Token{AccessToken: "refreshed"}, 156 Email: "new-email@example.com", 157 }, 158 } 159 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 160 cacheToken(auth, tokenProvider, &internal.Token{ 161 Token: oauth2.Token{ 162 AccessToken: "cached", 163 Expiry: past, 164 }, 165 Email: "cached-email@example.com", 166 }) 167 168 So(auth.CheckLoginRequired(), ShouldBeNil) 169 170 // The usage triggers refresh procedure. 171 oauthTok, err := auth.GetAccessToken(time.Minute) 172 So(err, ShouldBeNil) 173 So(oauthTok.AccessToken, ShouldEqual, "refreshed") 174 175 // Using a newly fetched email. 176 email, err := auth.GetEmail() 177 So(err, ShouldBeNil) 178 So(email, ShouldEqual, "new-email@example.com") 179 }) 180 181 Convey("Test interactive auth (no cache)", t, func() { 182 tokenProvider := &fakeTokenProvider{ 183 interactive: true, 184 tokenToMint: &internal.Token{ 185 Token: oauth2.Token{AccessToken: "minted"}, 186 Email: "freshly-minted@example.com", 187 }, 188 } 189 190 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 191 192 // No token cached. 193 tok, err := auth.currentToken() 194 So(err, ShouldBeNil) 195 So(tok, ShouldBeNil) 196 197 // Login is required, as reported by various methods. 198 So(auth.CheckLoginRequired(), ShouldEqual, ErrLoginRequired) 199 200 oauthTok, err := auth.GetAccessToken(time.Minute) 201 So(oauthTok, ShouldBeNil) 202 So(err, ShouldEqual, ErrLoginRequired) 203 204 email, err := auth.GetEmail() 205 So(email, ShouldEqual, "") 206 So(err, ShouldEqual, ErrLoginRequired) 207 208 // Do it. 209 err = auth.Login() 210 So(err, ShouldBeNil) 211 So(auth.CheckLoginRequired(), ShouldBeNil) 212 213 // Minted initial token. 214 tok, err = auth.currentToken() 215 So(err, ShouldBeNil) 216 So(tok.AccessToken, ShouldEqual, "minted") 217 218 // And it is actually used. 219 oauthTok, err = auth.GetAccessToken(time.Minute) 220 So(err, ShouldBeNil) 221 So(oauthTok.AccessToken, ShouldEqual, "minted") 222 223 // Email works too now. 224 email, err = auth.GetEmail() 225 So(err, ShouldBeNil) 226 So(email, ShouldEqual, "freshly-minted@example.com") 227 }) 228 229 Convey("Test interactive auth (with non-expired cache)", t, func() { 230 tokenProvider := &fakeTokenProvider{ 231 interactive: true, 232 } 233 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 234 cacheToken(auth, tokenProvider, &internal.Token{ 235 Token: oauth2.Token{ 236 AccessToken: "cached", 237 Expiry: future, 238 }, 239 Email: "cached-email@example.com", 240 }) 241 242 // No need to login, already have a token. 243 So(auth.CheckLoginRequired(), ShouldBeNil) 244 245 // Loaded cached token. 246 tok, err := auth.currentToken() 247 So(err, ShouldBeNil) 248 So(tok.AccessToken, ShouldEqual, "cached") 249 250 // And it is actually used. 251 oauthTok, err := auth.GetAccessToken(time.Minute) 252 So(err, ShouldBeNil) 253 So(oauthTok.AccessToken, ShouldEqual, "cached") 254 255 // Email works too now. 256 email, err := auth.GetEmail() 257 So(err, ShouldBeNil) 258 So(email, ShouldEqual, "cached-email@example.com") 259 }) 260 261 Convey("Test interactive auth (with expired cache)", t, func() { 262 tokenProvider := &fakeTokenProvider{ 263 interactive: true, 264 tokenToRefresh: &internal.Token{ 265 Token: oauth2.Token{AccessToken: "refreshed"}, 266 Email: "refreshed-email@example.com", 267 }, 268 } 269 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 270 cacheToken(auth, tokenProvider, &internal.Token{ 271 Token: oauth2.Token{ 272 AccessToken: "cached", 273 Expiry: past, 274 }, 275 Email: "cached-email@example.com", 276 }) 277 278 // No need to login, already have a token. Only its "access_token" part is 279 // expired. Refresh token part is still valid, so no login is required. 280 So(auth.CheckLoginRequired(), ShouldBeNil) 281 282 // Loaded cached token. 283 tok, err := auth.currentToken() 284 So(err, ShouldBeNil) 285 So(tok.AccessToken, ShouldEqual, "cached") 286 287 // Attempting to use it triggers a refresh. 288 oauthTok, err := auth.GetAccessToken(time.Minute) 289 So(err, ShouldBeNil) 290 So(oauthTok.AccessToken, ShouldEqual, "refreshed") 291 292 // Email is also refreshed. 293 email, err := auth.GetEmail() 294 So(err, ShouldBeNil) 295 So(email, ShouldEqual, "refreshed-email@example.com") 296 }) 297 298 Convey("Test revoked refresh_token", t, func() { 299 tokenProvider := &fakeTokenProvider{ 300 interactive: true, 301 revokedToken: true, 302 } 303 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 304 cacheToken(auth, tokenProvider, &internal.Token{ 305 Token: oauth2.Token{ 306 AccessToken: "cached", 307 Expiry: past, 308 }, 309 Email: "cached@example.com", 310 }) 311 312 // No need to login, already have a token. Only its "access_token" part is 313 // expired. Refresh token part is still presumably valid, there's no way to 314 // detect that it has been revoked without attempting to use it. 315 So(auth.CheckLoginRequired(), ShouldBeNil) 316 317 // Loaded cached token. 318 tok, err := auth.currentToken() 319 So(err, ShouldBeNil) 320 So(tok.AccessToken, ShouldEqual, "cached") 321 322 // Attempting to use it triggers a refresh that fails. 323 _, err = auth.GetAccessToken(time.Minute) 324 So(err, ShouldEqual, ErrLoginRequired) 325 326 // Same happens when trying to grab an email. 327 _, err = auth.GetEmail() 328 So(err, ShouldEqual, ErrLoginRequired) 329 }) 330 331 Convey("Test revoked credentials", t, func() { 332 tokenProvider := &fakeTokenProvider{ 333 interactive: false, 334 revokedCreds: true, 335 } 336 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 337 cacheToken(auth, tokenProvider, &internal.Token{ 338 Token: oauth2.Token{ 339 AccessToken: "cached", 340 Expiry: past, 341 }, 342 Email: "cached@example.com", 343 }) 344 345 So(auth.CheckLoginRequired(), ShouldBeNil) 346 347 // Attempting to use expired cached token triggers a refresh that fails. 348 _, err := auth.GetAccessToken(time.Minute) 349 So(err, ShouldErrLike, 350 "failed to refresh auth token: invalid or unavailable service account credentials") 351 352 // Same happens when trying to grab an email. 353 _, err = auth.GetEmail() 354 So(err, ShouldErrLike, 355 "failed to refresh auth token: invalid or unavailable service account credentials") 356 }) 357 358 Convey("Test transient errors when refreshing, success", t, func() { 359 tokenProvider := &fakeTokenProvider{ 360 interactive: false, 361 transientRefreshErrors: 5, 362 tokenToRefresh: &internal.Token{ 363 Token: oauth2.Token{AccessToken: "refreshed"}, 364 }, 365 } 366 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 367 cacheToken(auth, tokenProvider, &internal.Token{ 368 Token: oauth2.Token{ 369 AccessToken: "cached", 370 Expiry: past, 371 }, 372 }) 373 374 So(auth.CheckLoginRequired(), ShouldBeNil) 375 376 // Attempting to use expired cached token triggers a refresh that fails a 377 // bunch of times, but the succeeds. 378 tok, err := auth.GetAccessToken(time.Minute) 379 So(err, ShouldBeNil) 380 So(tok.AccessToken, ShouldEqual, "refreshed") 381 382 // All calls were actually made. 383 So(tokenProvider.transientRefreshErrors, ShouldEqual, 0) 384 }) 385 386 Convey("Test transient errors when refreshing, timeout", t, func() { 387 tokenProvider := &fakeTokenProvider{ 388 interactive: false, 389 transientRefreshErrors: 5000, // never succeeds 390 } 391 auth, ctx := newAuth(SilentLogin, tokenProvider, nil, "") 392 cacheToken(auth, tokenProvider, &internal.Token{ 393 Token: oauth2.Token{ 394 AccessToken: "cached", 395 Expiry: past, 396 }, 397 }) 398 399 So(auth.CheckLoginRequired(), ShouldBeNil) 400 401 // Attempting to use expired cached token triggers a refresh that constantly 402 // fails. Eventually we give up. 403 before := clock.Now(ctx) 404 _, err := auth.GetAccessToken(time.Minute) 405 So(err, ShouldErrLike, "transient error") 406 after := clock.Now(ctx) 407 408 // It took reasonable amount of time and number of attempts. 409 So(after.Sub(before), ShouldBeLessThan, 4*time.Minute) 410 So(5000-tokenProvider.transientRefreshErrors, ShouldEqual, 15) 411 }) 412 } 413 414 func TestActorMode(t *testing.T) { 415 t.Parallel() 416 417 Convey("Test non-interactive auth (no cache)", t, func() { 418 baseProvider := &fakeTokenProvider{ 419 interactive: false, 420 tokenToMint: &internal.Token{ 421 Token: oauth2.Token{ 422 AccessToken: "minted-base", 423 Expiry: now.Add(time.Hour), 424 }, 425 Email: "must-be-ignored@example.com", 426 }, 427 tokenToRefresh: &internal.Token{ 428 Token: oauth2.Token{ 429 AccessToken: "refreshed-base", 430 Expiry: now.Add(2 * time.Hour), 431 }, 432 Email: "must-be-ignored@example.com", 433 }, 434 } 435 iamProvider := &fakeTokenProvider{ 436 interactive: false, 437 tokenToMint: &internal.Token{ 438 Token: oauth2.Token{ 439 AccessToken: "minted-iam", 440 Expiry: now.Add(30 * time.Minute), 441 }, 442 Email: "minted-iam@example.com", 443 }, 444 tokenToRefresh: &internal.Token{ 445 Token: oauth2.Token{ 446 AccessToken: "refreshed-iam", 447 Expiry: now.Add(2 * time.Hour), 448 }, 449 Email: "refreshed-iam@example.com", 450 }, 451 } 452 auth, ctx := newAuth(SilentLogin, baseProvider, iamProvider, "as-actor") 453 So(auth.CheckLoginRequired(), ShouldBeNil) 454 455 // No token yet, it is is lazily loaded below. 456 tok, err := auth.currentToken() 457 So(err, ShouldBeNil) 458 So(tok, ShouldBeNil) 459 460 // The token is minted on the first request. It is IAM-derived token. 461 oauthTok, err := auth.GetAccessToken(time.Minute) 462 So(err, ShouldBeNil) 463 So(oauthTok.AccessToken, ShouldEqual, "minted-iam") 464 465 // The email also matches the IAM token. 466 email, err := auth.GetEmail() 467 So(err, ShouldBeNil) 468 So(email, ShouldEqual, "minted-iam@example.com") 469 470 // The correct base token was minted as well and used by IAM call. 471 So(iamProvider.baseTokenInMint.AccessToken, ShouldEqual, "minted-base") 472 iamProvider.baseTokenInMint = nil 473 474 // After 40 min the IAM-generated token expires, but base is still ok. 475 clock.Get(ctx).(testclock.TestClock).Add(40 * time.Minute) 476 477 // Getting a refreshed IAM token. 478 oauthTok, err = auth.GetAccessToken(time.Minute) 479 So(err, ShouldBeNil) 480 So(oauthTok.AccessToken, ShouldEqual, "refreshed-iam") 481 482 // The email also matches the IAM token. 483 email, err = auth.GetEmail() 484 So(err, ShouldBeNil) 485 So(email, ShouldEqual, "refreshed-iam@example.com") 486 487 // Using existing base token (still valid). 488 So(iamProvider.baseTokenInRefresh.AccessToken, ShouldEqual, "minted-base") 489 iamProvider.baseTokenInRefresh = nil 490 }) 491 } 492 493 func TestTransport(t *testing.T) { 494 t.Parallel() 495 496 Convey("Test transport works", t, func(c C) { 497 calls := 0 498 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 499 calls++ 500 switch r.URL.Path { 501 case "/1": 502 c.So(r.Header.Get("Authorization"), ShouldEqual, "Bearer minted") 503 case "/2": 504 c.So(r.Header.Get("Authorization"), ShouldEqual, "Bearer minted") 505 case "/3": 506 c.So(r.Header.Get("Authorization"), ShouldEqual, "Bearer refreshed") 507 default: 508 c.So(r.URL.Path, ShouldBeBlank) // just fail in some helpful way 509 } 510 w.WriteHeader(200) 511 })) 512 defer ts.Close() 513 514 tokenProvider := &fakeTokenProvider{ 515 interactive: false, 516 tokenToMint: &internal.Token{ 517 Token: oauth2.Token{AccessToken: "minted", Expiry: now.Add(time.Hour)}, 518 }, 519 tokenToRefresh: &internal.Token{ 520 Token: oauth2.Token{AccessToken: "refreshed", Expiry: now.Add(2 * time.Hour)}, 521 }, 522 } 523 524 auth, ctx := newAuth(SilentLogin, tokenProvider, nil, "") 525 client, err := auth.Client() 526 So(err, ShouldBeNil) 527 So(client, ShouldNotBeNil) 528 529 // Initial call will mint new token. 530 resp, err := client.Get(ts.URL + "/1") 531 So(err, ShouldBeNil) 532 io.ReadAll(resp.Body) 533 defer resp.Body.Close() 534 535 // Minted token is now cached. 536 tok, err := auth.currentToken() 537 So(err, ShouldBeNil) 538 So(tok.AccessToken, ShouldEqual, "minted") 539 540 cacheKey, _ := tokenProvider.CacheKey(ctx) 541 cached, err := auth.opts.testingCache.GetToken(cacheKey) 542 So(err, ShouldBeNil) 543 So(cached.AccessToken, ShouldEqual, "minted") 544 545 // 40 minutes later it is still OK to use. 546 clock.Get(ctx).(testclock.TestClock).Add(40 * time.Minute) 547 resp, err = client.Get(ts.URL + "/2") 548 So(err, ShouldBeNil) 549 io.ReadAll(resp.Body) 550 defer resp.Body.Close() 551 552 // 30 min later (70 min since the start) it is expired and refreshed. 553 clock.Get(ctx).(testclock.TestClock).Add(30 * time.Minute) 554 resp, err = client.Get(ts.URL + "/3") 555 So(err, ShouldBeNil) 556 io.ReadAll(resp.Body) 557 defer resp.Body.Close() 558 559 tok, err = auth.currentToken() 560 So(err, ShouldBeNil) 561 So(tok.AccessToken, ShouldEqual, "refreshed") 562 563 // All calls are actually made. 564 So(calls, ShouldEqual, 3) 565 }) 566 } 567 568 func TestOptionalLogin(t *testing.T) { 569 t.Parallel() 570 571 Convey("Test optional login works", t, func(c C) { 572 // This test simulates following scenario for OptionalLogin mode: 573 // 1. There's existing cached access token. 574 // 2. At some point it expires. 575 // 3. Refresh fails with ErrBadRefreshToken (refresh token is revoked). 576 // 4. Authenticator switches to anonymous calls. 577 calls := 0 578 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 579 calls++ 580 switch r.URL.Path { 581 case "/1": 582 c.So(r.Header.Get("Authorization"), ShouldEqual, "Bearer cached") 583 case "/2": 584 c.So(r.Header.Get("Authorization"), ShouldEqual, "") 585 default: 586 c.So(r.URL.Path, ShouldBeBlank) // just fail in some helpful way 587 } 588 w.WriteHeader(200) 589 })) 590 defer ts.Close() 591 592 tokenProvider := &fakeTokenProvider{ 593 interactive: true, 594 revokedToken: true, 595 } 596 auth, ctx := newAuth(OptionalLogin, tokenProvider, nil, "") 597 cacheToken(auth, tokenProvider, &internal.Token{ 598 Token: oauth2.Token{ 599 AccessToken: "cached", 600 Expiry: now.Add(time.Hour), 601 }, 602 }) 603 604 client, err := auth.Client() 605 So(err, ShouldBeNil) 606 So(client, ShouldNotBeNil) 607 608 // Initial call uses existing cached token. 609 resp, err := client.Get(ts.URL + "/1") 610 So(err, ShouldBeNil) 611 io.ReadAll(resp.Body) 612 defer resp.Body.Close() 613 614 // It expires at ~60 minutes, refresh fails, authenticator switches to 615 // anonymous access. 616 clock.Get(ctx).(testclock.TestClock).Add(65 * time.Minute) 617 resp, err = client.Get(ts.URL + "/2") 618 So(err, ShouldBeNil) 619 io.ReadAll(resp.Body) 620 defer resp.Body.Close() 621 622 // Bad token is removed from the cache. 623 tok, err := auth.currentToken() 624 So(err, ShouldBeNil) 625 So(tok, ShouldBeNil) 626 cacheKey, _ := tokenProvider.CacheKey(ctx) 627 cached, err := auth.opts.testingCache.GetToken(cacheKey) 628 So(cached, ShouldBeNil) 629 So(err, ShouldBeNil) 630 631 // All calls are actually made. 632 So(calls, ShouldEqual, 2) 633 }) 634 } 635 636 func TestGetEmail(t *testing.T) { 637 t.Parallel() 638 639 Convey("Test non-interactive auth (no cache)", t, func() { 640 tokenProvider := &fakeTokenProvider{ 641 interactive: false, 642 knownEmail: "known-email@example.com", 643 tokenToMint: &internal.Token{ 644 Token: oauth2.Token{AccessToken: "must-not-be-called"}, 645 Email: "must-not-be-called@example.com", 646 }, 647 } 648 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 649 650 // No cached token. 651 tok, err := auth.currentToken() 652 So(err, ShouldBeNil) 653 So(tok, ShouldBeNil) 654 655 // We get the email directly from the provider. 656 email, err := auth.GetEmail() 657 So(err, ShouldBeNil) 658 So(email, ShouldEqual, "known-email@example.com") 659 660 // MintToken was NOT called. 661 So(tokenProvider.mintTokenCalled, ShouldBeFalse) 662 }) 663 664 Convey("Non-expired cache without email is upgraded", t, func() { 665 tokenProvider := &fakeTokenProvider{ 666 interactive: true, 667 } 668 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 669 cacheToken(auth, tokenProvider, &internal.Token{ 670 Token: oauth2.Token{ 671 AccessToken: "cached", 672 Expiry: future, 673 }, 674 Email: "", // "old style" cache without an email 675 }) 676 677 // No need to login, already have a token. 678 So(auth.CheckLoginRequired(), ShouldBeNil) 679 680 // GetAccessToken returns the cached token. 681 oauthTok, err := auth.GetAccessToken(time.Minute) 682 So(err, ShouldBeNil) 683 So(oauthTok.AccessToken, ShouldEqual, "cached") 684 685 // But getting an email triggers a refresh, since the cached token doesn't 686 // have an email. 687 email, err := auth.GetEmail() 688 So(err, ShouldBeNil) 689 So(email, ShouldEqual, "some-email-refreshtoken@example.com") 690 691 // GetAccessToken picks up the refreshed token too. 692 oauthTok, err = auth.GetAccessToken(time.Minute) 693 So(err, ShouldBeNil) 694 So(oauthTok.AccessToken, ShouldEqual, "some refreshed access token") 695 }) 696 697 Convey("No email triggers ErrNoEmail", t, func() { 698 tokenProvider := &fakeTokenProvider{ 699 interactive: false, 700 tokenToMint: &internal.Token{ 701 Token: oauth2.Token{AccessToken: "minted"}, 702 Email: internal.NoEmail, 703 }, 704 } 705 auth, _ := newAuth(SilentLogin, tokenProvider, nil, "") 706 So(auth.CheckLoginRequired(), ShouldBeNil) 707 708 // The token is minted on first request. 709 oauthTok, err := auth.GetAccessToken(time.Minute) 710 So(err, ShouldBeNil) 711 So(oauthTok.AccessToken, ShouldEqual, "minted") 712 713 // But getting an email fails with ErrNoEmail. 714 email, err := auth.GetEmail() 715 So(err, ShouldEqual, ErrNoEmail) 716 So(email, ShouldEqual, "") 717 }) 718 } 719 720 func TestNormalizeScopes(t *testing.T) { 721 t.Parallel() 722 723 checkExactSameSlice := func(a, b []string) { 724 So(a, ShouldResemble, b) 725 So(&a[0], ShouldEqual, &b[0]) 726 } 727 728 Convey("Works", t, func() { 729 So(normalizeScopes(nil), ShouldBeNil) 730 731 // Doesn't copy already normalized slices. 732 slice := []string{"a"} 733 checkExactSameSlice(slice, normalizeScopes(slice)) 734 slice = []string{"a", "b"} 735 checkExactSameSlice(slice, normalizeScopes(slice)) 736 slice = []string{"a", "b", "c"} 737 checkExactSameSlice(slice, normalizeScopes(slice)) 738 739 // Removes dups and sorts. 740 So(normalizeScopes([]string{"b", "a"}), ShouldResemble, []string{"a", "b"}) 741 So(normalizeScopes([]string{"a", "a"}), ShouldResemble, []string{"a"}) 742 So(normalizeScopes([]string{"a", "b", "a"}), ShouldResemble, []string{"a", "b"}) 743 }) 744 } 745 746 func newAuth(loginMode LoginMode, base, iam internal.TokenProvider, actAs string) (*Authenticator, context.Context) { 747 // Use auto-advancing fake time. 748 ctx := mathrand.Set(context.Background(), rand.New(rand.NewSource(123))) 749 ctx, tc := testclock.UseTime(ctx, now) 750 tc.SetTimerCallback(func(d time.Duration, t clock.Timer) { 751 tc.Add(d) 752 }) 753 a := NewAuthenticator(ctx, loginMode, Options{ 754 ActAsServiceAccount: actAs, 755 testingCache: &internal.MemoryTokenCache{}, 756 testingBaseTokenProvider: base, 757 testingIAMTokenProvider: iam, 758 }) 759 return a, ctx 760 } 761 762 func cacheToken(a *Authenticator, p internal.TokenProvider, tok *internal.Token) { 763 cacheKey, err := p.CacheKey(a.ctx) 764 if err != nil { 765 panic(err) 766 } 767 err = a.opts.testingCache.PutToken(cacheKey, tok) 768 if err != nil { 769 panic(err) 770 } 771 } 772 773 //////////////////////////////////////////////////////////////////////////////// 774 775 type fakeTokenProvider struct { 776 interactive bool 777 revokedCreds bool 778 revokedToken bool 779 transientRefreshErrors int 780 tokenToMint *internal.Token 781 tokenToRefresh *internal.Token 782 783 mintTokenCalled bool 784 refreshTokenCalled bool 785 useIDTokens bool 786 787 baseTokenInMint *internal.Token 788 baseTokenInRefresh *internal.Token 789 790 knownEmail string 791 } 792 793 func (p *fakeTokenProvider) RequiresInteraction() bool { 794 return p.interactive 795 } 796 797 func (p *fakeTokenProvider) Lightweight() bool { 798 return true 799 } 800 801 func (p *fakeTokenProvider) Email() string { 802 return p.knownEmail 803 } 804 805 func (p *fakeTokenProvider) CacheKey(ctx context.Context) (*internal.CacheKey, error) { 806 return &internal.CacheKey{Key: "fake"}, nil 807 } 808 809 func (p *fakeTokenProvider) MintToken(ctx context.Context, base *internal.Token) (*internal.Token, error) { 810 p.mintTokenCalled = true 811 p.baseTokenInMint = base 812 if p.revokedCreds { 813 return nil, internal.ErrBadCredentials 814 } 815 if p.tokenToMint != nil { 816 return p.tokenToMint, nil 817 } 818 idTok := internal.NoIDToken 819 accessTok := internal.NoAccessToken 820 if p.useIDTokens { 821 idTok = "some minted ID token" 822 } else { 823 accessTok = "some minted access token" 824 } 825 return &internal.Token{ 826 Token: oauth2.Token{AccessToken: accessTok}, 827 IDToken: idTok, 828 Email: "some-email-minttoken@example.com", 829 }, nil 830 } 831 832 func (p *fakeTokenProvider) RefreshToken(ctx context.Context, prev, base *internal.Token) (*internal.Token, error) { 833 p.refreshTokenCalled = true 834 p.baseTokenInRefresh = base 835 if p.transientRefreshErrors != 0 { 836 p.transientRefreshErrors-- 837 return nil, errors.New("transient error", transient.Tag) 838 } 839 if p.revokedCreds { 840 return nil, internal.ErrBadCredentials 841 } 842 if p.revokedToken { 843 return nil, internal.ErrBadRefreshToken 844 } 845 if p.tokenToRefresh != nil { 846 return p.tokenToRefresh, nil 847 } 848 idTok := internal.NoIDToken 849 accessTok := internal.NoAccessToken 850 if p.useIDTokens { 851 idTok = "some refreshed ID token" 852 } else { 853 accessTok = "some refreshed access token" 854 } 855 return &internal.Token{ 856 Token: oauth2.Token{AccessToken: accessTok}, 857 IDToken: idTok, 858 Email: "some-email-refreshtoken@example.com", 859 }, nil 860 }