googleauth 1.3.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,11 @@ module Google
30
30
  REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
31
31
  ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
32
32
  PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
33
+ AWS_REGION_VAR = "AWS_REGION".freeze
34
+ AWS_DEFAULT_REGION_VAR = "AWS_DEFAULT_REGION".freeze
35
+ AWS_ACCESS_KEY_ID_VAR = "AWS_ACCESS_KEY_ID".freeze
36
+ AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze
37
+ AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
33
38
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
34
39
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
35
40
  GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
@@ -44,13 +49,6 @@ module Google
44
49
  CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
45
50
  "s.googleusercontent.com".freeze
46
51
 
47
- CLOUD_SDK_CREDENTIALS_WARNING =
48
- "Your application has authenticated using end user credentials from Google Cloud SDK. We recommend that most " \
49
- "server applications use service accounts instead. If your application continues to use end user credentials " \
50
- 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For more information about ' \
51
- "service accounts, see https://cloud.google.com/docs/authentication/. To suppress this message, set the " \
52
- "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
53
-
54
52
  # make_creds proxies the construction of a credentials instance
55
53
  #
56
54
  # By default, it calls #new on the current class, but this behaviour can
@@ -144,12 +142,6 @@ module Google
144
142
 
145
143
  module_function
146
144
 
147
- # Issues warning if cloud sdk client id is used
148
- def warn_if_cloud_sdk_credentials client_id
149
- return if ENV["GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS"]
150
- warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
151
- end
152
-
153
145
  # Finds project_id from gcloud CLI configuration
154
146
  def load_gcloud_project_id
155
147
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
@@ -18,6 +18,7 @@ require "stringio"
18
18
  require "googleauth/credentials_loader"
19
19
  require "googleauth/service_account"
20
20
  require "googleauth/user_refresh"
21
+ require "googleauth/external_account"
21
22
 
22
23
  module Google
23
24
  # Module Auth provides classes that provide Google-specific authorization
@@ -34,11 +35,9 @@ module Google
34
35
  json_key_io = options[:json_key_io]
35
36
  if json_key_io
36
37
  json_key, clz = determine_creds_class json_key_io
37
- warn_if_cloud_sdk_credentials json_key["client_id"]
38
38
  io = StringIO.new MultiJson.dump(json_key)
39
39
  clz.make_creds options.merge(json_key_io: io)
40
40
  else
41
- warn_if_cloud_sdk_credentials ENV[CredentialsLoader::CLIENT_ID_VAR]
42
41
  clz = read_creds
43
42
  clz.make_creds options
44
43
  end
@@ -53,6 +52,8 @@ module Google
53
52
  ServiceAccountCredentials
54
53
  when "authorized_user"
55
54
  UserRefreshCredentials
55
+ when "external_account"
56
+ ExternalAccount::Credentials
56
57
  else
57
58
  raise "credentials type '#{type}' is not supported"
58
59
  end
@@ -69,6 +70,8 @@ module Google
69
70
  [json_key, ServiceAccountCredentials]
70
71
  when "authorized_user"
71
72
  [json_key, UserRefreshCredentials]
73
+ when "external_account"
74
+ [json_key, ExternalAccount::Credentials]
72
75
  else
73
76
  raise "credentials type '#{type}' is not supported"
74
77
  end
@@ -0,0 +1,378 @@
1
+ # Copyright 2023 Google, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "time"
16
+ require "googleauth/external_account/base_credentials"
17
+ require "googleauth/external_account/external_account_utils"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
21
+ module Auth
22
+ # Authenticates requests using External Account credentials, such as those provided by the AWS provider.
23
+ module ExternalAccount
24
+ # This module handles the retrieval of credentials from Google Cloud by utilizing the AWS EC2 metadata service and
25
+ # then exchanging the credentials for a short-lived Google Cloud access token.
26
+ class AwsCredentials
27
+ # Constant for imdsv2 session token expiration in seconds
28
+ IMDSV2_TOKEN_EXPIRATION_IN_SECONDS = 300
29
+
30
+ include Google::Auth::ExternalAccount::BaseCredentials
31
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
32
+ extend CredentialsLoader
33
+
34
+ # Will always be nil, but method still gets used.
35
+ attr_reader :client_id
36
+
37
+ def initialize options = {}
38
+ base_setup options
39
+
40
+ @audience = options[:audience]
41
+ @credential_source = options[:credential_source] || {}
42
+ @environment_id = @credential_source[:environment_id]
43
+ @region_url = @credential_source[:region_url]
44
+ @credential_verification_url = @credential_source[:url]
45
+ @regional_cred_verification_url = @credential_source[:regional_cred_verification_url]
46
+ @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url]
47
+
48
+ # These will be lazily loaded when needed, or will raise an error if not provided
49
+ @region = nil
50
+ @request_signer = nil
51
+ @imdsv2_session_token = nil
52
+ @imdsv2_session_token_expiry = nil
53
+ end
54
+
55
+ # Retrieves the subject token using the credential_source object.
56
+ # The subject token is a serialized [AWS GetCallerIdentity signed request](
57
+ # https://cloud.google.com/iam/docs/access-resources-aws#exchange-token).
58
+ #
59
+ # The logic is summarized as:
60
+ #
61
+ # Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS
62
+ # metadata server availability-zone if not found in the environment variable.
63
+ #
64
+ # Check AWS credentials in environment variables. If not found, retrieve from the AWS metadata server
65
+ # security-credentials endpoint.
66
+ #
67
+ # When retrieving AWS credentials from the metadata server security-credentials endpoint, the AWS role needs to
68
+ # be determined by # calling the security-credentials endpoint without any argument.
69
+ # Then the credentials can be retrieved via: security-credentials/role_name
70
+ #
71
+ # Generate the signed request to AWS STS GetCallerIdentity action.
72
+ #
73
+ # Inject x-goog-cloud-target-resource into header and serialize the signed request.
74
+ # This will be the subject-token to pass to GCP STS.
75
+ #
76
+ # @return [string] The retrieved subject token.
77
+ #
78
+ def retrieve_subject_token!
79
+ if @request_signer.nil?
80
+ @region = region
81
+ @request_signer = AwsRequestSigner.new @region
82
+ end
83
+
84
+ request = {
85
+ method: "POST",
86
+ url: @regional_cred_verification_url.sub("{region}", @region)
87
+ }
88
+
89
+ request_options = @request_signer.generate_signed_request fetch_security_credentials, request
90
+
91
+ request_headers = request_options[:headers]
92
+ request_headers["x-goog-cloud-target-resource"] = @audience
93
+
94
+ aws_signed_request = {
95
+ headers: [],
96
+ method: request_options[:method],
97
+ url: request_options[:url]
98
+ }
99
+
100
+ aws_signed_request[:headers] = request_headers.keys.sort.map do |key|
101
+ { key: key, value: request_headers[key] }
102
+ end
103
+
104
+ uri_escape aws_signed_request.to_json
105
+ end
106
+
107
+ private
108
+
109
+ def imdsv2_session_token
110
+ return @imdsv2_session_token unless imdsv2_session_token_invalid?
111
+ raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
112
+ begin
113
+ response = connection.put @imdsv2_session_token_url do |req|
114
+ req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
115
+ end
116
+ rescue Faraday::Error => e
117
+ raise "Fetching AWS IMDSV2 token error: #{e}"
118
+ end
119
+ raise Faraday::Error unless response.success?
120
+ @imdsv2_session_token = response.body
121
+ @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
122
+ @imdsv2_session_token
123
+ end
124
+
125
+ def imdsv2_session_token_invalid?
126
+ return true if @imdsv2_session_token.nil?
127
+ @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
128
+ end
129
+
130
+ def get_aws_resource url, name, data: nil, headers: {}
131
+ begin
132
+ headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
133
+ response = if data
134
+ headers["Content-Type"] = "application/json"
135
+ connection.post url, data, headers
136
+ else
137
+ connection.get url, nil, headers
138
+ end
139
+
140
+ raise Faraday::Error unless response.success?
141
+ response
142
+ rescue Faraday::Error
143
+ raise "Failed to retrieve AWS #{name}."
144
+ end
145
+ end
146
+
147
+ def uri_escape string
148
+ if string.nil?
149
+ nil
150
+ else
151
+ CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~")
152
+ end
153
+ end
154
+
155
+ # Retrieves the AWS security credentials required for signing AWS requests from either the AWS security
156
+ # credentials environment variables or from the AWS metadata server.
157
+ def fetch_security_credentials
158
+ env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR]
159
+ env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR]
160
+ # This is normally not available for permanent credentials.
161
+ env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR]
162
+
163
+ if env_aws_access_key_id && env_aws_secret_access_key
164
+ return {
165
+ access_key_id: env_aws_access_key_id,
166
+ secret_access_key: env_aws_secret_access_key,
167
+ session_token: env_aws_session_token
168
+ }
169
+ end
170
+
171
+ role_name = fetch_metadata_role_name
172
+ credentials = fetch_metadata_security_credentials role_name
173
+
174
+ {
175
+ access_key_id: credentials["AccessKeyId"],
176
+ secret_access_key: credentials["SecretAccessKey"],
177
+ session_token: credentials["Token"]
178
+ }
179
+ end
180
+
181
+ # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
182
+ # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
183
+ # credentials needed to sign requests to AWS APIs.
184
+ def fetch_metadata_role_name
185
+ unless @credential_verification_url
186
+ raise "Unable to determine the AWS metadata server security credentials endpoint"
187
+ end
188
+
189
+ get_aws_resource(@credential_verification_url, "IAM Role").body
190
+ end
191
+
192
+ # Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.
193
+ def fetch_metadata_security_credentials role_name
194
+ response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
195
+ MultiJson.load response.body
196
+ end
197
+
198
+ def region
199
+ @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
200
+
201
+ unless @region
202
+ raise "region_url or region must be set for external account credentials" unless @region_url
203
+
204
+ @region ||= get_aws_resource(@region_url, "region").body[0..-2]
205
+ end
206
+
207
+ @region
208
+ end
209
+ end
210
+
211
+ # Implements an AWS request signer based on the AWS Signature Version 4 signing process.
212
+ # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
213
+ class AwsRequestSigner
214
+ # Instantiates an AWS request signer used to compute authenticated signed requests to AWS APIs based on the AWS
215
+ # Signature Version 4 signing process.
216
+ #
217
+ # @param [string] region_name
218
+ # The AWS region to use.
219
+ def initialize region_name
220
+ @region_name = region_name
221
+ end
222
+
223
+ # Generates the signed request for the provided HTTP request for calling
224
+ # an AWS API. This follows the steps described at:
225
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
226
+ #
227
+ # @param [Hash[string, string]] aws_security_credentials
228
+ # A dictionary containing the AWS security credentials.
229
+ # @param [string] url
230
+ # The AWS service URL containing the canonical URI and query string.
231
+ # @param [string] method
232
+ # The HTTP method used to call this API.
233
+ #
234
+ # @return [hash{string => string}]
235
+ # The AWS signed request dictionary object.
236
+ #
237
+ def generate_signed_request aws_credentials, original_request
238
+ uri = Addressable::URI.parse original_request[:url]
239
+ raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
240
+ service_name = uri.host.split(".").first
241
+
242
+ datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
243
+ date = datetime[0, 8]
244
+
245
+ headers = aws_headers aws_credentials, original_request, datetime
246
+
247
+ request_payload = original_request[:data] || ""
248
+ content_sha256 = sha256_hexdigest request_payload
249
+
250
+ canonical_req = canonical_request original_request[:method], uri, headers, content_sha256
251
+ sts = string_to_sign datetime, canonical_req, service_name
252
+
253
+ # Authorization header requires everything else to be properly setup in order to be properly
254
+ # calculated.
255
+ headers["Authorization"] = build_authorization_header headers, sts, aws_credentials, service_name, date
256
+
257
+ {
258
+ url: uri.to_s,
259
+ headers: headers,
260
+ method: original_request[:method],
261
+ data: (request_payload unless request_payload.empty?)
262
+ }.compact
263
+ end
264
+
265
+ private
266
+
267
+ def aws_headers aws_credentials, original_request, datetime
268
+ uri = Addressable::URI.parse original_request[:url]
269
+ temp_headers = original_request[:headers] || {}
270
+ headers = {}
271
+ temp_headers.each_key { |k| headers[k.to_s] = temp_headers[k] }
272
+ headers["host"] = uri.host
273
+ headers["x-amz-date"] = datetime
274
+ headers["x-amz-security-token"] = aws_credentials[:session_token] if aws_credentials[:session_token]
275
+ headers
276
+ end
277
+
278
+ def build_authorization_header headers, sts, aws_credentials, service_name, date
279
+ [
280
+ "AWS4-HMAC-SHA256",
281
+ "Credential=#{credential aws_credentials[:access_key_id], date, service_name},",
282
+ "SignedHeaders=#{headers.keys.sort.join ';'},",
283
+ "Signature=#{signature aws_credentials[:secret_access_key], date, sts, service_name}"
284
+ ].join(" ")
285
+ end
286
+
287
+ def signature secret_access_key, date, string_to_sign, service
288
+ k_date = hmac "AWS4#{secret_access_key}", date
289
+ k_region = hmac k_date, @region_name
290
+ k_service = hmac k_region, service
291
+ k_credentials = hmac k_service, "aws4_request"
292
+
293
+ hexhmac k_credentials, string_to_sign
294
+ end
295
+
296
+ def hmac key, value
297
+ OpenSSL::HMAC.digest OpenSSL::Digest.new("sha256"), key, value
298
+ end
299
+
300
+ def hexhmac key, value
301
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest.new("sha256"), key, value
302
+ end
303
+
304
+ def credential access_key_id, date, service
305
+ "#{access_key_id}/#{credential_scope date, service}"
306
+ end
307
+
308
+ def credential_scope date, service
309
+ [
310
+ date,
311
+ @region_name,
312
+ service,
313
+ "aws4_request"
314
+ ].join("/")
315
+ end
316
+
317
+ def string_to_sign datetime, canonical_request, service
318
+ [
319
+ "AWS4-HMAC-SHA256",
320
+ datetime,
321
+ credential_scope(datetime[0, 8], service),
322
+ sha256_hexdigest(canonical_request)
323
+ ].join("\n")
324
+ end
325
+
326
+ def host uri
327
+ # Handles known and unknown URI schemes; default_port nil when unknown.
328
+ if uri.default_port == uri.port
329
+ uri.host
330
+ else
331
+ "#{uri.host}:#{uri.port}"
332
+ end
333
+ end
334
+
335
+ def canonical_request http_method, uri, headers, content_sha256
336
+ headers = headers.sort_by(&:first) # transforms to a sorted array of [key, value]
337
+
338
+ [
339
+ http_method,
340
+ uri.path.empty? ? "/" : uri.path,
341
+ build_canonical_querystring(uri.query || ""),
342
+ headers.map { |k, v| "#{k}:#{v}\n" }.join, # Canonical headers
343
+ headers.map(&:first).join(";"), # Signed headers
344
+ content_sha256
345
+ ].join("\n")
346
+ end
347
+
348
+ def sha256_hexdigest string
349
+ OpenSSL::Digest::SHA256.hexdigest string
350
+ end
351
+
352
+ # Generates the canonical query string given a raw query string.
353
+ # Logic is based on
354
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
355
+ # Code is from the AWS SDK for Ruby
356
+ # https://github.com/aws/aws-sdk-ruby/blob/0ac3d0a393ed216290bfb5f0383380376f6fb1f1/gems/aws-sigv4/lib/aws-sigv4/signer.rb#L532
357
+ def build_canonical_querystring query
358
+ params = query.split "&"
359
+ params = params.map { |p| p.include?("=") ? p : "#{p}=" }
360
+
361
+ params.each.with_index.sort do |(a, a_offset), (b, b_offset)|
362
+ a_name, a_value = a.split "="
363
+ b_name, b_value = b.split "="
364
+ if a_name == b_name
365
+ if a_value == b_value
366
+ a_offset <=> b_offset
367
+ else
368
+ a_value <=> b_value
369
+ end
370
+ else
371
+ a_name <=> b_name
372
+ end
373
+ end.map(&:first).join("&")
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,159 @@
1
+ # Copyright 2023 Google, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.require "time"
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/helpers/connection"
17
+ require "googleauth/oauth2/sts_client"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization
21
+ # used to access Google APIs.
22
+ module Auth
23
+ module ExternalAccount
24
+ # Authenticates requests using External Account credentials, such
25
+ # as those provided by the AWS provider or OIDC provider like Azure, etc.
26
+ module BaseCredentials
27
+ # Contains all methods needed for all external account credentials.
28
+ # Other credentials should call `base_setup` during initialization
29
+ # And should define the :retrieve_subject_token! method
30
+
31
+ # External account JSON type identifier.
32
+ EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze
33
+ # The token exchange grant_type used for exchanging credentials.
34
+ STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
35
+ # The token exchange requested_token_type. This is always an access_token.
36
+ STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
37
+ # Default IAM_SCOPE
38
+ IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
39
+
40
+ include Google::Auth::BaseClient
41
+ include Helpers::Connection
42
+
43
+ attr_reader :expires_at
44
+ attr_accessor :access_token
45
+ attr_accessor :universe_domain
46
+
47
+ def expires_within? seconds
48
+ # This method is needed for BaseClient
49
+ @expires_at && @expires_at - Time.now.utc < seconds
50
+ end
51
+
52
+ def expires_at= new_expires_at
53
+ @expires_at = normalize_timestamp new_expires_at
54
+ end
55
+
56
+ def fetch_access_token! _options = {}
57
+ # This method is needed for BaseClient
58
+ response = exchange_token
59
+
60
+ if @service_account_impersonation_url
61
+ impersonated_response = get_impersonated_access_token response["access_token"]
62
+ self.expires_at = impersonated_response["expireTime"]
63
+ self.access_token = impersonated_response["accessToken"]
64
+ else
65
+ # Extract the expiration time in seconds from the response and calculate the actual expiration time
66
+ # and then save that to the expiry variable.
67
+ self.expires_at = Time.now.utc + response["expires_in"].to_i
68
+ self.access_token = response["access_token"]
69
+ end
70
+
71
+ notify_refresh_listeners
72
+ end
73
+
74
+ # Retrieves the subject token using the credential_source object.
75
+ # @return [string]
76
+ # The retrieved subject token.
77
+ #
78
+ def retrieve_subject_token!
79
+ raise NotImplementedError
80
+ end
81
+
82
+ # Returns whether the credentials represent a workforce pool (True) or
83
+ # workload (False) based on the credentials' audience.
84
+ #
85
+ # @return [bool]
86
+ # true if the credentials represent a workforce pool.
87
+ # false if they represent a workload.
88
+ def is_workforce_pool?
89
+ %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
+ end
91
+
92
+ private
93
+
94
+ def token_type
95
+ # This method is needed for BaseClient
96
+ :access_token
97
+ end
98
+
99
+ def base_setup options
100
+ self.default_connection = options[:connection]
101
+
102
+ @audience = options[:audience]
103
+ @scope = options[:scope] || IAM_SCOPE
104
+ @subject_token_type = options[:subject_token_type]
105
+ @token_url = options[:token_url]
106
+ @token_info_url = options[:token_info_url]
107
+ @service_account_impersonation_url = options[:service_account_impersonation_url]
108
+ @service_account_impersonation_options = options[:service_account_impersonation_options] || {}
109
+ @client_id = options[:client_id]
110
+ @client_secret = options[:client_secret]
111
+ @quota_project_id = options[:quota_project_id]
112
+ @project_id = nil
113
+ @workforce_pool_user_project = options[:workforce_pool_user_project]
114
+ @universe_domain = options[:universe_domain] || "googleapis.com"
115
+
116
+ @expires_at = nil
117
+ @access_token = nil
118
+
119
+ @sts_client = Google::Auth::OAuth2::STSClient.new(
120
+ token_exchange_endpoint: @token_url,
121
+ connection: default_connection
122
+ )
123
+ return unless @workforce_pool_user_project && !is_workforce_pool?
124
+ raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
125
+ end
126
+
127
+ def exchange_token
128
+ additional_options = nil
129
+ if @client_id.nil? && @workforce_pool_user_project
130
+ additional_options = { userProject: @workforce_pool_user_project }
131
+ end
132
+ @sts_client.exchange_token(
133
+ audience: @audience,
134
+ grant_type: STS_GRANT_TYPE,
135
+ subject_token: retrieve_subject_token!,
136
+ subject_token_type: @subject_token_type,
137
+ scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
138
+ requested_token_type: STS_REQUESTED_TOKEN_TYPE,
139
+ additional_options: additional_options
140
+ )
141
+ end
142
+
143
+ def get_impersonated_access_token token, _options = {}
144
+ response = connection.post @service_account_impersonation_url do |req|
145
+ req.headers["Authorization"] = "Bearer #{token}"
146
+ req.headers["Content-Type"] = "application/json"
147
+ req.body = MultiJson.dump({ scope: @scope })
148
+ end
149
+
150
+ if response.status != 200
151
+ raise "Service account impersonation failed with status #{response.status}"
152
+ end
153
+
154
+ MultiJson.load response.body
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end