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