googleauth 1.11.1 → 1.13.1

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.
@@ -0,0 +1,282 @@
1
+ # Copyright 2024 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 "googleauth/signet"
16
+ require "googleauth/base_client"
17
+ require "googleauth/helpers/connection"
18
+
19
+ module Google
20
+ module Auth
21
+ # Authenticates requests using impersonation from base credentials.
22
+ # This is a two-step process: first authentication claim from the base credentials is created
23
+ # and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
24
+ # The short-lived token and its expiration time are cached.
25
+ class ImpersonatedServiceAccountCredentials
26
+ # @private
27
+ ERROR_SUFFIX = <<~ERROR.freeze
28
+ when trying to get security access token
29
+ from IAM Credentials endpoint using the credentials provided.
30
+ ERROR
31
+
32
+ # @private
33
+ IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
34
+
35
+ # BaseClient most importantly implements the `:updater_proc` getter,
36
+ # that returns a reference to an `apply!` method that updates
37
+ # a hash argument provided with the authorization header containing
38
+ # the access token (impersonation token in this case).
39
+ include Google::Auth::BaseClient
40
+
41
+ include Helpers::Connection
42
+
43
+ # @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens
44
+ attr_reader :base_credentials
45
+
46
+ # @return [Object] The modified version of base credentials, tailored for impersonation purposes
47
+ # with necessary scope adjustments
48
+ attr_reader :source_credentials
49
+
50
+ # @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific
51
+ # format to specify the impersonated service account.
52
+ attr_reader :impersonation_url
53
+
54
+ # @return [Array<String>, String] The scope(s) required for the impersonated access token,
55
+ # indicating the permissions needed for the short-lived token
56
+ attr_reader :scope
57
+
58
+ # @return [String, nil] The short-lived impersonation access token, retrieved and cached
59
+ # after making the impersonation request
60
+ attr_reader :access_token
61
+
62
+ # @return [Time, nil] The expiration time of the current access token, used to determine
63
+ # if the token is still valid
64
+ attr_reader :expires_at
65
+
66
+ # Create a ImpersonatedServiceAccountCredentials
67
+ # When you use service account impersonation, you start with an authenticated principal
68
+ # (e.g. your user account or a service account)
69
+ # and request short-lived credentials for a service account
70
+ # that has the authorization that your use case requires.
71
+ #
72
+ # @param options [Hash] A hash of options to configure the credentials.
73
+ # @option options [Object] :base_credentials (required) The authenticated principal.
74
+ # It will be used as following:
75
+ # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
76
+ # * as source credentials otherwise.
77
+ # @option options [String] :impersonation_url (required) The URL to impersonate the service account.
78
+ # This URL should follow the format:
79
+ # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
80
+ # where:
81
+ # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
82
+ # - `{source_sa_email}` is the email address of the service account to impersonate.
83
+ # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
84
+ # defining the permissions required for the token.
85
+ # @option options [Object] :source_credentials The authenticated principal that will be used
86
+ # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
87
+ #
88
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
89
+ def self.make_creds options = {}
90
+ new options
91
+ end
92
+
93
+ # Initializes a new instance of ImpersonatedServiceAccountCredentials.
94
+ #
95
+ # @param options [Hash] A hash of options to configure the credentials.
96
+ # @option options [Object] :base_credentials (required) The authenticated principal.
97
+ # It will be used as following:
98
+ # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
99
+ # * as source credentials otherwise.
100
+ # @option options [String] :impersonation_url (required) The URL to impersonate the service account.
101
+ # This URL should follow the format:
102
+ # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
103
+ # where:
104
+ # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
105
+ # - `{source_sa_email}` is the email address of the service account to impersonate.
106
+ # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
107
+ # defining the permissions required for the token.
108
+ # @option options [Object] :source_credentials The authenticated principal that will be used
109
+ # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
110
+ # It is redundant to provide both source and base credentials as only source will be used,
111
+ # but it can be done, e.g. when duplicating existing credentials.
112
+ #
113
+ # @raise [ArgumentError] If any of the required options are missing.
114
+ #
115
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
116
+ def initialize options = {}
117
+ @base_credentials, @impersonation_url, @scope =
118
+ options.values_at :base_credentials,
119
+ :impersonation_url,
120
+ :scope
121
+
122
+ # Fail-fast checks for required parameters
123
+ if @base_credentials.nil? && !options.key?(:source_credentials)
124
+ raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials"
125
+ end
126
+ raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil?
127
+ raise ArgumentError, "Missing required option: :scope" if @scope.nil?
128
+
129
+ # Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state
130
+ # (e.g. refresh status) as part of themselves
131
+ # so a copy needs to be created with the scope overriden and transient state dropped.
132
+ #
133
+ # If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope.
134
+ # This might result in an "access denied" error downstream when the token from that credentials is being used
135
+ # for the token exchange.
136
+ @source_credentials = if options.key? :source_credentials
137
+ options[:source_credentials]
138
+ elsif @base_credentials.respond_to? :duplicate
139
+ @base_credentials.duplicate({
140
+ scope: IAM_SCOPE
141
+ })
142
+ else
143
+ @base_credentials
144
+ end
145
+ end
146
+
147
+ # Determines whether the current access token expires within the specified number of seconds.
148
+ #
149
+ # @param seconds [Integer] The number of seconds to check against the token's expiration time.
150
+ #
151
+ # @return [Boolean] Whether the access token expires within the given time frame
152
+ def expires_within? seconds
153
+ # This method is needed for BaseClient
154
+ @expires_at && @expires_at - Time.now.utc < seconds
155
+ end
156
+
157
+ # The universe domain of the impersonated credentials.
158
+ # Effectively this retrieves the universe domain of the source credentials.
159
+ #
160
+ # @return [String] The universe domain of the credentials.
161
+ def universe_domain
162
+ @source_credentials.universe_domain
163
+ end
164
+
165
+ # @return [Logger, nil] The logger of the credentials.
166
+ def logger
167
+ @source_credentials.logger if source_credentials.respond_to? :logger
168
+ end
169
+
170
+ # Creates a duplicate of these credentials without transient token state
171
+ #
172
+ # @param options [Hash] Overrides for the credentials parameters.
173
+ # The following keys are recognized
174
+ # * `base_credentials` the base credentials used to initialize the impersonation
175
+ # * `source_credentials` the authenticated credentials which usually would be
176
+ # base credentials with scope overridden to IAM_SCOPE
177
+ # * `impersonation_url` the URL to use to make an impersonation token exchange
178
+ # * `scope` the scope(s) to access
179
+ #
180
+ # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
181
+ def duplicate options = {}
182
+ options = deep_hash_normalize options
183
+
184
+ options = {
185
+ base_credentials: @base_credentials,
186
+ source_credentials: @source_credentials,
187
+ impersonation_url: @impersonation_url,
188
+ scope: @scope
189
+ }.merge(options)
190
+
191
+ self.class.new options
192
+ end
193
+
194
+ private
195
+
196
+ # Generates a new impersonation access token by exchanging the source credentials' token
197
+ # at the impersonation URL.
198
+ #
199
+ # This method first fetches an access token from the source credentials and then exchanges it
200
+ # for an impersonation token using the specified impersonation URL. The generated token and
201
+ # its expiration time are cached for subsequent use.
202
+ #
203
+ # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
204
+ #
205
+ # @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
206
+ # @raise [Signet::AuthorizationError] For other unexpected response statuses.
207
+ #
208
+ # @return [String] The newly generated impersonation access token.
209
+ def fetch_access_token! _options = {}
210
+ auth_header = {}
211
+ auth_header = @source_credentials.updater_proc.call auth_header
212
+
213
+ resp = connection.post @impersonation_url do |req|
214
+ req.headers.merge! auth_header
215
+ req.headers["Content-Type"] = "application/json"
216
+ req.body = MultiJson.dump({ scope: @scope })
217
+ end
218
+
219
+ case resp.status
220
+ when 200
221
+ response = MultiJson.load resp.body
222
+ self.expires_at = response["expireTime"]
223
+ @access_token = response["accessToken"]
224
+ access_token
225
+ when 403, 500
226
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
227
+ raise Signet::UnexpectedStatusError, msg
228
+ else
229
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
230
+ raise Signet::AuthorizationError, msg
231
+ end
232
+ end
233
+
234
+ # Setter for the expires_at value that makes sure it is converted
235
+ # to Time object.
236
+ def expires_at= new_expires_at
237
+ @expires_at = normalize_timestamp new_expires_at
238
+ end
239
+
240
+ # Returns the type of token (access_token).
241
+ # This method is needed for BaseClient.
242
+ def token_type
243
+ :access_token
244
+ end
245
+
246
+ # Normalizes a timestamp to a Time object.
247
+ #
248
+ # @param time [Time, String, nil] The timestamp to normalize.
249
+ #
250
+ # @return [Time, nil] The normalized Time object, or nil if the input is nil.
251
+ #
252
+ # @raise [RuntimeError] If the input is not a Time, String, or nil.
253
+ def normalize_timestamp time
254
+ case time
255
+ when NilClass
256
+ nil
257
+ when Time
258
+ time
259
+ when String
260
+ Time.parse time
261
+ else
262
+ raise "Invalid time value #{time}"
263
+ end
264
+ end
265
+
266
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
267
+ def recursive_hash_normalize_keys val
268
+ if val.is_a? Hash
269
+ deep_hash_normalize val
270
+ else
271
+ val
272
+ end
273
+ end
274
+
275
+ def deep_hash_normalize old_hash
276
+ sym_hash = {}
277
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
278
+ sym_hash
279
+ end
280
+ end
281
+ end
282
+ end
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "google/logging/message"
15
16
  require "googleauth/signet"
16
17
  require "googleauth/credentials_loader"
17
18
  require "googleauth/json_key_reader"
@@ -80,6 +81,30 @@ module Google
80
81
  .configure_connection(options)
81
82
  end
82
83
 
84
+ # Creates a duplicate of these credentials
85
+ # without the Signet::OAuth2::Client-specific
86
+ # transient state (e.g. cached tokens)
87
+ #
88
+ # @param options [Hash] Overrides for the credentials parameters.
89
+ # The following keys are recognized in addition to keys in the
90
+ # Signet::OAuth2::Client
91
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
92
+ # be used for the authentication
93
+ # * `project_id` the project id to use during the authentication
94
+ # * `quota_project_id` the quota project id to use
95
+ # during the authentication
96
+ def duplicate options = {}
97
+ options = deep_hash_normalize options
98
+ super(
99
+ {
100
+ enable_self_signed_jwt: @enable_self_signed_jwt,
101
+ project_id: project_id,
102
+ quota_project_id: quota_project_id,
103
+ logger: logger
104
+ }.merge(options)
105
+ )
106
+ end
107
+
83
108
  # Handles certain escape sequences that sometimes appear in input.
84
109
  # Specifically, interprets the "\n" sequence for newline, and removes
85
110
  # enclosing quotes.
@@ -111,6 +136,32 @@ module Google
111
136
  super && !enable_self_signed_jwt?
112
137
  end
113
138
 
139
+ # Destructively updates these credentials
140
+ #
141
+ # This method is called by `Signet::OAuth2::Client`'s constructor
142
+ #
143
+ # @param options [Hash] Overrides for the credentials parameters.
144
+ # The following keys are recognized in addition to keys in the
145
+ # Signet::OAuth2::Client
146
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
147
+ # be used for the authentication
148
+ # * `project_id` the project id to use during the authentication
149
+ # * `quota_project_id` the quota project id to use
150
+ # during the authentication
151
+ # @return [Google::Auth::ServiceAccountCredentials]
152
+ def update! options = {}
153
+ # Normalize all keys to symbols to allow indifferent access.
154
+ options = deep_hash_normalize options
155
+
156
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
157
+ @project_id = options[:project_id] if options.key? :project_id
158
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
159
+
160
+ super(options)
161
+
162
+ self
163
+ end
164
+
114
165
  private
115
166
 
116
167
  def apply_self_signed_jwt! a_hash
@@ -123,6 +174,7 @@ module Google
123
174
  }
124
175
  key_io = StringIO.new MultiJson.dump(cred_json)
125
176
  alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope
177
+ alt.logger = logger
126
178
  alt.apply! a_hash
127
179
  end
128
180
  end
@@ -142,11 +194,14 @@ module Google
142
194
  TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
143
195
  SIGNING_ALGORITHM = "RS256".freeze
144
196
  EXPIRY = 60
197
+
145
198
  extend CredentialsLoader
146
199
  extend JsonKeyReader
200
+
147
201
  attr_reader :project_id
148
202
  attr_reader :quota_project_id
149
203
  attr_accessor :universe_domain
204
+ attr_accessor :logger
150
205
 
151
206
  # Create a ServiceAccountJwtHeaderCredentials.
152
207
  #
@@ -166,16 +221,43 @@ module Google
166
221
  @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
167
222
  self.class.read_json_key json_key_io
168
223
  else
169
- @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
170
- @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
171
- @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
172
- @quota_project_id = nil
173
- @universe_domain = nil
224
+ @private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR]
225
+ @issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
226
+ @project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR]
227
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
228
+ @universe_domain = options[:universe_domain] if options.key? :universe_domain
174
229
  end
175
230
  @universe_domain ||= "googleapis.com"
176
231
  @project_id ||= CredentialsLoader.load_gcloud_project_id
177
232
  @signing_key = OpenSSL::PKey::RSA.new @private_key
178
- @scope = options[:scope]
233
+ @scope = options[:scope] if options.key? :scope
234
+ @logger = options[:logger] if options.key? :scope
235
+ end
236
+
237
+ # Creates a duplicate of these credentials
238
+ #
239
+ # @param options [Hash] Overrides for the credentials parameters.
240
+ # The following keys are recognized
241
+ # * `private key` the private key in string form
242
+ # * `issuer` the SA issuer
243
+ # * `scope` the scope(s) to access
244
+ # * `project_id` the project id to use during the authentication
245
+ # * `quota_project_id` the quota project id to use
246
+ # * `universe_domain` the universe domain of the credentials
247
+ def duplicate options = {}
248
+ options = deep_hash_normalize options
249
+
250
+ options = {
251
+ private_key: @private_key,
252
+ issuer: @issuer,
253
+ scope: @scope,
254
+ project_id: project_id,
255
+ quota_project_id: quota_project_id,
256
+ universe_domain: universe_domain,
257
+ logger: logger
258
+ }.merge(options)
259
+
260
+ self.class.new options
179
261
  end
180
262
 
181
263
  # Construct a jwt token if the JWT_AUD_URI key is present in the input
@@ -187,10 +269,14 @@ module Google
187
269
  return a_hash if jwt_aud_uri.nil? && @scope.nil?
188
270
  jwt_token = new_jwt_token jwt_aud_uri, opts
189
271
  a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
272
+ logger&.debug do
273
+ hash = Digest::SHA256.hexdigest jwt_token
274
+ Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})"
275
+ end
190
276
  a_hash
191
277
  end
192
278
 
193
- # Returns a clone of a_hash updated with the authoriation header
279
+ # Returns a clone of a_hash updated with the authorization header
194
280
  def apply a_hash, opts = {}
195
281
  a_copy = a_hash.clone
196
282
  apply! a_copy, opts
@@ -219,6 +305,10 @@ module Google
219
305
  assertion["scope"] = Array(@scope).join " " if @scope
220
306
  assertion["aud"] = jwt_aud_uri if jwt_aud_uri
221
307
 
308
+ logger&.debug do
309
+ Google::Logging::Message.from message: "JWT assertion: #{assertion}"
310
+ end
311
+
222
312
  JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
223
313
  end
224
314
 
@@ -226,6 +316,23 @@ module Google
226
316
  def needs_access_token?
227
317
  false
228
318
  end
319
+
320
+ private
321
+
322
+ def deep_hash_normalize old_hash
323
+ sym_hash = {}
324
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
325
+ sym_hash
326
+ end
327
+
328
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
329
+ def recursive_hash_normalize_keys val
330
+ if val.is_a? Hash
331
+ deep_hash_normalize val
332
+ else
333
+ val
334
+ end
335
+ end
229
336
  end
230
337
  end
231
338
  end
@@ -38,6 +38,22 @@ module Signet
38
38
  self
39
39
  end
40
40
 
41
+ alias update_signet_base update!
42
+ def update! options = {}
43
+ # Normalize all keys to symbols to allow indifferent access.
44
+ options = deep_hash_normalize options
45
+
46
+ # This `update!` method "overide" adds the `@logger`` update and
47
+ # the `universe_domain` update.
48
+ #
49
+ # The `universe_domain` is also updated in `update_token!` but is
50
+ # included here for completeness
51
+ self.universe_domain = options[:universe_domain] if options.key? :universe_domain
52
+ @logger = options[:logger] if options.key? :logger
53
+
54
+ update_signet_base options
55
+ end
56
+
41
57
  def configure_connection options
42
58
  @connection_info =
43
59
  options[:connection_builder] || options[:default_connection]
@@ -65,6 +81,24 @@ module Signet
65
81
  info
66
82
  end
67
83
 
84
+ alias googleauth_orig_generate_access_token_request generate_access_token_request
85
+ def generate_access_token_request options = {}
86
+ parameters = googleauth_orig_generate_access_token_request options
87
+ logger&.info do
88
+ Google::Logging::Message.from(
89
+ message: "Requesting access token from #{parameters['grant_type']}",
90
+ "credentialsId" => object_id
91
+ )
92
+ end
93
+ logger&.debug do
94
+ Google::Logging::Message.from(
95
+ message: "Token fetch params: #{parameters}",
96
+ "credentialsId" => object_id
97
+ )
98
+ end
99
+ parameters
100
+ end
101
+
68
102
  def build_default_connection
69
103
  if !defined?(@connection_info)
70
104
  nil
@@ -79,21 +113,62 @@ module Signet
79
113
  retry_count = 0
80
114
 
81
115
  begin
82
- yield
116
+ yield.tap { |resp| log_response resp }
83
117
  rescue StandardError => e
84
- raise e if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
118
+ if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
119
+ log_auth_error e
120
+ raise e
121
+ end
85
122
 
86
123
  if retry_count < max_retry_count
124
+ log_transient_error e
87
125
  retry_count += 1
88
126
  sleep retry_count * 0.3
89
127
  retry
90
128
  else
129
+ log_retries_exhausted e
91
130
  msg = "Unexpected error: #{e.inspect}"
92
131
  raise Signet::AuthorizationError, msg
93
132
  end
94
133
  end
95
134
  end
96
135
 
136
+ # Creates a duplicate of these credentials
137
+ # without the Signet::OAuth2::Client-specific
138
+ # transient state (e.g. cached tokens)
139
+ #
140
+ # @param options [Hash] Overrides for the credentials parameters.
141
+ # @see Signet::OAuth2::Client#update!
142
+ def duplicate options = {}
143
+ options = deep_hash_normalize options
144
+
145
+ opts = {
146
+ authorization_uri: @authorization_uri,
147
+ token_credential_uri: @token_credential_uri,
148
+ client_id: @client_id,
149
+ client_secret: @client_secret,
150
+ scope: @scope,
151
+ target_audience: @target_audience,
152
+ redirect_uri: @redirect_uri,
153
+ username: @username,
154
+ password: @password,
155
+ issuer: @issuer,
156
+ person: @person,
157
+ sub: @sub,
158
+ audience: @audience,
159
+ signing_key: @signing_key,
160
+ extension_parameters: @extension_parameters,
161
+ additional_parameters: @additional_parameters,
162
+ access_type: @access_type,
163
+ universe_domain: @universe_domain,
164
+ logger: @logger
165
+ }.merge(options)
166
+
167
+ new_client = self.class.new opts
168
+
169
+ new_client.configure_connection options
170
+ end
171
+
97
172
  private
98
173
 
99
174
  def expires_at_from_id_token id_token
@@ -106,6 +181,49 @@ module Signet
106
181
  # Shouldn't happen unless we get a garbled ID token
107
182
  nil
108
183
  end
184
+
185
+ def log_response token_response
186
+ response_hash = JSON.parse token_response rescue {}
187
+ if response_hash["access_token"]
188
+ digest = Digest::SHA256.hexdigest response_hash["access_token"]
189
+ response_hash["access_token"] = "(sha256:#{digest})"
190
+ end
191
+ if response_hash["id_token"]
192
+ digest = Digest::SHA256.hexdigest response_hash["id_token"]
193
+ response_hash["id_token"] = "(sha256:#{digest})"
194
+ end
195
+ Google::Logging::Message.from(
196
+ message: "Received auth token response: #{response_hash}",
197
+ "credentialsId" => object_id
198
+ )
199
+ end
200
+
201
+ def log_auth_error err
202
+ logger&.info do
203
+ Google::Logging::Message.from(
204
+ message: "Auth error when fetching auth token: #{err}",
205
+ "credentialsId" => object_id
206
+ )
207
+ end
208
+ end
209
+
210
+ def log_transient_error err
211
+ logger&.info do
212
+ Google::Logging::Message.from(
213
+ message: "Transient error when fetching auth token: #{err}",
214
+ "credentialsId" => object_id
215
+ )
216
+ end
217
+ end
218
+
219
+ def log_retries_exhausted err
220
+ logger&.info do
221
+ Google::Logging::Message.from(
222
+ message: "Exhausted retries when fetching auth token: #{err}",
223
+ "credentialsId" => object_id
224
+ )
225
+ end
226
+ end
109
227
  end
110
228
  end
111
229
  end
@@ -85,6 +85,26 @@ module Google
85
85
  super options
86
86
  end
87
87
 
88
+ # Creates a duplicate of these credentials
89
+ # without the Signet::OAuth2::Client-specific
90
+ # transient state (e.g. cached tokens)
91
+ #
92
+ # @param options [Hash] Overrides for the credentials parameters.
93
+ # The following keys are recognized in addition to keys in the
94
+ # Signet::OAuth2::Client
95
+ # * `project_id` the project id to use during the authentication
96
+ # * `quota_project_id` the quota project id to use
97
+ # during the authentication
98
+ def duplicate options = {}
99
+ options = deep_hash_normalize options
100
+ super(
101
+ {
102
+ project_id: @project_id,
103
+ quota_project_id: @quota_project_id
104
+ }.merge(options)
105
+ )
106
+ end
107
+
88
108
  # Revokes the credential
89
109
  def revoke! options = {}
90
110
  c = options[:connection] || Faraday.default_connection
@@ -114,6 +134,29 @@ module Google
114
134
  Google::Auth::ScopeUtil.normalize(scope)
115
135
  missing_scope.empty?
116
136
  end
137
+
138
+ # Destructively updates these credentials
139
+ #
140
+ # This method is called by `Signet::OAuth2::Client`'s constructor
141
+ #
142
+ # @param options [Hash] Overrides for the credentials parameters.
143
+ # The following keys are recognized in addition to keys in the
144
+ # Signet::OAuth2::Client
145
+ # * `project_id` the project id to use during the authentication
146
+ # * `quota_project_id` the quota project id to use
147
+ # during the authentication
148
+ # @return [Google::Auth::UserRefreshCredentials]
149
+ def update! options = {}
150
+ # Normalize all keys to symbols to allow indifferent access.
151
+ options = deep_hash_normalize options
152
+
153
+ @project_id = options[:project_id] if options.key? :project_id
154
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
155
+
156
+ super(options)
157
+
158
+ self
159
+ end
117
160
  end
118
161
  end
119
162
  end