googleauth 1.14.0 → 1.16.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Credentials.md +110 -0
  4. data/Errors.md +152 -0
  5. data/README.md +0 -1
  6. data/lib/googleauth/api_key.rb +9 -0
  7. data/lib/googleauth/application_default.rb +3 -1
  8. data/lib/googleauth/base_client.rb +5 -0
  9. data/lib/googleauth/bearer_token.rb +16 -2
  10. data/lib/googleauth/client_id.rb +9 -5
  11. data/lib/googleauth/compute_engine.rb +64 -18
  12. data/lib/googleauth/credentials.rb +67 -35
  13. data/lib/googleauth/credentials_loader.rb +24 -4
  14. data/lib/googleauth/default_credentials.rb +67 -32
  15. data/lib/googleauth/errors.rb +117 -0
  16. data/lib/googleauth/external_account/aws_credentials.rb +85 -18
  17. data/lib/googleauth/external_account/base_credentials.rb +31 -2
  18. data/lib/googleauth/external_account/external_account_utils.rb +15 -4
  19. data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
  20. data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
  21. data/lib/googleauth/external_account.rb +44 -6
  22. data/lib/googleauth/iam.rb +19 -3
  23. data/lib/googleauth/id_tokens/errors.rb +13 -7
  24. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  25. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  26. data/lib/googleauth/id_tokens.rb +4 -4
  27. data/lib/googleauth/impersonated_service_account.rb +117 -18
  28. data/lib/googleauth/json_key_reader.rb +11 -2
  29. data/lib/googleauth/oauth2/sts_client.rb +9 -4
  30. data/lib/googleauth/scope_util.rb +1 -1
  31. data/lib/googleauth/service_account.rb +37 -10
  32. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  33. data/lib/googleauth/signet.rb +24 -6
  34. data/lib/googleauth/user_authorizer.rb +35 -7
  35. data/lib/googleauth/user_refresh.rb +42 -16
  36. data/lib/googleauth/version.rb +1 -1
  37. data/lib/googleauth/web_user_authorizer.rb +46 -9
  38. data/lib/googleauth.rb +1 -0
  39. metadata +8 -5
@@ -27,8 +27,9 @@ module Google
27
27
 
28
28
  # Initializes an IAMCredentials.
29
29
  #
30
- # @param selector the IAM selector.
31
- # @param token the IAM token.
30
+ # @param selector [String] The IAM selector.
31
+ # @param token [String] The IAM token.
32
+ # @raise [TypeError] If selector or token is not a String
32
33
  def initialize selector, token
33
34
  raise TypeError unless selector.is_a? String
34
35
  raise TypeError unless token.is_a? String
@@ -37,13 +38,19 @@ module Google
37
38
  end
38
39
 
39
40
  # Adds the credential fields to the hash.
41
+ #
42
+ # @param a_hash [Hash] The hash to update with credentials
43
+ # @return [Hash] The updated hash with credentials
40
44
  def apply! a_hash
41
45
  a_hash[SELECTOR_KEY] = @selector
42
46
  a_hash[TOKEN_KEY] = @token
43
47
  a_hash
44
48
  end
45
49
 
46
- # Returns a clone of a_hash updated with the authoriation header
50
+ # Returns a clone of a_hash updated with the authorization header
51
+ #
52
+ # @param a_hash [Hash] The hash to clone and update with credentials
53
+ # @return [Hash] A new hash with credentials
47
54
  def apply a_hash
48
55
  a_copy = a_hash.clone
49
56
  apply! a_copy
@@ -52,9 +59,18 @@ module Google
52
59
 
53
60
  # Returns a reference to the #apply method, suitable for passing as
54
61
  # a closure
62
+ #
63
+ # @return [Proc] A procedure that updates a hash with credentials
55
64
  def updater_proc
56
65
  proc { |a_hash, _opts = {}| apply a_hash }
57
66
  end
67
+
68
+ # Returns the IAM authority selector as the principal
69
+ # @private
70
+ # @return [String] the IAM authoirty selector
71
+ def principal
72
+ @selector
73
+ end
58
74
  end
59
75
  end
60
76
  end
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require "googleauth/errors"
18
+
17
19
 
18
20
  module Google
19
21
  module Auth
@@ -21,35 +23,39 @@ module Google
21
23
  ##
22
24
  # Failed to obtain keys from the key source.
23
25
  #
24
- class KeySourceError < StandardError; end
26
+ class KeySourceError < StandardError
27
+ include Google::Auth::Error
28
+ end
25
29
 
26
30
  ##
27
31
  # Failed to verify a token.
28
32
  #
29
- class VerificationError < StandardError; end
33
+ class VerificationError < StandardError
34
+ include Google::Auth::Error
35
+ end
30
36
 
31
37
  ##
32
- # Failed to verify a token because it is expired.
38
+ # Failed to verify token because it is expired.
33
39
  #
34
40
  class ExpiredTokenError < VerificationError; end
35
41
 
36
42
  ##
37
- # Failed to verify a token because its signature did not match.
43
+ # Failed to verify token because its signature did not match.
38
44
  #
39
45
  class SignatureError < VerificationError; end
40
46
 
41
47
  ##
42
- # Failed to verify a token because its issuer did not match.
48
+ # Failed to verify token because its issuer did not match.
43
49
  #
44
50
  class IssuerMismatchError < VerificationError; end
45
51
 
46
52
  ##
47
- # Failed to verify a token because its audience did not match.
53
+ # Failed to verify token because its audience did not match.
48
54
  #
49
55
  class AudienceMismatchError < VerificationError; end
50
56
 
51
57
  ##
52
- # Failed to verify a token because its authorized party did not match.
58
+ # Failed to verify token because its authorized party did not match.
53
59
  #
54
60
  class AuthorizedPartyMismatchError < VerificationError; end
55
61
  end
@@ -72,8 +72,8 @@ module Google
72
72
  #
73
73
  # @param jwk [Hash,String] The JWK specification.
74
74
  # @return [KeyInfo]
75
- # @raise [KeySourceError] If the key could not be extracted from the
76
- # JWK.
75
+ # @raise [Google::Auth::IDTokens::KeySourceError] If the key could not be extracted from the
76
+ # JWK due to invalid type, malformed JSON, or invalid key data.
77
77
  #
78
78
  def from_jwk jwk
79
79
  jwk = symbolize_keys ensure_json_parsed jwk
@@ -94,10 +94,10 @@ module Google
94
94
  # Create an array of KeyInfo from a JWK Set, which may be given as
95
95
  # either a hash or an unparsed JSON string.
96
96
  #
97
- # @param jwk [Hash,String] The JWK Set specification.
97
+ # @param jwk_set [Hash,String] The JWK Set specification.
98
98
  # @return [Array<KeyInfo>]
99
- # @raise [KeySourceError] If a key could not be extracted from the
100
- # JWK Set.
99
+ # @raise [Google::Auth::IDTokens::KeySourceError] If a key could not be extracted from the
100
+ # JWK Set, or if the set contains no keys.
101
101
  #
102
102
  def from_jwk_set jwk_set
103
103
  jwk_set = symbolize_keys ensure_json_parsed jwk_set
@@ -261,7 +261,8 @@ module Google
261
261
  # return the new keys.
262
262
  #
263
263
  # @return [Array<KeyInfo>]
264
- # @raise [KeySourceError] if key retrieval failed.
264
+ # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval fails, JSON parsing
265
+ # fails, or the data cannot be interpreted as keys
265
266
  #
266
267
  def refresh_keys
267
268
  @monitor.synchronize do
@@ -310,6 +311,11 @@ module Google
310
311
 
311
312
  protected
312
313
 
314
+ # Interpret JSON data as X509 certificates
315
+ #
316
+ # @param data [Hash] The JSON data containing certificate strings
317
+ # @return [Array<KeyInfo>] Array of key info objects
318
+ # @raise [Google::Auth::IDTokens::KeySourceError] If X509 certificates cannot be parsed
313
319
  def interpret_json data
314
320
  data.map do |id, cert_str|
315
321
  key = OpenSSL::X509::Certificate.new(cert_str).public_key
@@ -371,7 +377,7 @@ module Google
371
377
  # Attempt to refresh keys and return the new keys.
372
378
  #
373
379
  # @return [Array<KeyInfo>]
374
- # @raise [KeySourceError] if key retrieval failed.
380
+ # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval failed for any source.
375
381
  #
376
382
  def refresh_keys
377
383
  @sources.flat_map(&:refresh_keys)
@@ -61,10 +61,9 @@ module Google
61
61
  # @param iss [String,nil] If given, override the `iss` check.
62
62
  #
63
63
  # @return [Hash] the decoded payload, if verification succeeded.
64
- # @raise [KeySourceError] if the key source failed to obtain public keys
65
- # @raise [VerificationError] if the token verification failed.
64
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
65
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
66
66
  # Additional data may be available in the error subclass and message.
67
- #
68
67
  def verify token,
69
68
  key_source: :default,
70
69
  aud: :default,
@@ -160,8 +160,8 @@ module Google
160
160
  # checking is performed. Default is to check against {OIDC_ISSUERS}.
161
161
  #
162
162
  # @return [Hash] The decoded token payload.
163
- # @raise [KeySourceError] if the key source failed to obtain public keys
164
- # @raise [VerificationError] if the token verification failed.
163
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
164
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
165
165
  # Additional data may be available in the error subclass and message.
166
166
  #
167
167
  def verify_oidc token,
@@ -197,8 +197,8 @@ module Google
197
197
  # checking is performed. Default is to check against {IAP_ISSUERS}.
198
198
  #
199
199
  # @return [Hash] The decoded token payload.
200
- # @raise [KeySourceError] if the key source failed to obtain public keys
201
- # @raise [VerificationError] if the token verification failed.
200
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
201
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
202
202
  # Additional data may be available in the error subclass and message.
203
203
  #
204
204
  def verify_iap token,
@@ -12,8 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "googleauth/signet"
16
15
  require "googleauth/base_client"
16
+ require "googleauth/errors"
17
17
  require "googleauth/helpers/connection"
18
18
 
19
19
  module Google
@@ -23,6 +23,9 @@ module Google
23
23
  # and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
24
24
  # The short-lived token and its expiration time are cached.
25
25
  class ImpersonatedServiceAccountCredentials
26
+ # @private
27
+ CREDENTIAL_TYPE_NAME = "impersonated_service_account".freeze
28
+
26
29
  # @private
27
30
  ERROR_SUFFIX = <<~ERROR.freeze
28
31
  when trying to get security access token
@@ -69,6 +72,15 @@ module Google
69
72
  # and request short-lived credentials for a service account
70
73
  # that has the authorization that your use case requires.
71
74
  #
75
+ # @note Warning:
76
+ # This method does not validate the credential configuration. A security
77
+ # risk occurs when a credential configuration configured with malicious urls
78
+ # is used.
79
+ # When the credential configuration is accepted from an
80
+ # untrusted source, you should validate it before using with this method.
81
+ # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
82
+ # for more details.
83
+ #
72
84
  # @param options [Hash] A hash of options to configure the credentials.
73
85
  # @option options [Object] :base_credentials (required) The authenticated principal.
74
86
  # It will be used as following:
@@ -84,12 +96,51 @@ module Google
84
96
  # defining the permissions required for the token.
85
97
  # @option options [Object] :source_credentials The authenticated principal that will be used
86
98
  # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
99
+ # @option options [IO] :json_key_io The IO object that contains the credential configuration.
100
+ # It is exclusive with `:base_credentials` and `:source_credentials` options.
87
101
  #
88
102
  # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
89
103
  def self.make_creds options = {}
90
- new options
104
+ if options[:json_key_io]
105
+ make_creds_from_json options
106
+ else
107
+ new options
108
+ end
91
109
  end
92
110
 
111
+ # @private
112
+ def self.make_creds_from_json options
113
+ json_key_io = options[:json_key_io]
114
+ if options[:base_credentials] || options[:source_credentials]
115
+ raise Google::Auth::InitializationError,
116
+ "json_key_io is not compatible with base_credentials or source_credentials"
117
+ end
118
+
119
+ require "googleauth/default_credentials"
120
+ impersonated_json = MultiJson.load json_key_io.read
121
+ source_credentials_info = impersonated_json["source_credentials"]
122
+
123
+ if source_credentials_info["type"] == CREDENTIAL_TYPE_NAME
124
+ raise Google::Auth::InitializationError,
125
+ "Source credentials can't be of type impersonated_service_account, " \
126
+ "use delegates to chain impersonation."
127
+ end
128
+
129
+ source_credentials = DefaultCredentials.make_creds(
130
+ json_key_io: StringIO.new(MultiJson.dump(source_credentials_info))
131
+ )
132
+
133
+ impersonation_url = impersonated_json["service_account_impersonation_url"]
134
+ scope = options[:scope] || impersonated_json["scopes"]
135
+
136
+ new(
137
+ source_credentials: source_credentials,
138
+ impersonation_url: impersonation_url,
139
+ scope: scope
140
+ )
141
+ end
142
+ private_class_method :make_creds_from_json
143
+
93
144
  # Initializes a new instance of ImpersonatedServiceAccountCredentials.
94
145
  #
95
146
  # @param options [Hash] A hash of options to configure the credentials.
@@ -105,6 +156,7 @@ module Google
105
156
  # - `{source_sa_email}` is the email address of the service account to impersonate.
106
157
  # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
107
158
  # defining the permissions required for the token.
159
+ # It will override the scope from the `json_key_io` file if provided.
108
160
  # @option options [Object] :source_credentials The authenticated principal that will be used
109
161
  # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
110
162
  # It is redundant to provide both source and base credentials as only source will be used,
@@ -191,6 +243,20 @@ module Google
191
243
  self.class.new options
192
244
  end
193
245
 
246
+ # The principal behind the credentials. This class allows custom source credentials type
247
+ # that might not implement `principal`, in which case `:unknown` is returned.
248
+ #
249
+ # @private
250
+ # @return [String, Symbol] The string representation of the principal,
251
+ # the token type in lieu of the principal, or :unknown if source principal is unknown.
252
+ def principal
253
+ if @source_credentials.respond_to? :principal
254
+ @source_credentials.principal
255
+ else
256
+ :unknown
257
+ end
258
+ end
259
+
194
260
  private
195
261
 
196
262
  # Generates a new impersonation access token by exchanging the source credentials' token
@@ -200,21 +266,16 @@ module Google
200
266
  # for an impersonation token using the specified impersonation URL. The generated token and
201
267
  # its expiration time are cached for subsequent use.
202
268
  #
269
+ # @private
203
270
  # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
204
271
  #
205
- # @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
206
- # @raise [Signet::AuthorizationError] For other unexpected response statuses.
272
+ # @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500.
273
+ # @raise [Google::Auth::AuthorizationError] For other unexpected response statuses.
207
274
  #
208
275
  # @return [String] The newly generated impersonation access token.
209
276
  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
277
+ auth_header = prepare_auth_header
278
+ resp = make_impersonation_request auth_header
218
279
 
219
280
  case resp.status
220
281
  when 200
@@ -223,14 +284,51 @@ module Google
223
284
  @access_token = response["accessToken"]
224
285
  access_token
225
286
  when 403, 500
226
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
227
- raise Signet::UnexpectedStatusError, msg
287
+ handle_error_response resp, UnexpectedStatusError
228
288
  else
229
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
230
- raise Signet::AuthorizationError, msg
289
+ handle_error_response resp, AuthorizationError
290
+ end
291
+ end
292
+
293
+ # Prepares the authorization header for the impersonation request
294
+ # by fetching a token from source credentials.
295
+ #
296
+ # @private
297
+ # @return [Hash] The authorization header with the source credentials' token
298
+ def prepare_auth_header
299
+ auth_header = {}
300
+ @source_credentials.updater_proc.call auth_header
301
+ auth_header
302
+ end
303
+
304
+ # Makes the HTTP request to the impersonation endpoint.
305
+ #
306
+ # @private
307
+ # @param [Hash] auth_header The authorization header containing the source token
308
+ # @return [Faraday::Response] The HTTP response from the impersonation endpoint
309
+ def make_impersonation_request auth_header
310
+ connection.post @impersonation_url do |req|
311
+ req.headers.merge! auth_header
312
+ req.headers["Content-Type"] = "application/json"
313
+ req.body = MultiJson.dump({ scope: @scope })
231
314
  end
232
315
  end
233
316
 
317
+ # Creates and raises an appropriate error based on the response.
318
+ #
319
+ # @private
320
+ # @param [Faraday::Response] resp The HTTP response
321
+ # @param [Class] error_class The error class to instantiate
322
+ # @raise [StandardError] The appropriate error with details
323
+ def handle_error_response resp, error_class
324
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
325
+ raise error_class.with_details(
326
+ msg,
327
+ credential_type_name: self.class.name,
328
+ principal: principal
329
+ )
330
+ end
331
+
234
332
  # Setter for the expires_at value that makes sure it is converted
235
333
  # to Time object.
236
334
  def expires_at= new_expires_at
@@ -249,7 +347,7 @@ module Google
249
347
  #
250
348
  # @return [Time, nil] The normalized Time object, or nil if the input is nil.
251
349
  #
252
- # @raise [RuntimeError] If the input is not a Time, String, or nil.
350
+ # @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil.
253
351
  def normalize_timestamp time
254
352
  case time
255
353
  when NilClass
@@ -259,7 +357,8 @@ module Google
259
357
  when String
260
358
  Time.parse time
261
359
  else
262
- raise "Invalid time value #{time}"
360
+ message = "Invalid time value #{time}"
361
+ raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal)
263
362
  end
264
363
  end
265
364
 
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "googleauth/errors"
16
+
15
17
  module Google
16
18
  # Module Auth provides classes that provide Google-specific authorization
17
19
  # used to access Google APIs.
@@ -19,10 +21,17 @@ module Google
19
21
  # JsonKeyReader contains the behaviour used to read private key and
20
22
  # client email fields from the service account
21
23
  module JsonKeyReader
24
+ # Reads a JSON key from an IO object and extracts common fields.
25
+ #
26
+ # @param json_key_io [IO] An IO object containing the JSON key
27
+ # @return [Array(String, String, String, String, String)] An array containing:
28
+ # private_key, client_email, project_id, quota_project_id, and universe_domain
29
+ # @raise [Google::Auth::InitializationError] If client_email or private_key
30
+ # fields are missing from the JSON
22
31
  def read_json_key json_key_io
23
32
  json_key = MultiJson.load json_key_io.read
24
- raise "missing client_email" unless json_key.key? "client_email"
25
- raise "missing private_key" unless json_key.key? "private_key"
33
+ raise InitializationError, "missing client_email" unless json_key.key? "client_email"
34
+ raise InitializationError, "missing private_key" unless json_key.key? "private_key"
26
35
  [
27
36
  json_key["private_key"],
28
37
  json_key["client_email"],
@@ -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 "googleauth/errors"
15
16
  require "googleauth/helpers/connection"
16
17
 
17
18
  module Google
@@ -36,10 +37,12 @@ module Google
36
37
 
37
38
  # Create a new instance of the STSClient.
38
39
  #
39
- # @param [String] token_exchange_endpoint
40
- # The token exchange endpoint.
40
+ # @param [Hash] options Configuration options
41
+ # @option options [String] :token_exchange_endpoint The token exchange endpoint
42
+ # @option options [Faraday::Connection] :connection The Faraday connection to use
43
+ # @raise [Google::Auth::InitializationError] If token_exchange_endpoint is nil
41
44
  def initialize options = {}
42
- raise "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
45
+ raise InitializationError, "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
43
46
  self.default_connection = options[:connection]
44
47
  @token_exchange_endpoint = options[:token_exchange_endpoint]
45
48
  end
@@ -67,6 +70,8 @@ module Google
67
70
  # The optional additional headers to pass to the token exchange endpoint.
68
71
  #
69
72
  # @return [Hash] A hash containing the token exchange response.
73
+ # @raise [ArgumentError] If required options are missing
74
+ # @raise [Google::Auth::AuthorizationError] If the token exchange request fails
70
75
  def exchange_token options = {}
71
76
  missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys
72
77
  unless missing_required_opts.empty?
@@ -81,7 +86,7 @@ module Google
81
86
  response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
82
87
 
83
88
  if response.status != 200
84
- raise "Token exchange failed with status #{response.status}"
89
+ raise AuthorizationError, "Token exchange failed with status #{response.status}"
85
90
  end
86
91
 
87
92
  MultiJson.load response.body
@@ -57,7 +57,7 @@ module Google
57
57
  #
58
58
  # @param scope [String,Array<String>] Input scope(s)
59
59
  # @return [Array<String>] Always an array of strings
60
- # @raise ArgumentError If the input is not a string or array of strings
60
+ # @raise [ArgumentError] If the input is not a string or array of strings
61
61
  #
62
62
  def self.as_array scope
63
63
  case scope
@@ -41,6 +41,10 @@ module Google
41
41
  attr_reader :project_id
42
42
  attr_reader :quota_project_id
43
43
 
44
+ # @private
45
+ # @type [::String] The type name for this credential.
46
+ CREDENTIAL_TYPE_NAME = "service_account".freeze
47
+
44
48
  def enable_self_signed_jwt?
45
49
  # Use a self-singed JWT if there's no information that can be used to
46
50
  # obtain an OAuth token, OR if there are scopes but also an assertion
@@ -51,23 +55,22 @@ module Google
51
55
 
52
56
  # Creates a ServiceAccountCredentials.
53
57
  #
54
- # @param json_key_io [IO] an IO from which the JSON key can be read
58
+ # @param json_key_io [IO] An IO object containing the JSON key
55
59
  # @param scope [string|array|nil] the scope(s) to access
60
+ # @raise [ArgumentError] If both scope and target_audience are specified
56
61
  def self.make_creds options = {}
57
62
  json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
58
63
  options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
59
64
  :audience, :token_credential_uri
60
65
  raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
61
66
 
62
- if json_key_io
63
- private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
64
- else
65
- private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
66
- client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
67
- project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
68
- quota_project_id = nil
69
- universe_domain = nil
70
- end
67
+ private_key, client_email, project_id, quota_project_id, universe_domain =
68
+ if json_key_io
69
+ CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME
70
+ read_json_key json_key_io
71
+ else
72
+ creds_from_env
73
+ end
71
74
  project_id ||= CredentialsLoader.load_gcloud_project_id
72
75
 
73
76
  new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
@@ -110,6 +113,9 @@ module Google
110
113
  # Handles certain escape sequences that sometimes appear in input.
111
114
  # Specifically, interprets the "\n" sequence for newline, and removes
112
115
  # enclosing quotes.
116
+ #
117
+ # @param str [String] The string to unescape
118
+ # @return [String] The unescaped string
113
119
  def self.unescape str
114
120
  str = str.gsub '\n', "\n"
115
121
  str = str[1..-2] if str.start_with?('"') && str.end_with?('"')
@@ -164,6 +170,13 @@ module Google
164
170
  self
165
171
  end
166
172
 
173
+ # Returns the client email as the principal for service account credentials
174
+ # @private
175
+ # @return [String] the email address of the service account
176
+ def principal
177
+ @issuer
178
+ end
179
+
167
180
  private
168
181
 
169
182
  def apply_self_signed_jwt! a_hash
@@ -179,6 +192,20 @@ module Google
179
192
  alt.logger = logger
180
193
  alt.apply! a_hash
181
194
  end
195
+
196
+ # @private
197
+ # Loads service account credential details from environment variables.
198
+ #
199
+ # @return [Array<String, String, String, nil, nil>] An array containing private_key,
200
+ # client_email, project_id, quota_project_id, and universe_domain.
201
+ def self.creds_from_env
202
+ private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
203
+ client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
204
+ project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
205
+ [private_key, client_email, project_id, nil, nil]
206
+ end
207
+
208
+ private_class_method :creds_from_env
182
209
  end
183
210
  end
184
211
  end
@@ -47,7 +47,7 @@ module Google
47
47
 
48
48
  # Create a ServiceAccountJwtHeaderCredentials.
49
49
  #
50
- # @param json_key_io [IO] an IO from which the JSON key can be read
50
+ # @param json_key_io [IO] An IO object containing the JSON key
51
51
  # @param scope [string|array|nil] the scope(s) to access
52
52
  def self.make_creds options = {}
53
53
  json_key_io, scope = options.values_at :json_key_io, :scope
@@ -56,7 +56,7 @@ module Google
56
56
 
57
57
  # Initializes a ServiceAccountJwtHeaderCredentials.
58
58
  #
59
- # @param json_key_io [IO] an IO from which the JSON key can be read
59
+ # @param json_key_io [IO] An IO object containing the JSON key
60
60
  def initialize options = {}
61
61
  json_key_io = options[:json_key_io]
62
62
  if json_key_io
@@ -159,6 +159,13 @@ module Google
159
159
  false
160
160
  end
161
161
 
162
+ # Returns the client email as the principal for service account JWT header credentials
163
+ # @private
164
+ # @return [String] the email address of the service account
165
+ def principal
166
+ @issuer
167
+ end
168
+
162
169
  private
163
170
 
164
171
  def deep_hash_normalize old_hash
@@ -16,6 +16,7 @@ require "base64"
16
16
  require "json"
17
17
  require "signet/oauth_2/client"
18
18
  require "googleauth/base_client"
19
+ require "googleauth/errors"
19
20
 
20
21
  module Signet
21
22
  # OAuth2 supports OAuth2 authentication.
@@ -109,17 +110,29 @@ module Signet
109
110
  end
110
111
  end
111
112
 
113
+ # rubocop:disable Metrics/MethodLength
114
+
115
+ # Retries the provided block with exponential backoff, handling and wrapping errors.
116
+ #
117
+ # @param [Integer] max_retry_count The maximum number of retries before giving up
118
+ # @yield The block to execute and potentially retry
119
+ # @return [Object] The result of the block if successful
120
+ # @raise [Google::Auth::AuthorizationError] If a Signet::AuthorizationError occurs or if retries are exhausted
121
+ # @raise [Google::Auth::ParseError] If a Signet::ParseError occurs during token parsing
112
122
  def retry_with_error max_retry_count = 5
113
123
  retry_count = 0
114
124
 
115
125
  begin
116
126
  yield.tap { |resp| log_response resp }
127
+ rescue Signet::AuthorizationError, Signet::ParseError => e
128
+ log_auth_error e
129
+ error_class = e.is_a?(Signet::ParseError) ? Google::Auth::ParseError : Google::Auth::AuthorizationError
130
+ raise error_class.with_details(
131
+ e.message,
132
+ credential_type_name: self.class.name,
133
+ principal: respond_to?(:principal) ? principal : :signet_client
134
+ )
117
135
  rescue StandardError => e
118
- if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
119
- log_auth_error e
120
- raise e
121
- end
122
-
123
136
  if retry_count < max_retry_count
124
137
  log_transient_error e
125
138
  retry_count += 1
@@ -128,10 +141,15 @@ module Signet
128
141
  else
129
142
  log_retries_exhausted e
130
143
  msg = "Unexpected error: #{e.inspect}"
131
- raise Signet::AuthorizationError, msg
144
+ raise Google::Auth::AuthorizationError.with_details(
145
+ msg,
146
+ credential_type_name: self.class.name,
147
+ principal: respond_to?(:principal) ? principal : :signet_client
148
+ )
132
149
  end
133
150
  end
134
151
  end
152
+ # rubocop:enable Metrics/MethodLength
135
153
 
136
154
  # Creates a duplicate of these credentials
137
155
  # without the Signet::OAuth2::Client-specific