...

Source file src/golang.org/x/oauth2/google/externalaccount/basecredentials.go

Documentation: golang.org/x/oauth2/google/externalaccount

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  /*
     6  Package externalaccount provides support for creating workload identity
     7  federation and workforce identity federation token sources that can be
     8  used to access Google Cloud resources from external identity providers.
     9  
    10  # Workload Identity Federation
    11  
    12  Using workload identity federation, your application can access Google Cloud
    13  resources from Amazon Web Services (AWS), Microsoft Azure or any identity
    14  provider that supports OpenID Connect (OIDC) or SAML 2.0.
    15  Traditionally, applications running outside Google Cloud have used service
    16  account keys to access Google Cloud resources. Using identity federation,
    17  you can allow your workload to impersonate a service account.
    18  This lets you access Google Cloud resources directly, eliminating the
    19  maintenance and security burden associated with service account keys.
    20  
    21  Follow the detailed instructions on how to configure Workload Identity Federation
    22  in various platforms:
    23  
    24  Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws
    25  Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure
    26  OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc
    27  SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml
    28  
    29  For OIDC and SAML providers, the library can retrieve tokens in fours ways:
    30  from a local file location (file-sourced credentials), from a server
    31  (URL-sourced credentials), from a local executable (executable-sourced
    32  credentials), or from a user defined function that returns an OIDC or SAML token.
    33  For file-sourced credentials, a background process needs to be continuously
    34  refreshing the file location with a new OIDC/SAML token prior to expiration.
    35  For tokens with one hour lifetimes, the token needs to be updated in the file
    36  every hour. The token can be stored directly as plain text or in JSON format.
    37  For URL-sourced credentials, a local server needs to host a GET endpoint to
    38  return the OIDC/SAML token. The response can be in plain text or JSON.
    39  Additional required request headers can also be specified.
    40  For executable-sourced credentials, an application needs to be available to
    41  output the OIDC/SAML token and other information in a JSON format.
    42  For more information on how these work (and how to implement
    43  executable-sourced credentials), please check out:
    44  https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration
    45  
    46  To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers,
    47  or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config].
    48  The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used to access Google
    49  Cloud resources. For instance, you can create a new client from the
    50  [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
    51  
    52  Note that this library does not perform any validation on the token_url, token_info_url,
    53  or service_account_impersonation_url fields of the credential configuration.
    54  It is not recommended to use a credential configuration that you did not generate with
    55  the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
    56  
    57  # Workforce Identity Federation
    58  
    59  Workforce identity federation lets you use an external identity provider (IdP) to
    60  authenticate and authorize a workforce—a group of users, such as employees, partners,
    61  and contractors—using IAM, so that the users can access Google Cloud services.
    62  Workforce identity federation extends Google Cloud's identity capabilities to support
    63  syncless, attribute-based single sign on.
    64  
    65  With workforce identity federation, your workforce can access Google Cloud resources
    66  using an external identity provider (IdP) that supports OpenID Connect (OIDC) or
    67  SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation
    68  Services (AD FS), Okta, and others.
    69  
    70  Follow the detailed instructions on how to configure Workload Identity Federation
    71  in various platforms:
    72  
    73  Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad
    74  Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta
    75  OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc
    76  SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml
    77  
    78  For workforce identity federation, the library can retrieve tokens in four ways:
    79  from a local file location (file-sourced credentials), from a server
    80  (URL-sourced credentials), from a local executable (executable-sourced
    81  credentials), or from a user supplied function that returns an OIDC or SAML token.
    82  For file-sourced credentials, a background process needs to be continuously
    83  refreshing the file location with a new OIDC/SAML token prior to expiration.
    84  For tokens with one hour lifetimes, the token needs to be updated in the file
    85  every hour. The token can be stored directly as plain text or in JSON format.
    86  For URL-sourced credentials, a local server needs to host a GET endpoint to
    87  return the OIDC/SAML token. The response can be in plain text or JSON.
    88  Additional required request headers can also be specified.
    89  For executable-sourced credentials, an application needs to be available to
    90  output the OIDC/SAML token and other information in a JSON format.
    91  For more information on how these work (and how to implement
    92  executable-sourced credentials), please check out:
    93  https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in
    94  
    95  To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers.
    96  This can then be used when building a [Config].
    97  The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google
    98  Cloud resources. For instance, you can create a new client from the
    99  [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
   100  
   101  # Security considerations
   102  
   103  Note that this library does not perform any validation on the token_url, token_info_url,
   104  or service_account_impersonation_url fields of the credential configuration.
   105  It is not recommended to use a credential configuration that you did not generate with
   106  the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
   107  */
   108  package externalaccount
   109  
   110  import (
   111  	"context"
   112  	"fmt"
   113  	"net/http"
   114  	"regexp"
   115  	"strconv"
   116  	"strings"
   117  	"time"
   118  
   119  	"golang.org/x/oauth2"
   120  	"golang.org/x/oauth2/google/internal/impersonate"
   121  	"golang.org/x/oauth2/google/internal/stsexchange"
   122  )
   123  
   124  const (
   125  	universeDomainPlaceholder = "UNIVERSE_DOMAIN"
   126  	defaultTokenURL           = "https://sts.UNIVERSE_DOMAIN/v1/token"
   127  	defaultUniverseDomain     = "googleapis.com"
   128  )
   129  
   130  // now aliases time.Now for testing
   131  var now = func() time.Time {
   132  	return time.Now().UTC()
   133  }
   134  
   135  // Config stores the configuration for fetching tokens with external credentials.
   136  type Config struct {
   137  	// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
   138  	// identity pool or the workforce pool and the provider identifier in that pool. Required.
   139  	Audience string
   140  	// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec.
   141  	// Expected values include:
   142  	// “urn:ietf:params:oauth:token-type:jwt”
   143  	// “urn:ietf:params:oauth:token-type:id-token”
   144  	// “urn:ietf:params:oauth:token-type:saml2”
   145  	// “urn:ietf:params:aws:token-type:aws4_request”
   146  	// Required.
   147  	SubjectTokenType string
   148  	// TokenURL is the STS token exchange endpoint. If not provided, will default to
   149  	// https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set to the
   150  	// default service domain googleapis.com unless UniverseDomain is set.
   151  	// Optional.
   152  	TokenURL string
   153  	// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
   154  	// user attributes like account identifier, eg. email, username, uid, etc). This is
   155  	// needed for gCloud session account identification. Optional.
   156  	TokenInfoURL string
   157  	// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
   158  	// required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional.
   159  	ServiceAccountImpersonationURL string
   160  	// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
   161  	// token will be valid for. If not provided, it will default to 3600. Optional.
   162  	ServiceAccountImpersonationLifetimeSeconds int
   163  	// ClientSecret is currently only required if token_info endpoint also
   164  	// needs to be called with the generated GCP access token. When provided, STS will be
   165  	// called with additional basic authentication using ClientId as username and ClientSecret as password. Optional.
   166  	ClientSecret string
   167  	// ClientID is only required in conjunction with ClientSecret, as described above. Optional.
   168  	ClientID string
   169  	// CredentialSource contains the necessary information to retrieve the token itself, as well
   170  	// as some environmental information. One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or
   171  	// CredentialSource must be provided. Optional.
   172  	CredentialSource *CredentialSource
   173  	// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
   174  	// will set the x-goog-user-project header which overrides the project associated with the credentials. Optional.
   175  	QuotaProjectID string
   176  	// Scopes contains the desired scopes for the returned access token. Optional.
   177  	Scopes []string
   178  	// WorkforcePoolUserProject is the workforce pool user project number when the credential
   179  	// corresponds to a workforce pool and not a workload identity pool.
   180  	// The underlying principal must still have serviceusage.services.use IAM
   181  	// permission to use the project for billing/quota. Optional.
   182  	WorkforcePoolUserProject string
   183  	// SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials.
   184  	// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
   185  	SubjectTokenSupplier SubjectTokenSupplier
   186  	// AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials.
   187  	// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
   188  	AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
   189  	// UniverseDomain is the default service domain for a given Cloud universe.
   190  	// This value will be used in the default STS token URL. The default value
   191  	// is "googleapis.com". It will not be used if TokenURL is set. Optional.
   192  	UniverseDomain string
   193  }
   194  
   195  var (
   196  	validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
   197  )
   198  
   199  func validateWorkforceAudience(input string) bool {
   200  	return validWorkforceAudiencePattern.MatchString(input)
   201  }
   202  
   203  // NewTokenSource Returns an external account TokenSource using the provided external account config.
   204  func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) {
   205  	if conf.Audience == "" {
   206  		return nil, fmt.Errorf("oauth2/google/externalaccount: Audience must be set")
   207  	}
   208  	if conf.SubjectTokenType == "" {
   209  		return nil, fmt.Errorf("oauth2/google/externalaccount: Subject token type must be set")
   210  	}
   211  	if conf.WorkforcePoolUserProject != "" {
   212  		valid := validateWorkforceAudience(conf.Audience)
   213  		if !valid {
   214  			return nil, fmt.Errorf("oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials")
   215  		}
   216  	}
   217  	count := 0
   218  	if conf.CredentialSource != nil {
   219  		count++
   220  	}
   221  	if conf.SubjectTokenSupplier != nil {
   222  		count++
   223  	}
   224  	if conf.AwsSecurityCredentialsSupplier != nil {
   225  		count++
   226  	}
   227  	if count == 0 {
   228  		return nil, fmt.Errorf("oauth2/google/externalaccount: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
   229  	}
   230  	if count > 1 {
   231  		return nil, fmt.Errorf("oauth2/google/externalaccount: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
   232  	}
   233  	return conf.tokenSource(ctx, "https")
   234  }
   235  
   236  // tokenSource is a private function that's directly called by some of the tests,
   237  // because the unit test URLs are mocked, and would otherwise fail the
   238  // validity check.
   239  func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
   240  
   241  	ts := tokenSource{
   242  		ctx:  ctx,
   243  		conf: c,
   244  	}
   245  	if c.ServiceAccountImpersonationURL == "" {
   246  		return oauth2.ReuseTokenSource(nil, ts), nil
   247  	}
   248  	scopes := c.Scopes
   249  	ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
   250  	imp := impersonate.ImpersonateTokenSource{
   251  		Ctx:                  ctx,
   252  		URL:                  c.ServiceAccountImpersonationURL,
   253  		Scopes:               scopes,
   254  		Ts:                   oauth2.ReuseTokenSource(nil, ts),
   255  		TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
   256  	}
   257  	return oauth2.ReuseTokenSource(nil, imp), nil
   258  }
   259  
   260  // Subject token file types.
   261  const (
   262  	fileTypeText = "text"
   263  	fileTypeJSON = "json"
   264  )
   265  
   266  // Format contains information needed to retireve a subject token for URL or File sourced credentials.
   267  type Format struct {
   268  	// Type should be either "text" or "json". This determines whether the file or URL sourced credentials
   269  	// expect a simple text subject token or if the subject token will be contained in a JSON object.
   270  	// When not provided "text" type is assumed.
   271  	Type string `json:"type"`
   272  	// SubjectTokenFieldName is only required for JSON format. This is the field name that the credentials will check
   273  	// for the subject token in the file or URL response. This would be "access_token" for azure.
   274  	SubjectTokenFieldName string `json:"subject_token_field_name"`
   275  }
   276  
   277  // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
   278  type CredentialSource struct {
   279  	// File is the location for file sourced credentials.
   280  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
   281  	File string `json:"file"`
   282  
   283  	// Url is the URL to call for URL sourced credentials.
   284  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
   285  	URL string `json:"url"`
   286  	// Headers are the headers to attach to the request for URL sourced credentials.
   287  	Headers map[string]string `json:"headers"`
   288  
   289  	// Executable is the configuration object for executable sourced credentials.
   290  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
   291  	Executable *ExecutableConfig `json:"executable"`
   292  
   293  	// EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS".
   294  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
   295  	EnvironmentID string `json:"environment_id"`
   296  	// RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials.
   297  	RegionURL string `json:"region_url"`
   298  	// RegionalCredVerificationURL is the AWS regional credential verification URL, will default to
   299  	//  "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided."
   300  	RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
   301  	// IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS.
   302  	IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
   303  	// Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json".
   304  	Format Format `json:"format"`
   305  }
   306  
   307  // ExecutableConfig contains information needed for executable sourced credentials.
   308  type ExecutableConfig struct {
   309  	// Command is the the full command to run to retrieve the subject token.
   310  	// This can include arguments. Must be an absolute path for the program. Required.
   311  	Command string `json:"command"`
   312  	// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
   313  	TimeoutMillis *int `json:"timeout_millis"`
   314  	// OutputFile is the absolute path to the output file where the executable will cache the response.
   315  	// If specified the auth libraries will first check this location before running the executable. Optional.
   316  	OutputFile string `json:"output_file"`
   317  }
   318  
   319  // SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token.
   320  type SubjectTokenSupplier interface {
   321  	// SubjectToken should return a valid subject token or an error.
   322  	// The external account token source does not cache the returned subject token, so caching
   323  	// logic should be implemented in the supplier to prevent multiple requests for the same subject token.
   324  	SubjectToken(ctx context.Context, options SupplierOptions) (string, error)
   325  }
   326  
   327  // AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to
   328  // exchange for a GCP access token.
   329  type AwsSecurityCredentialsSupplier interface {
   330  	// AwsRegion should return the AWS region or an error.
   331  	AwsRegion(ctx context.Context, options SupplierOptions) (string, error)
   332  	// GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error.
   333  	// The external account token source does not cache the returned security credentials, so caching
   334  	// logic should be implemented in the supplier to prevent multiple requests for the same security credentials.
   335  	AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error)
   336  }
   337  
   338  // SupplierOptions contains information about the requested subject token or AWS security credentials from the
   339  // Google external account credential.
   340  type SupplierOptions struct {
   341  	// Audience is the requested audience for the external account credential.
   342  	Audience string
   343  	// Subject token type is the requested subject token type for the external account credential. Expected values include:
   344  	// “urn:ietf:params:oauth:token-type:jwt”
   345  	// “urn:ietf:params:oauth:token-type:id-token”
   346  	// “urn:ietf:params:oauth:token-type:saml2”
   347  	// “urn:ietf:params:aws:token-type:aws4_request”
   348  	SubjectTokenType string
   349  }
   350  
   351  // tokenURL returns the default STS token endpoint with the configured universe
   352  // domain.
   353  func (c *Config) tokenURL() string {
   354  	if c.UniverseDomain == "" {
   355  		return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
   356  	}
   357  	return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
   358  }
   359  
   360  // parse determines the type of CredentialSource needed.
   361  func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
   362  	//set Defaults
   363  	if c.TokenURL == "" {
   364  		c.TokenURL = c.tokenURL()
   365  	}
   366  	supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
   367  
   368  	if c.AwsSecurityCredentialsSupplier != nil {
   369  		awsCredSource := awsCredentialSource{
   370  			awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier,
   371  			targetResource:                 c.Audience,
   372  			supplierOptions:                supplierOptions,
   373  			ctx:                            ctx,
   374  		}
   375  		return awsCredSource, nil
   376  	} else if c.SubjectTokenSupplier != nil {
   377  		return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil
   378  	} else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
   379  		if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
   380  			if awsVersion != 1 {
   381  				return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion)
   382  			}
   383  
   384  			awsCredSource := awsCredentialSource{
   385  				environmentID:               c.CredentialSource.EnvironmentID,
   386  				regionURL:                   c.CredentialSource.RegionURL,
   387  				regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
   388  				credVerificationURL:         c.CredentialSource.URL,
   389  				targetResource:              c.Audience,
   390  				ctx:                         ctx,
   391  			}
   392  			if c.CredentialSource.IMDSv2SessionTokenURL != "" {
   393  				awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
   394  			}
   395  
   396  			return awsCredSource, nil
   397  		}
   398  	} else if c.CredentialSource.File != "" {
   399  		return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
   400  	} else if c.CredentialSource.URL != "" {
   401  		return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
   402  	} else if c.CredentialSource.Executable != nil {
   403  		return createExecutableCredential(ctx, c.CredentialSource.Executable, c)
   404  	}
   405  	return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source")
   406  }
   407  
   408  type baseCredentialSource interface {
   409  	credentialSourceType() string
   410  	subjectToken() (string, error)
   411  }
   412  
   413  // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
   414  type tokenSource struct {
   415  	ctx  context.Context
   416  	conf *Config
   417  }
   418  
   419  func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
   420  	return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
   421  		goVersion(),
   422  		"unknown",
   423  		credSource.credentialSourceType(),
   424  		conf.ServiceAccountImpersonationURL != "",
   425  		conf.ServiceAccountImpersonationLifetimeSeconds != 0)
   426  }
   427  
   428  // Token allows tokenSource to conform to the oauth2.TokenSource interface.
   429  func (ts tokenSource) Token() (*oauth2.Token, error) {
   430  	conf := ts.conf
   431  
   432  	credSource, err := conf.parse(ts.ctx)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	subjectToken, err := credSource.subjectToken()
   437  
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	stsRequest := stsexchange.TokenExchangeRequest{
   442  		GrantType:          "urn:ietf:params:oauth:grant-type:token-exchange",
   443  		Audience:           conf.Audience,
   444  		Scope:              conf.Scopes,
   445  		RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
   446  		SubjectToken:       subjectToken,
   447  		SubjectTokenType:   conf.SubjectTokenType,
   448  	}
   449  	header := make(http.Header)
   450  	header.Add("Content-Type", "application/x-www-form-urlencoded")
   451  	header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
   452  	clientAuth := stsexchange.ClientAuthentication{
   453  		AuthStyle:    oauth2.AuthStyleInHeader,
   454  		ClientID:     conf.ClientID,
   455  		ClientSecret: conf.ClientSecret,
   456  	}
   457  	var options map[string]interface{}
   458  	// Do not pass workforce_pool_user_project when client authentication is used.
   459  	// The client ID is sufficient for determining the user project.
   460  	if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
   461  		options = map[string]interface{}{
   462  			"userProject": conf.WorkforcePoolUserProject,
   463  		}
   464  	}
   465  	stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  
   470  	accessToken := &oauth2.Token{
   471  		AccessToken: stsResp.AccessToken,
   472  		TokenType:   stsResp.TokenType,
   473  	}
   474  
   475  	// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
   476  	if stsResp.ExpiresIn <= 0 {
   477  		return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service")
   478  	}
   479  	accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
   480  
   481  	if stsResp.RefreshToken != "" {
   482  		accessToken.RefreshToken = stsResp.RefreshToken
   483  	}
   484  	return accessToken, nil
   485  }
   486  

View as plain text