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