/
auth.go
1372 lines (1243 loc) · 49 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package auth implements a wrapper around golang.org/x/oauth2.
//
// Its main improvement is the on-disk cache for OAuth tokens, which is
// especially important for 3-legged interactive OAuth flows: its usage
// eliminates annoying login prompts each time a program is used (because the
// refresh token can now be reused). The cache also allows to reduce unnecessary
// token refresh calls when sharing a service account between processes.
//
// The package also implements some best practices regarding interactive login
// flows in CLI programs. It makes it easy to implement a login process as
// a separate interactive step that happens before the main program loop.
//
// The antipattern it tries to prevent is "launch an interactive login flow
// whenever program hits 'Not Authorized' response from the server". This
// usually results in a very confusing behavior, when login prompts pop up
// unexpectedly at random time, random places and from multiple goroutines at
// once, unexpectedly consuming unintended stdin input.
package auth
import (
"context"
"fmt"
"net/http"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"google.golang.org/grpc/credentials"
"github.com/TriggerMail/luci-go/auth/internal"
"github.com/TriggerMail/luci-go/common/clock"
"github.com/TriggerMail/luci-go/common/errors"
"github.com/TriggerMail/luci-go/common/gcloud/iam"
"github.com/TriggerMail/luci-go/common/logging"
"github.com/TriggerMail/luci-go/common/retry"
"github.com/TriggerMail/luci-go/common/retry/transient"
"github.com/TriggerMail/luci-go/lucictx"
)
var (
// ErrLoginRequired is returned by Transport() in case long term credentials
// are not cached and the user must go through interactive login.
ErrLoginRequired = errors.New("interactive login is required")
// ErrInsufficientAccess is returned by Login() or Transport() if access_token
// can't be minted for given OAuth scopes. For example if GCE instance wasn't
// granted access to requested scopes when it was created.
ErrInsufficientAccess = internal.ErrInsufficientAccess
// ErrBadCredentials is returned by authenticating RoundTripper if service
// account key used to generate access tokens is revoked, malformed or can not
// be read from disk.
ErrBadCredentials = internal.ErrBadCredentials
// ErrNoEmail is returned by GetEmail() if the cached credentials are not
// associated with some particular email. This may happen, for example, when
// using a refresh token that doesn't have 'userinfo.email' scope.
ErrNoEmail = errors.New("the token is not associated with an email")
)
// Known Google API OAuth scopes.
const (
OAuthScopeEmail = "https://www.googleapis.com/auth/userinfo.email"
)
const (
// GCEServiceAccount is special value that can be passed instead of path to
// a service account credentials file to indicate that GCE VM credentials should
// be used instead of a real credentials file.
GCEServiceAccount = ":gce"
)
// Method defines a method to use to obtain OAuth access token.
type Method string
// Supported authentication methods.
const (
// AutoSelectMethod can be used to allow the library to pick a method most
// appropriate for given set of options and the current execution environment.
//
// For example, passing ServiceAccountJSONPath or ServiceAcountJSON makes
// Authenticator to pick ServiceAccountMethod.
//
// See SelectBestMethod function for details.
AutoSelectMethod Method = ""
// UserCredentialsMethod is used for interactive OAuth 3-legged login flow.
//
// Using this method requires specifying an OAuth client by passing ClientID
// and ClientSecret in Options when calling NewAuthenticator.
//
// Additionally, SilentLogin and OptionalLogin (i.e. non-interactive) login
// modes rely on a presence of a refresh token in the token cache, thus using
// these modes with UserCredentialsMethod also requires configured token
// cache (see SecretsDir field of Options).
UserCredentialsMethod Method = "UserCredentialsMethod"
// ServiceAccountMethod is used to authenticate as a service account using
// a private key.
//
// Callers of NewAuthenticator must pass either a path to a JSON file with
// service account key (as produced by Google Cloud Console) or a body of this
// JSON file. See ServiceAccountJSONPath and ServiceAccountJSON fields in
// Options.
//
// Using ServiceAccountJSONPath has an advantage: Authenticator always loads
// the private key from the file before refreshing the token, it allows to
// replace the key while the process is running.
ServiceAccountMethod Method = "ServiceAccountMethod"
// GCEMetadataMethod is used on Compute Engine to use tokens provided by
// Metadata server. See https://cloud.google.com/compute/docs/authentication
GCEMetadataMethod Method = "GCEMetadataMethod"
// LUCIContextMethod is used by LUCI-aware applications to fetch tokens though
// a local auth server (discoverable via "local_auth" key in LUCI_CONTEXT).
//
// This method is similar in spirit to GCEMetadataMethod: it uses some local
// HTTP server as a provider of OAuth access tokens, which gives an ambient
// authentication context to apps that use it.
//
// There are some big differences:
// 1. LUCIContextMethod supports minting tokens for multiple different set
// of scopes, unlike GCE metadata server that always gives a token with
// preconfigured scopes (set when the GCE instance was created).
// 2. LUCIContextMethod is not GCE-specific. It doesn't use magic link-local
// IP address. It can run on any machine.
// 3. The access to the local auth server is controlled by file system
// permissions of LUCI_CONTEXT file (there's a secret in this file).
// 4. There can be many local auth servers running at once (on different
// ports). Useful for bringing up sub-contexts, in particular in
// combination with ActAsServiceAcccount ("sudo" mode) or for tests.
//
// See auth/integration/localauth package for the implementation of the server
// side of the protocol.
LUCIContextMethod Method = "LUCIContextMethod"
)
// LoginMode is used as enum in NewAuthenticator function.
type LoginMode string
const (
// InteractiveLogin indicates to Authenticator that it is okay to run an
// interactive login flow (via Login()) in Transport(), Client() or other
// factories if there's no cached token.
//
// This is typically used with UserCredentialsMethod to generate an OAuth
// refresh token and put it in the token cache at the start of the program,
// when grabbing a transport.
//
// Has no effect when used with service account credentials.
InteractiveLogin LoginMode = "InteractiveLogin"
// SilentLogin indicates to Authenticator that it must return a transport that
// implements authentication, but it is NOT OK to run interactive login flow
// to make it.
//
// Transport() and other factories will fail with ErrLoginRequired error if
// there's no cached token or one can't be generated on the fly in
// non-interactive mode. This may happen when using UserCredentialsMethod.
//
// It is always OK to use SilentLogin mode with service accounts credentials
// (ServiceAccountMethod mode), since no user interaction is necessary to
// generate an access token in this case.
SilentLogin LoginMode = "SilentLogin"
// OptionalLogin indicates to Authenticator that it should return a transport
// that implements authentication, but it is OK to return non-authenticating
// transport if there are no valid cached credentials.
//
// An interactive login flow will never be invoked. An unauthenticated client
// will be returned if no credentials are present.
//
// Can be used when making calls to backends that allow anonymous access. This
// is especially useful with UserCredentialsMethod: a user may start using
// the service right away (in anonymous mode), and later login (using Login()
// method or any other way of initializing credentials cache) to get more
// permissions.
//
// When used with ServiceAccountMethod it is identical to SilentLogin, since
// it makes no sense to ignore invalid service account credentials when the
// caller is explicitly asking the authenticator to use them.
//
// Has the original meaning when used with GCEMetadataMethod: it instructs to
// skip authentication if the token returned by GCE metadata service doesn't
// have all requested scopes.
OptionalLogin LoginMode = "OptionalLogin"
)
// minAcceptedLifetime is minimal lifetime of a token returned by the token
// source or put into authentication headers.
//
// If token is expected to live less than this duration, it will be refreshed.
const minAcceptedLifetime = 2 * time.Minute
// Options are used by NewAuthenticator call.
type Options struct {
// Transport is underlying round tripper to use for requests.
//
// Default: http.DefaultTransport.
Transport http.RoundTripper
// Method defines how to grab OAuth2 tokens.
//
// Default: AutoSelectMethod.
Method Method
// Scopes is a list of OAuth scopes to request.
//
// Default: [OAuthScopeEmail].
Scopes []string
// ActAsServiceAccount is used to act as a specified service account email.
//
// This uses signBlob Cloud IAM API and "iam.serviceAccountActor" role.
//
// When this option is set, there are two identities involved:
// 1. A service account identity directly specified by `ActAsServiceAccount`.
// 2. An identity conveyed by the authenticator options (via cached refresh
// token, or via `ServiceAccountJSON`, or other similar ways), i.e. the
// identity asserted by the authenticator in case `ActAsServiceAccount` is
// not set. This identity must have "iam.serviceAccountActor" role in
// the `ActAsServiceAccount` IAM resource. It is referred to below as
// Actor identity.
//
// The resulting authenticator will produce access tokens for service account
// `ActAsServiceAccount`, using Actor identity to generate them via Cloud IAM
// API.
//
// `Scopes` parameter specifies what OAuth scopes to request for access tokens
// belonging to `ActAsServiceAccount`.
//
// The Actor credentials will be internally used to generate access token with
// IAM scope ("https://www.googleapis.com/auth/iam"). It means Login() action
// sets up a refresh token with IAM scope (not `Scopes`), and the user will
// be presented with a consent screen for IAM scope.
//
// More info at https://cloud.google.com/iam/docs/service-accounts
//
// Default: none.
ActAsServiceAccount string
// ClientID is OAuth client ID to use with UserCredentialsMethod.
//
// See https://developers.google.com/identity/protocols/OAuth2InstalledApp
// (in particular everything related to "Desktop apps").
//
// Together with Scopes forms a cache key in the token cache, which in
// practical terms means there can be only one concurrently "logged in" user
// per [ClientID, Scopes] combination. So if multiple binaries use exact same
// ClientID and Scopes, they'll share credentials cache (a login in one app
// makes the user logged in in the other app too).
//
// If you don't want to share login information between tools, use separate
// ClientID or SecretsDir values.
//
// If not set, UserCredentialsMethod auth method will not work.
//
// Default: none.
ClientID string
// ClientSecret is OAuth client secret to use with UserCredentialsMethod.
//
// Default: none.
ClientSecret string
// ServiceAccountJSONPath is a path to a JSON blob with a private key to use.
//
// Can also be set to GCEServiceAccount (':gce') to indicate that the GCE VM
// service account should be used instead. Useful in CLI interfaces. This
// works only if Method is set to AutoSelectMethod (which is the default for
// most CLI apps). If GCEServiceAccount is used on non-GCE machine,
// authenticator methods return ErrBadCredentials.
//
// Used only with ServiceAccountMethod.
ServiceAccountJSONPath string
// ServiceAccountJSON is a body of JSON key file to use.
//
// Overrides ServiceAccountJSONPath if given.
ServiceAccountJSON []byte
// GCEAccountName is an account name to query to fetch token for from metadata
// server when GCEMetadataMethod is used.
//
// If given account wasn't granted required set of scopes during instance
// creation time, Transport() call fails with ErrInsufficientAccess.
//
// Default: "default" account.
GCEAccountName string
// GCEAllowAsDefault indicates whether it is OK to pick GCE authentication
// method as default if no other methods apply.
//
// Effective only when running on GCE and Method is set to AutoSelectMethod.
//
// In theory using GCE metadata server for authentication when it is
// available looks attractive. In practice, especially if running in a
// heterogeneous fleet with a mix of GCE and non-GCE machines, automatically
// enabling GCE-based authentication is very surprising when it happens.
//
// Default: false (don't "sniff" GCE environment).
GCEAllowAsDefault bool
// SecretsDir can be used to set the path to a directory where tokens
// are cached.
//
// If not set, tokens will be cached only in the process memory. For refresh
// tokens it means the user would have to go through the login process each
// time process is started. For service account tokens it means there'll be
// HTTP round trip to OAuth backend to generate access token each time the
// process is started.
SecretsDir string
// DisableMonitoring can be used to disable the monitoring instrumentation.
//
// The transport produced by this authenticator sends tsmon metrics IFF:
// 1. DisableMonitoring is false (default).
// 2. The context passed to 'NewAuthenticator' has monitoring initialized.
DisableMonitoring bool
// MonitorAs is used for 'client' field of monitoring metrics.
//
// The default is 'luci-go'.
MonitorAs string
// testingBaseTokenProvider is used in unit tests.
testingBaseTokenProvider internal.TokenProvider
// testingIAMTokenProvider is used in unit tests.
testingIAMTokenProvider internal.TokenProvider
}
// SelectBestMethod returns a most appropriate authentication method for the
// given set of options and the current execution environment.
//
// Invoked by Authenticator if AutoSelectMethod is passed as Method in Options.
// It picks the first applicable method in this order:
// * ServiceAccountMethod (if the service account private key is configured).
// * LUCIContextMethod (if running inside LUCI_CONTEXT with an auth server).
// * GCEMetadataMethod (if running on GCE and GCEAllowAsDefault is true).
// * UserCredentialsMethod (if no other method applies).
//
// Beware: it may do relatively heavy calls on first usage (to detect GCE
// environment). Fast after that.
func SelectBestMethod(ctx context.Context, opts Options) Method {
// Asked to use JSON private key.
if opts.ServiceAccountJSONPath != "" || len(opts.ServiceAccountJSON) != 0 {
if opts.ServiceAccountJSONPath == GCEServiceAccount {
return GCEMetadataMethod
}
return ServiceAccountMethod
}
// Have a local auth server and an account we are allowed to pick by default.
// If no default account is given, don't automatically pick up this method.
if la := lucictx.GetLocalAuth(ctx); la != nil && la.DefaultAccountID != "" {
return LUCIContextMethod
}
// Running on GCE and callers are fine with automagically picking up GCE
// metadata server.
if opts.GCEAllowAsDefault && metadata.OnGCE() {
return GCEMetadataMethod
}
return UserCredentialsMethod
}
// AllowsArbitraryScopes returns true if given authenticator options allow
// generating tokens for arbitrary set of scopes.
//
// For example, using a private key to sign assertions allows to mint tokens
// for any set of scopes (since there's no restriction on what scopes we can
// put into JWT to be signed).
//
// On other hand, using e.g GCE metadata server restricts us to use only scopes
// assigned to GCE instance when it was created.
func AllowsArbitraryScopes(ctx context.Context, opts Options) bool {
if opts.Method == AutoSelectMethod {
opts.Method = SelectBestMethod(ctx, opts)
}
switch {
case opts.Method == ServiceAccountMethod:
// A private key can be used to generate tokens with any combination of
// scopes.
return true
case opts.Method == LUCIContextMethod:
// We can ask the local auth server for any combination of scopes.
return true
case opts.ActAsServiceAccount != "":
// When using IAM-derived tokens authenticator relies on singBytes IAM RPC.
// It is similar to having a private key, and also can be used to generate
// tokens with any combination of scopes
return true
}
return false
}
// Authenticator is a factory for http.RoundTripper objects that know how to use
// cached OAuth credentials and how to send monitoring metrics (if tsmon package
// was imported).
//
// Authenticator also knows how to run interactive login flow, if required.
type Authenticator struct {
// Immutable members.
loginMode LoginMode
opts *Options
transport http.RoundTripper
ctx context.Context
testingCache internal.TokenCache // set in unit tests
// Mutable members.
lock sync.RWMutex
err error
// baseToken is a token (and its provider and cache) whose possession is
// sufficient to get the final access token used for authentication of user
// calls (see 'authToken' below).
//
// Methods like 'CheckLoginRequired' check that the base token exists in the
// cache or can be generated on the fly.
//
// In actor mode, the base token is always an IAM-scoped token that is used
// to call signBlob API to generate an auth token. Base token is also always
// using whatever auth method was specified by Options.Method.
//
// In non-actor mode, baseToken coincides with authToken: both point to exact
// same struct.
baseToken *tokenWithProvider
// authToken is a token (and its provider and cache) that is actually used for
// authentication of user calls.
//
// It is a token returned by 'GetAccessToken'. It is always scoped to 'Scopes'
// list, as passed to NewAuthenticator via Options.
//
// In actor mode, it is derived from the base token by using SignBlob IAM API.
// This process is non-interactive and thus can always be performed as long
// as we have the base token.
//
// In non-actor mode it is the main token generated by the authenticator. In
// this case it coincides with baseToken: both point to exact same object.
authToken *tokenWithProvider
}
// NewAuthenticator returns a new instance of Authenticator given its options.
//
// The authenticator is essentially a factory for http.RoundTripper that knows
// how to use OAuth2 tokens. It is bound to the given context: uses its logger,
// clock, transport and deadline.
func NewAuthenticator(ctx context.Context, loginMode LoginMode, opts Options) *Authenticator {
// Add default scope, sort scopes.
if len(opts.Scopes) == 0 {
opts.Scopes = []string{OAuthScopeEmail}
} else {
opts.Scopes = append([]string(nil), opts.Scopes...) // copy
sort.Strings(opts.Scopes)
}
// Fill in blanks with default values.
if opts.GCEAccountName == "" {
opts.GCEAccountName = "default"
}
if opts.Transport == nil {
opts.Transport = http.DefaultTransport
}
// TODO(vadimsh): Check SecretsDir permissions. It should be 0700.
if opts.SecretsDir != "" && !filepath.IsAbs(opts.SecretsDir) {
var err error
opts.SecretsDir, err = filepath.Abs(opts.SecretsDir)
if err != nil {
panic(fmt.Errorf("failed to get abs path to token cache dir: %s", err))
}
}
// See ensureInitialized for the rest of the initialization.
auth := &Authenticator{
ctx: ctx,
loginMode: loginMode,
opts: &opts,
}
auth.transport = NewModifyingTransport(opts.Transport, auth.authTokenInjector)
// Include the token refresh time into the monitored request time.
if globalInstrumentTransport != nil && !opts.DisableMonitoring {
monitorAs := opts.MonitorAs
if monitorAs == "" {
monitorAs = "luci-go"
}
instrumented := globalInstrumentTransport(ctx, auth.transport, monitorAs)
if instrumented != auth.transport {
logging.Debugf(ctx, "Enabling monitoring instrumentation (client == %q)", monitorAs)
auth.transport = instrumented
}
}
return auth
}
// Transport optionally performs a login and returns http.RoundTripper.
//
// It is a high level wrapper around CheckLoginRequired() and Login() calls. See
// documentation for LoginMode for more details.
func (a *Authenticator) Transport() (http.RoundTripper, error) {
switch useAuth, err := a.doLoginIfRequired(false); {
case err != nil:
return nil, err
case useAuth:
return a.transport, nil // token-injecting transport
default:
return a.opts.Transport, nil // original non-authenticating transport
}
}
// Client optionally performs a login and returns http.Client.
//
// It uses transport returned by Transport(). See documentation for LoginMode
// for more details.
func (a *Authenticator) Client() (*http.Client, error) {
transport, err := a.Transport()
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
}
// TokenSource optionally performs a login and returns oauth2.TokenSource.
//
// Can be used for interoperability with libraries that use golang.org/x/oauth2.
//
// It doesn't support 'OptionalLogin' mode, since oauth2.TokenSource must return
// some token. Otherwise its logic is similar to Transport(). In particular it
// may return ErrLoginRequired if interactive login is required, but the
// authenticator is in the silent mode. See LoginMode enum for more details.
func (a *Authenticator) TokenSource() (oauth2.TokenSource, error) {
if _, err := a.doLoginIfRequired(true); err != nil {
return nil, err
}
return tokenSource{a}, nil
}
// PerRPCCredentials optionally performs a login and returns PerRPCCredentials.
//
// It can be used to authenticate outbound gPRC RPC's.
//
// Has same logic as Transport(), in particular supports OptionalLogin mode.
// See Transport() for more details.
func (a *Authenticator) PerRPCCredentials() (credentials.PerRPCCredentials, error) {
switch useAuth, err := a.doLoginIfRequired(false); {
case err != nil:
return nil, err
case useAuth:
return perRPCCreds{a}, nil // token-injecting PerRPCCredentials
default:
return perRPCCreds{}, nil // noop PerRPCCredentials
}
}
// GetAccessToken returns a valid access token with specified minimum lifetime.
//
// Does not interact with the user. May return ErrLoginRequired.
func (a *Authenticator) GetAccessToken(lifetime time.Duration) (*oauth2.Token, error) {
tok, err := a.currentToken()
if err != nil {
return nil, err
}
// Impose some arbitrary limit, since <= 0 lifetime won't work.
if lifetime < time.Second {
lifetime = time.Second
}
if tok == nil || internal.TokenExpiresInRnd(a.ctx, tok, lifetime) {
// Give 5 sec extra to make sure callers definitely receive a token that
// has at least 'lifetime' seconds of life left. Without this margin, we
// can get into an unlucky situation where the token is valid here, but
// no longer valid (has fewer than 'lifetime' life left) up the stack, due
// to the passage of time.
var err error
tok, err = a.refreshToken(tok, lifetime+5*time.Second)
if err != nil {
return nil, err
}
// Note: no randomization here. It is a sanity check that verifies
// refreshToken did its job.
if internal.TokenExpiresIn(a.ctx, tok, lifetime) {
return nil, fmt.Errorf("auth: failed to refresh the token")
}
}
return &tok.Token, nil
}
// GetEmail returns an email associated with the credentials.
//
// In most cases this is a fast call that hits the cache. In some rare cases it
// may do an RPC to the Token Info endpoint to grab an email associated with the
// token.
//
// Returns ErrNoEmail if the email is not available. This may happen, for
// example, when using a refresh token that doesn't have 'userinfo.email' scope.
// Callers must expect this error to show up and should prepare a fallback.
//
// Returns an error if the email can't be fetched due to some other transient
// or fatal error. In particular, returns ErrLoginRequired if interactive login
// is required to get the token in the first place.
func (a *Authenticator) GetEmail() (string, error) {
// Grab last known token and its associated email. Note that this call also
// initializes the guts of the authenticator, including a.authToken.
tok, err := a.currentToken()
switch {
case err != nil:
return "", err
case tok != nil && tok.Email == internal.NoEmail:
return "", ErrNoEmail
case tok != nil && tok.Email != internal.UnknownEmail:
return tok.Email, nil
}
// There's no token cached yet (and thus email is not known). First try to ask
// the provider for email only. Most providers can return it without doing any
// RPCs or heavy calls. If this is not supported, resort to a heavier code
// paths that actually refreshes the token and grabs its email along the way.
a.lock.RLock()
email := a.authToken.provider.Email()
a.lock.RUnlock()
switch {
case email == internal.NoEmail:
return "", ErrNoEmail
case email != "":
return email, nil
}
// The provider doesn't know the email. We need a forceful token refresh to
// grab it (or discover it is NoEmail). This is relatively rare. It happens
// only when using UserAuth TokenProvider and there's no cached token at all
// or it is in old format that don't have email field.
//
// Pass -1 as lifetime to force trigger the refresh right now.
tok, err = a.refreshToken(tok, -1)
switch {
case err != nil:
return "", err
case tok.Email == internal.NoEmail:
return "", ErrNoEmail
case tok.Email == internal.UnknownEmail: // this must not happen, but let's be cautious
return "", fmt.Errorf("internal error when fetching the email, see logs")
default:
return tok.Email, nil
}
}
// CheckLoginRequired decides whether an interactive login is required.
//
// It examines the token cache and the configured authentication method to
// figure out whether we can attempt to grab an access token without involving
// the user interaction.
//
// Note: it does not check that the cached refresh token is still valid (i.e.
// not revoked). A revoked token will result in ErrLoginRequired error on a
// first attempt to use it.
//
// Returns:
// * nil if we have a valid cached token or can mint one on the fly.
// * ErrLoginRequired if we have no cached token and need to bother the user.
// * ErrInsufficientAccess if the configured auth method can't mint the token
// we require (e.g when using GCE method and the instance doesn't have all
// requested OAuth scopes).
// * Generic error on other unexpected errors.
func (a *Authenticator) CheckLoginRequired() error {
a.lock.Lock()
defer a.lock.Unlock()
if err := a.ensureInitialized(); err != nil {
return err
}
// No cached base token and the token provider requires interaction with the
// user: need to login. Only non-interactive token providers are allowed to
// mint tokens on the fly, see refreshToken.
if a.baseToken.token == nil && a.baseToken.provider.RequiresInteraction() {
return ErrLoginRequired
}
return nil
}
// Login perform an interaction with the user to get a long term refresh token
// and cache it.
//
// Blocks for user input, can use stdin. It overwrites currently cached
// credentials, if any.
//
// When used with non-interactive token providers (e.g. based on service
// accounts), just clears the cached access token, so next the next
// authenticated call gets a fresh one.
func (a *Authenticator) Login() error {
a.lock.Lock()
defer a.lock.Unlock()
err := a.ensureInitialized()
if err != nil {
return err
}
// Remove any cached tokens to trigger full relogin.
if err := a.purgeCredentialsCacheLocked(); err != nil {
return err
}
if !a.baseToken.provider.RequiresInteraction() {
return nil // can mint the token on the fly, no need for login
}
// Create an initial base token. This may require interaction with a user. Do
// not do retries here, since Login is called when the user is looking, let
// the user do the retries (since if MintToken() interacts with the user,
// retrying it automatically will be extra confusing).
a.baseToken.token, err = a.baseToken.provider.MintToken(a.ctx, nil)
if err != nil {
return err
}
// Store the initial token in the cache. Don't abort if it fails, the token
// is still usable from the memory.
if err := a.baseToken.putToCache(a.ctx); err != nil {
logging.Warningf(a.ctx, "Failed to write token to cache: %s", err)
}
return nil
}
// PurgeCredentialsCache removes cached tokens.
//
// Does not revoke them!
func (a *Authenticator) PurgeCredentialsCache() error {
a.lock.Lock()
defer a.lock.Unlock()
if err := a.ensureInitialized(); err != nil {
return err
}
return a.purgeCredentialsCacheLocked()
}
func (a *Authenticator) purgeCredentialsCacheLocked() error {
// No need to purge twice if baseToken == authToken, which is the case in
// non-actor mode.
var merr errors.MultiError
if a.baseToken != a.authToken {
merr = errors.NewMultiError(
a.baseToken.purgeToken(a.ctx),
a.authToken.purgeToken(a.ctx))
} else {
merr = errors.NewMultiError(a.baseToken.purgeToken(a.ctx))
}
switch total, first := merr.Summary(); {
case total == 0:
return nil
case total == 1:
return first
default:
return merr
}
}
////////////////////////////////////////////////////////////////////////////////
// credentials.PerRPCCredentials implementation.
type perRPCCreds struct {
a *Authenticator
}
func (creds perRPCCreds) GetRequestMetadata(c context.Context, uri ...string) (map[string]string, error) {
if len(uri) == 0 {
panic("perRPCCreds: no URI given")
}
if creds.a == nil {
return nil, nil
}
tok, err := creds.a.GetAccessToken(minAcceptedLifetime)
if err != nil {
return nil, err
}
return map[string]string{
"Authorization": tok.TokenType + " " + tok.AccessToken,
}, nil
}
func (creds perRPCCreds) RequireTransportSecurity() bool { return true }
////////////////////////////////////////////////////////////////////////////////
// oauth2.TokenSource implementation.
type tokenSource struct {
a *Authenticator
}
// Token is part of oauth2.TokenSource inteface.
func (s tokenSource) Token() (*oauth2.Token, error) {
return s.a.GetAccessToken(minAcceptedLifetime)
}
////////////////////////////////////////////////////////////////////////////////
// Authenticator private methods.
// isActing is true if ActAsServiceAccount is set.
//
// In this mode baseToken != authToken.
func (a *Authenticator) isActing() bool {
return a.opts.ActAsServiceAccount != ""
}
// checkInitialized is (true, <err>) if initialization happened (successfully or
// not) of (false, nil) if not.
func (a *Authenticator) checkInitialized() (bool, error) {
if a.err != nil || a.baseToken != nil {
return true, a.err
}
return false, nil
}
// ensureInitialized instantiates TokenProvider and reads token from cache.
//
// It is supposed to be called under the lock.
func (a *Authenticator) ensureInitialized() error {
// Already initialized (successfully or not)?
if initialized, err := a.checkInitialized(); initialized {
return err
}
// SelectBestMethod may do heavy calls like talking to GCE metadata server,
// call it lazily here rather than in NewAuthenticator.
if a.opts.Method == AutoSelectMethod {
a.opts.Method = SelectBestMethod(a.ctx, *a.opts)
}
// In Actor mode, make the base token IAM-scoped, to be able to use SignBlob
// API. In non-actor mode, the base token is also the main auth token, so
// scope it to whatever options were requested.
scopes := a.opts.Scopes
if a.isActing() {
scopes = []string{iam.OAuthScope}
}
a.baseToken = &tokenWithProvider{}
a.baseToken.provider, a.err = makeBaseTokenProvider(a.ctx, a.opts, scopes)
if a.err != nil {
return a.err // note: this can be ErrInsufficientAccess
}
// In non-actor mode, the token we must check in 'CheckLoginRequired' is the
// same as returned by 'GetAccessToken'. In actor mode, they are different.
// See comments for 'baseToken' and 'authToken'.
if a.isActing() {
a.authToken = &tokenWithProvider{}
a.authToken.provider, a.err = makeIAMTokenProvider(a.ctx, a.opts)
if a.err != nil {
return a.err
}
} else {
a.authToken = a.baseToken
}
// Initialize the token cache. Use the disk cache only if SecretsDir is given
// and any of the providers is not "lightweight" (so it makes sense to
// actually hit the disk, rather then call the provider each time new token is
// needed).
//
// Note also that tests set a.testingCache before ensureInitialized() is
// called to mock the cache. Respect this.
if a.testingCache != nil {
a.baseToken.cache = a.testingCache
a.authToken.cache = a.testingCache
} else {
cache := internal.ProcTokenCache
if !a.baseToken.provider.Lightweight() || !a.authToken.provider.Lightweight() {
if a.opts.SecretsDir != "" {
cache = &internal.DiskTokenCache{
Context: a.ctx,
SecretsDir: a.opts.SecretsDir,
}
} else {
logging.Warningf(a.ctx, "Disabling auth disk token cache. Not configured.")
}
}
// Use the disk cache only for non-lightweight providers to avoid
// unnecessarily leaks of tokens to the disk.
if a.baseToken.provider.Lightweight() {
a.baseToken.cache = internal.ProcTokenCache
} else {
a.baseToken.cache = cache
}
if a.authToken.provider.Lightweight() {
a.authToken.cache = internal.ProcTokenCache
} else {
a.authToken.cache = cache
}
}
// Interactive providers need to know whether there's a cached token (to ask
// to run interactive login if there's none). Non-interactive providers do not
// care about state of the cache that much (they know how to update it
// themselves). So examine the cache here only when using interactive
// provider. Non interactive providers will do it lazily on a first
// refreshToken(...) call.
if a.baseToken.provider.RequiresInteraction() {
// Broken token cache is not a fatal error. So just log it and forget, a new
// token will be minted in Login.
if err := a.baseToken.fetchFromCache(a.ctx); err != nil {
logging.Warningf(a.ctx, "Failed to read auth token from cache: %s", err)
}
}
// Note: a.authToken.provider is either equal to a.baseToken.provider (if not
// using actor mode), or (when using actor mode) it doesn't require
// interaction (because it is an IAM one). So don't bother with fetching
// 'authToken' from cache. It will be fetched lazily on the first use.
return nil
}
// doLoginIfRequired optionally performs an interactive login.
//
// This is the main place where LoginMode handling is performed. Used by various
// factories (Transport, PerRPCCredentials, TokenSource, ...).
//
// If requiresAuth is false, we respect OptionalLogin mode. If true - we treat
// OptionalLogin mode as SilentLogin: some authentication mechanisms (like
// oauth2.TokenSource) require valid tokens no matter what. The corresponding
// factories set requiresAuth to true.
//
// Returns:
// (true, nil) if successfully initialized the authenticator with some token.
// (false, nil) to disable authentication (for OptionalLogin mode).
// (false, err) on errors.
func (a *Authenticator) doLoginIfRequired(requiresAuth bool) (useAuth bool, err error) {
err = a.CheckLoginRequired() // also initializes guts for effectiveLoginMode()
effectiveMode := a.effectiveLoginMode()
if requiresAuth && effectiveMode == OptionalLogin {
effectiveMode = SilentLogin
}
switch {
case err == nil:
return true, nil // have a valid cached base token
case err == ErrInsufficientAccess && effectiveMode == OptionalLogin:
return false, nil // have the base token, but it doesn't have enough scopes
case err != ErrLoginRequired:
return false, err // some error we can't handle (we handle only ErrLoginRequired)
case effectiveMode == SilentLogin:
return false, ErrLoginRequired // can't run Login in SilentLogin mode
case effectiveMode == OptionalLogin:
return false, nil // we can skip auth in OptionalLogin if we have no token
case effectiveMode != InteractiveLogin:
return false, fmt.Errorf("invalid mode argument: %s", effectiveMode)
}
if err := a.Login(); err != nil {
return false, err
}
return true, nil
}
// effectiveLoginMode returns a login mode to use, considering what kind of a
// token provider is being used.
//
// See comments for OptionalLogin for more info. The gist of it: we treat
// OptionalLogin as SilentLogin when using a service account private key.
func (a *Authenticator) effectiveLoginMode() (lm LoginMode) {
// a.opts.Method is modified under a lock, need to grab the lock to avoid a
// race. Note that a.loginMode is immutable and can be read outside the
// lock. We skip the locking if we know for sure that the return value will be
// same as a.loginMode (which is the case for a.loginMode != OptionalLogin).
lm = a.loginMode
if lm == OptionalLogin {
a.lock.RLock()
if a.opts.Method == ServiceAccountMethod {
lm = SilentLogin
}
a.lock.RUnlock()
}
return
}
// currentToken returns last known authentication token (or nil).