basecredentials.go raw

   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 retrieve 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  	//
 282  	// Important: If you accept a credential configuration (credential
 283  	// JSON/File/Stream) from an external source for authentication to Google
 284  	// Cloud Platform, you must validate it before providing it to any Google
 285  	// API or library. Providing an unvalidated credential configuration to
 286  	// Google APIs can compromise the security of your systems and data. For
 287  	// more information, refer to [Validate credential configurations from
 288  	// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 289  	File string `json:"file"`
 290  
 291  	// Url is the URL to call for URL sourced credentials.
 292  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
 293  	//
 294  	// Important: If you accept a credential configuration (credential
 295  	// JSON/File/Stream) from an external source for authentication to Google
 296  	// Cloud Platform, you must validate it before providing it to any Google
 297  	// API or library. Providing an unvalidated credential configuration to
 298  	// Google APIs can compromise the security of your systems and data. For
 299  	// more information, refer to [Validate credential configurations from
 300  	// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 301  	URL string `json:"url"`
 302  	// Headers are the headers to attach to the request for URL sourced credentials.
 303  	Headers map[string]string `json:"headers"`
 304  
 305  	// Executable is the configuration object for executable sourced credentials.
 306  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
 307  	//
 308  	// Important: If you accept a credential configuration (credential
 309  	// JSON/File/Stream) from an external source for authentication to Google
 310  	// Cloud Platform, you must validate it before providing it to any Google
 311  	// API or library. Providing an unvalidated credential configuration to
 312  	// Google APIs can compromise the security of your systems and data. For
 313  	// more information, refer to [Validate credential configurations from
 314  	// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 315  	Executable *ExecutableConfig `json:"executable"`
 316  
 317  	// EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS".
 318  	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
 319  	//
 320  	// Important: If you accept a credential configuration (credential
 321  	// JSON/File/Stream) from an external source for authentication to Google
 322  	// Cloud Platform, you must validate it before providing it to any Google
 323  	// API or library. Providing an unvalidated credential configuration to
 324  	// Google APIs can compromise the security of your systems and data. For
 325  	// more information, refer to [Validate credential configurations from
 326  	// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 327  	EnvironmentID string `json:"environment_id"`
 328  	// RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials.
 329  	RegionURL string `json:"region_url"`
 330  	// RegionalCredVerificationURL is the AWS regional credential verification URL, will default to
 331  	//  "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided."
 332  	RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
 333  	// IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS.
 334  	IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
 335  	// Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json".
 336  	Format Format `json:"format"`
 337  }
 338  
 339  // ExecutableConfig contains information needed for executable sourced credentials.
 340  type ExecutableConfig struct {
 341  	// Command is the the full command to run to retrieve the subject token.
 342  	// This can include arguments. Must be an absolute path for the program. Required.
 343  	Command string `json:"command"`
 344  	// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
 345  	TimeoutMillis *int `json:"timeout_millis"`
 346  	// OutputFile is the absolute path to the output file where the executable will cache the response.
 347  	// If specified the auth libraries will first check this location before running the executable. Optional.
 348  	OutputFile string `json:"output_file"`
 349  }
 350  
 351  // SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token.
 352  type SubjectTokenSupplier interface {
 353  	// SubjectToken should return a valid subject token or an error.
 354  	// The external account token source does not cache the returned subject token, so caching
 355  	// logic should be implemented in the supplier to prevent multiple requests for the same subject token.
 356  	SubjectToken(ctx context.Context, options SupplierOptions) (string, error)
 357  }
 358  
 359  // AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to
 360  // exchange for a GCP access token.
 361  type AwsSecurityCredentialsSupplier interface {
 362  	// AwsRegion should return the AWS region or an error.
 363  	AwsRegion(ctx context.Context, options SupplierOptions) (string, error)
 364  	// AwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error.
 365  	// The external account token source does not cache the returned security credentials, so caching
 366  	// logic should be implemented in the supplier to prevent multiple requests for the same security credentials.
 367  	AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error)
 368  }
 369  
 370  // SupplierOptions contains information about the requested subject token or AWS security credentials from the
 371  // Google external account credential.
 372  type SupplierOptions struct {
 373  	// Audience is the requested audience for the external account credential.
 374  	Audience string
 375  	// Subject token type is the requested subject token type for the external account credential. Expected values include:
 376  	// “urn:ietf:params:oauth:token-type:jwt”
 377  	// “urn:ietf:params:oauth:token-type:id-token”
 378  	// “urn:ietf:params:oauth:token-type:saml2”
 379  	// “urn:ietf:params:aws:token-type:aws4_request”
 380  	SubjectTokenType string
 381  }
 382  
 383  // tokenURL returns the default STS token endpoint with the configured universe
 384  // domain.
 385  func (c *Config) tokenURL() string {
 386  	if c.UniverseDomain == "" {
 387  		return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
 388  	}
 389  	return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
 390  }
 391  
 392  // parse determines the type of CredentialSource needed.
 393  func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
 394  	//set Defaults
 395  	if c.TokenURL == "" {
 396  		c.TokenURL = c.tokenURL()
 397  	}
 398  	supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
 399  
 400  	if c.AwsSecurityCredentialsSupplier != nil {
 401  		awsCredSource := awsCredentialSource{
 402  			awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier,
 403  			targetResource:                 c.Audience,
 404  			supplierOptions:                supplierOptions,
 405  			ctx:                            ctx,
 406  		}
 407  		return awsCredSource, nil
 408  	} else if c.SubjectTokenSupplier != nil {
 409  		return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil
 410  	} else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
 411  		if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
 412  			if awsVersion != 1 {
 413  				return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion)
 414  			}
 415  
 416  			awsCredSource := awsCredentialSource{
 417  				environmentID:               c.CredentialSource.EnvironmentID,
 418  				regionURL:                   c.CredentialSource.RegionURL,
 419  				regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
 420  				credVerificationURL:         c.CredentialSource.URL,
 421  				targetResource:              c.Audience,
 422  				ctx:                         ctx,
 423  			}
 424  			if c.CredentialSource.IMDSv2SessionTokenURL != "" {
 425  				awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
 426  			}
 427  
 428  			return awsCredSource, nil
 429  		}
 430  	} else if c.CredentialSource.File != "" {
 431  		return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
 432  	} else if c.CredentialSource.URL != "" {
 433  		return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
 434  	} else if c.CredentialSource.Executable != nil {
 435  		return createExecutableCredential(ctx, c.CredentialSource.Executable, c)
 436  	}
 437  	return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source")
 438  }
 439  
 440  type baseCredentialSource interface {
 441  	credentialSourceType() string
 442  	subjectToken() (string, error)
 443  }
 444  
 445  // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
 446  type tokenSource struct {
 447  	ctx  context.Context
 448  	conf *Config
 449  }
 450  
 451  func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
 452  	return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
 453  		goVersion(),
 454  		"unknown",
 455  		credSource.credentialSourceType(),
 456  		conf.ServiceAccountImpersonationURL != "",
 457  		conf.ServiceAccountImpersonationLifetimeSeconds != 0)
 458  }
 459  
 460  // Token allows tokenSource to conform to the oauth2.TokenSource interface.
 461  func (ts tokenSource) Token() (*oauth2.Token, error) {
 462  	conf := ts.conf
 463  
 464  	credSource, err := conf.parse(ts.ctx)
 465  	if err != nil {
 466  		return nil, err
 467  	}
 468  	subjectToken, err := credSource.subjectToken()
 469  
 470  	if err != nil {
 471  		return nil, err
 472  	}
 473  	stsRequest := stsexchange.TokenExchangeRequest{
 474  		GrantType:          "urn:ietf:params:oauth:grant-type:token-exchange",
 475  		Audience:           conf.Audience,
 476  		Scope:              conf.Scopes,
 477  		RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
 478  		SubjectToken:       subjectToken,
 479  		SubjectTokenType:   conf.SubjectTokenType,
 480  	}
 481  	header := make(http.Header)
 482  	header.Add("Content-Type", "application/x-www-form-urlencoded")
 483  	header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
 484  	clientAuth := stsexchange.ClientAuthentication{
 485  		AuthStyle:    oauth2.AuthStyleInHeader,
 486  		ClientID:     conf.ClientID,
 487  		ClientSecret: conf.ClientSecret,
 488  	}
 489  	var options map[string]any
 490  	// Do not pass workforce_pool_user_project when client authentication is used.
 491  	// The client ID is sufficient for determining the user project.
 492  	if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
 493  		options = map[string]any{
 494  			"userProject": conf.WorkforcePoolUserProject,
 495  		}
 496  	}
 497  	stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
 498  	if err != nil {
 499  		return nil, err
 500  	}
 501  
 502  	accessToken := &oauth2.Token{
 503  		AccessToken: stsResp.AccessToken,
 504  		TokenType:   stsResp.TokenType,
 505  	}
 506  
 507  	// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
 508  	if stsResp.ExpiresIn <= 0 {
 509  		return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service")
 510  	}
 511  	accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
 512  
 513  	if stsResp.RefreshToken != "" {
 514  		accessToken.RefreshToken = stsResp.RefreshToken
 515  	}
 516  	return accessToken, nil
 517  }
 518