| // 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" |
| |
| "go.chromium.org/luci/auth/internal" |
| "go.chromium.org/luci/common/clock" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/gcloud/iam" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/common/retry" |
| "go.chromium.org/luci/common/retry/transient" |
| "go.chromium.org/luci/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") |
| ) |
| |
| // Some known Google API OAuth scopes. |
| const ( |
| OAuthScopeEmail = "https://www.googleapis.com/auth/userinfo.email" |
| OAuthScopeIAM = "https://www.googleapis.com/auth/iam" |
| ) |
| |
| 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 ServiceAccountJSON 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 ActAsServiceAccount ("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" |
| ) |
| |
| // 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 generateAccessToken Cloud IAM API and "Service Account Token |
| // Creator" 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 "Service Account Token Creator" 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 (see `OAuthScopeIAM`). 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 |
| |
| // MinTokenLifetime defines a minimally acceptable lifetime of access tokens |
| // generated internally by authenticating http.RoundTripper, TokenSource and |
| // PerRPCCredentials. |
| // |
| // Not used when GetAccessToken is called directly (it accepts this parameter |
| // as an argument). |
| // |
| // The default is 2 min. There's rarely a need to change it and using smaller |
| // values may be dangerous (e.g. if the request gets stuck somewhere or the |
| // token is cached incorrectly it may expire before it is checked). |
| MinTokenLifetime time.Duration |
| |
| // 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 generateAccessToken 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 IAM API call |
| // generateAccessToken. 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 |
| } |
| if opts.MinTokenLifetime == 0 { |
| opts.MinTokenLifetime = 2 * time.Minute |
| } |
| |
| // 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(creds.a.opts.MinTokenLifetime) |
| 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 interface. |
| func (s tokenSource) Token() (*oauth2.Token, error) { |
| return s.a.GetAccessToken(s.a.opts.MinTokenLifetime) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 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 |
| // generateAccessToken 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). |
| // |
| // If the token is not loaded yet, will attempt to load it from the on-disk |
| // cache. Returns nil if it's not there. |
| // |
| // It lock a.lock inside. It MUST NOT be called when a.lock is held. It will |
| // lazily call 'ensureInitialized' if necessary, returning its error. |
| func (a *Authenticator) currentToken() (tok *internal.Token, err error) { |
| a.lock.RLock() |
| initialized, err := a.checkInitialized() |
| if initialized && err == nil { |
| tok = a.authToken.token |
| } |
| a.lock.RUnlock() |
| if err != nil { |
| return |
| } |
| |
| if !initialized || tok == nil { |
| a.lock.Lock() |
| defer a.lock.Unlock() |
| |
| if !initialized { |
| if err = a.ensureInitialized(); err != nil { |
| return |
| } |
| tok = a.authToken.token |
| } |
| |
| if tok == nil { |
| // Reading the token from cache is best effort. A broken cache is treated |
| // like a cache miss. |
| if cacheErr := a.authToken.fetchFromCache(a.ctx); cacheErr != nil { |
| logging.Warningf(a.ctx, "Failed to read auth token from cache: %s", cacheErr) |
| } |
| tok = a.authToken.token |
| } |
| } |
| |
| return |
| } |
| |
| // refreshToken compares current auth token to 'prev' and launches token refresh |
| // procedure if they still match. |
| // |
| // Returns a refreshed token (if a refresh procedure happened) or the current |
| // token, if it's already different from 'prev'. Acts as "Compare-And-Swap" |
| // where "Swap" is a token refresh procedure. |
| // |
| // If the token can't be refreshed (e.g. the base token or the credentials were |
| // revoked), sets the current auth token to nil and returns an error. |
| func (a *Authenticator) refreshToken(prev *internal.Token, lifetime time.Duration) (*internal.Token, error) { |
| return a.authToken.compareAndRefresh(a.ctx, compareAndRefreshOp{ |
| lock: &a.lock, |
| prev: prev, |
| lifetime: lifetime, |
| refreshCb: func(ctx context.Context, prev *internal.Token) (*internal.Token, error) { |
| // In Actor mode, need to make sure we have a sufficiently fresh base |
| // token first, since it's needed to get the new auth token. 30 sec should |
| // be more than enough to make an IAM call. |
| var base *internal.Token |
| if a.isActing() { |
| var err error |
| if base, err = a.getBaseTokenLocked(ctx, 30*time.Second); err != nil { |
| return nil, err |
| } |
| } |
| return a.authToken.renewToken(ctx, prev, base) |
| }, |
| }) |
| } |
| |
| // getBaseTokenLocked is used to get an IAM-scoped token when running in |
| // actor mode. |
| // |
| // Passing negative lifetime causes a forced refresh. |
| // |
| // It is called with a.lock locked. |
| func (a *Authenticator) getBaseTokenLocked(ctx context.Context, lifetime time.Duration) (*internal.Token, error) { |
| if !a.isActing() { |
| panic("impossible") |
| } |
| |
| // Already have a good token? |
| if lifetime > 0 && !internal.TokenExpiresInRnd(ctx, a.baseToken.token, lifetime) { |
| return a.baseToken.token, nil |
| } |
| |
| // Need to make one. |
| return a.baseToken.compareAndRefresh(ctx, compareAndRefreshOp{ |
| lock: nil, // already holding the lock |
| prev: a.baseToken.token, |
| lifetime: lifetime, |
| refreshCb: func(ctx context.Context, prev *internal.Token) (*internal.Token, error) { |
| return a.baseToken.renewToken(ctx, prev, nil) |
| }, |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Transport implementation. |
| |
| // authTokenInjector injects an authentication token into request headers. |
| // |
| // Used as a callback for NewModifyingTransport. |
| func (a *Authenticator) authTokenInjector(req *http.Request) error { |
| switch tok, err := a.GetAccessToken(a.opts.MinTokenLifetime); { |
| case err == ErrLoginRequired && a.effectiveLoginMode() == OptionalLogin: |
| return nil // skip auth, no need for modifications |
| case err != nil: |
| return err |
| default: |
| tok.SetAuthHeader(req) |
| return nil |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // tokenWithProvider implementation. |
| |
| // tokenWithProvider wraps a token with provider that can update it and a cache |
| // that stores it. |
| type tokenWithProvider struct { |
| token *internal.Token // in-memory cache of the token |
| provider internal.TokenProvider // knows how to generate 'token' |
| cache internal.TokenCache // persistent cache for the token |
| } |
| |
| // fetchFromCache updates 't.token' by reading it from the cache. |
| func (t *tokenWithProvider) fetchFromCache(ctx context.Context) error { |
| key, err := t.provider.CacheKey(ctx) |
| if err != nil { |
| return err |
| } |
| tok, err := t.cache.GetToken(key) |
| if err != nil { |
| return err |
| } |
| t.token = tok |
| return nil |
| } |
| |
| // putToCache puts 't.token' value into the cache. |
| func (t *tokenWithProvider) putToCache(ctx context.Context) error { |
| key, err := t.provider.CacheKey(ctx) |
| if err != nil { |
| return err |
| } |
| return t.cache.PutToken(key, t.token) |
| } |
| |
| // purgeToken removes the token from both on-disk cache and memory. |
| func (t *tokenWithProvider) purgeToken(ctx context.Context) error { |
| t.token = nil |
| key, err := t.provider.CacheKey(ctx) |
| if err != nil { |
| return err |
| } |
| return t.cache.DeleteToken(key) |
| } |
| |
| // compareAndRefreshOp is parameters for 'compareAndRefresh' call. |
| type compareAndRefreshOp struct { |
| lock sync.Locker // optional lock to grab when comparing and refreshing |
| prev *internal.Token // previously known token (the one we are refreshing) |
| lifetime time.Duration // minimum acceptable token lifetime or <0 to force a refresh |
| refreshCb func(ctx context.Context, existing *internal.Token) (*internal.Token, error) |
| } |
| |
| // compareAndRefresh compares currently stored token to 'prev' and calls the |
| // given callback (under the lock, if not nil) to refresh it if they are still |
| // equal. |
| // |
| // Returns a refreshed token (if a refresh procedure happened) or the current |
| // token, if it's already different from 'prev'. Acts as "Compare-And-Swap" |
| // where "Swap" is a token refresh callback. |
| // |
| // If the callback returns an error (meaning the token can't be refreshed), sets |
| // the token to nil and returns the error. |
| func (t *tokenWithProvider) compareAndRefresh(ctx context.Context, params compareAndRefreshOp) (*internal.Token, error) { |
| cacheKey, err := t.provider.CacheKey(ctx) |
| if err != nil { |
| // An error here is truly fatal. It is something like "can't read service |
| // account JSON from disk". There's no way to refresh a token without it. |
| return nil, err |
| } |
| |
| // To give a context to "Minting a new token" messages and similar below. |
| ctx = logging.SetFields(ctx, logging.Fields{ |
| "key": cacheKey.Key, |
| "scopes": strings.Join(cacheKey.Scopes, " "), |
| }) |
| |
| // Check that the token still need a refresh and do it (under the lock). |
| tok, cacheIt, err := func() (*internal.Token, bool, error) { |
| if params.lock != nil { |
| params.lock.Lock() |
| defer params.lock.Unlock() |
| } |
| |
| // Some other goroutine already updated the token, just return the new one. |
| if t.token != nil && !internal.EqualTokens(t.token, params.prev) { |
| return t.token, false, nil |
| } |
| |
| // Rescan the cache. Maybe some other process updated the token. This branch |
| // is also responsible for lazy-loading of tokens from cache for |
| // non-interactive providers, see ensureInitialized(). |
| if cached, _ := t.cache.GetToken(cacheKey); cached != nil { |
| t.token = cached |
| if !internal.EqualTokens(cached, params.prev) && params.lifetime > 0 && !internal.TokenExpiresIn(ctx, cached, params.lifetime) { |
| return cached, false, nil |
| } |
| } |
| |
| // No one updated the token yet. It should be us. Mint a new token or |
| // refresh the existing one. |
| start := clock.Now(ctx) |
| newTok, err := params.refreshCb(ctx, t.token) |
| if err != nil { |
| t.token = nil |
| return nil, false, err |
| } |
| now := clock.Now(ctx) |
| logging.Debugf( |
| ctx, "The token refreshed in %s, expires in %s", |
| now.Sub(start), newTok.Expiry.Round(0).Sub(now)) |
| t.token = newTok |
| return newTok, true, nil |
| }() |
| |
| if err == internal.ErrBadRefreshToken || err == internal.ErrBadCredentials { |
| // Do not keep the broken token in the cache. It is unusable. Do this |
| // outside the lock to avoid blocking other callers. Note that t.token is |
| // already nil. |
| if err := t.cache.DeleteToken(cacheKey); err != nil { |
| logging.Warningf(ctx, "Failed to remove broken token from the cache: %s", err) |
| } |
| // A bad refresh token can be fixed by interactive login, so adjust the |
| // error accordingly in this case. |
| if err == internal.ErrBadRefreshToken { |
| err = ErrLoginRequired |
| } |
| } |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| // Update the cache outside the lock, no need for callers to wait for this. |
| // Do not die if failed, the token is still usable from the memory. |
| if cacheIt && tok != nil { |
| if err := t.cache.PutToken(cacheKey, tok); err != nil { |
| logging.Warningf(ctx, "Failed to write refreshed token to the cache: %s", err) |
| } |
| } |
| |
| return tok, nil |
| } |
| |
| // renewToken is called to mint a new token or update existing one. |
| // |
| // It is called from non-interactive 'refreshToken' method, and thus it can't |
| // use interactive login flow. |
| func (t *tokenWithProvider) renewToken(ctx context.Context, prev, base *internal.Token) (*internal.Token, error) { |
| if prev == nil { |
| if t.provider.RequiresInteraction() { |
| return nil, ErrLoginRequired |
| } |
| logging.Debugf(ctx, "Minting a new token") |
| tok, err := t.mintTokenWithRetries(ctx, base) |
| if err != nil { |
| logging.Warningf(ctx, "Failed to mint a token: %s", err) |
| return nil, err |
| } |
| return tok, nil |
| } |
| |
| logging.Debugf(ctx, "Refreshing the token") |
| tok, err := t.refreshTokenWithRetries(ctx, prev, base) |
| if err != nil { |
| logging.Warningf(ctx, "Failed to refresh the token: %s", err) |
| return nil, err |
| } |
| return tok, nil |
| } |
| |
| // retryParams defines retry strategy for handling transient errors when minting |
| // or refreshing tokens. |
| func retryParams() retry.Iterator { |
| return &retry.ExponentialBackoff{ |
| Limited: retry.Limited{ |
| Delay: 100 * time.Millisecond, |
| Retries: 50, |
| MaxTotal: 30 * time.Second, |
| }, |
| Multiplier: 2, |
| } |
| } |
| |
| // mintTokenWithRetries calls provider's MintToken() retrying on transient |
| // errors a bunch of times. Called only for non-interactive providers. |
| func (t *tokenWithProvider) mintTokenWithRetries(ctx context.Context, base *internal.Token) (tok *internal.Token, err error) { |
| err = retry.Retry(ctx, transient.Only(retryParams), func() error { |
| tok, err = t.provider.MintToken(ctx, base) |
| return err |
| }, retry.LogCallback(ctx, "token-mint")) |
| return |
| } |
| |
| // refreshTokenWithRetries calls providers' RefreshToken(...) retrying on |
| // transient errors a bunch of times. |
| func (t *tokenWithProvider) refreshTokenWithRetries(ctx context.Context, prev, base *internal.Token) (tok *internal.Token, err error) { |
| err = retry.Retry(ctx, transient.Only(retryParams), func() error { |
| tok, err = t.provider.RefreshToken(ctx, prev, base) |
| return err |
| }, retry.LogCallback(ctx, "token-refresh")) |
| return |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Utility functions. |
| |
| // makeBaseTokenProvider creates TokenProvider implementation based on options. |
| // |
| // opts.Scopes is ignored, 'scopes' is used instead. This is used in actor mode |
| // to substitute scopes with IAM scope. |
| // |
| // Called by ensureInitialized. |
| func makeBaseTokenProvider(ctx context.Context, opts *Options, scopes []string) (internal.TokenProvider, error) { |
| if opts.testingBaseTokenProvider != nil { |
| return opts.testingBaseTokenProvider, nil |
| } |
| |
| switch opts.Method { |
| case UserCredentialsMethod: |
| return internal.NewUserAuthTokenProvider( |
| ctx, |
| opts.ClientID, |
| opts.ClientSecret, |
| scopes) |
| case ServiceAccountMethod: |
| serviceAccountPath := "" |
| if len(opts.ServiceAccountJSON) == 0 { |
| serviceAccountPath = opts.ServiceAccountJSONPath |
| } |
| return internal.NewServiceAccountTokenProvider( |
| ctx, |
| opts.ServiceAccountJSON, |
| serviceAccountPath, |
| scopes) |
| case GCEMetadataMethod: |
| return internal.NewGCETokenProvider(ctx, opts.GCEAccountName, scopes) |
| case LUCIContextMethod: |
| return internal.NewLUCIContextTokenProvider(ctx, scopes, opts.Transport) |
| default: |
| return nil, fmt.Errorf("auth: unrecognized authentication method: %s", opts.Method) |
| } |
| } |
| |
| // makeIAMTokenProvider create TokenProvider to use in Actor mode. |
| // |
| // Called by ensureInitialized in actor mode. |
| func makeIAMTokenProvider(ctx context.Context, opts *Options) (internal.TokenProvider, error) { |
| if opts.testingIAMTokenProvider != nil { |
| return opts.testingIAMTokenProvider, nil |
| } |
| return internal.NewIAMTokenProvider( |
| ctx, |
| opts.ActAsServiceAccount, |
| opts.Scopes, |
| opts.Transport) |
| } |