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  }