googleauth 1.14.0 → 1.17.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 +49 -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 +15 -7
  11. data/lib/googleauth/compute_engine.rb +64 -18
  12. data/lib/googleauth/credentials.rb +66 -35
  13. data/lib/googleauth/credentials_loader.rb +30 -5
  14. data/lib/googleauth/default_credentials.rb +69 -34
  15. data/lib/googleauth/errors.rb +117 -0
  16. data/lib/googleauth/external_account/aws_credentials.rb +87 -19
  17. data/lib/googleauth/external_account/base_credentials.rb +34 -4
  18. data/lib/googleauth/external_account/external_account_utils.rb +17 -5
  19. data/lib/googleauth/external_account/identity_pool_credentials.rb +42 -16
  20. data/lib/googleauth/external_account/pluggable_credentials.rb +37 -21
  21. data/lib/googleauth/external_account.rb +53 -7
  22. data/lib/googleauth/iam.rb +20 -4
  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 +119 -19
  28. data/lib/googleauth/json_key_reader.rb +17 -3
  29. data/lib/googleauth/oauth2/sts_client.rb +12 -6
  30. data/lib/googleauth/scope_util.rb +2 -2
  31. data/lib/googleauth/service_account.rb +55 -21
  32. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  33. data/lib/googleauth/signet.rb +30 -10
  34. data/lib/googleauth/user_authorizer.rb +38 -10
  35. data/lib/googleauth/user_refresh.rb +54 -19
  36. data/lib/googleauth/version.rb +1 -1
  37. data/lib/googleauth/web_user_authorizer.rb +54 -17
  38. data/lib/googleauth.rb +1 -0
  39. metadata +22 -19
@@ -14,7 +14,9 @@
14
14
 
15
15
  require "time"
16
16
  require "uri"
17
+ require "json"
17
18
  require "googleauth/credentials_loader"
19
+ require "googleauth/errors"
18
20
  require "googleauth/external_account/aws_credentials"
19
21
  require "googleauth/external_account/identity_pool_credentials"
20
22
  require "googleauth/external_account/pluggable_credentials"
@@ -33,32 +35,64 @@ module Google
33
35
  MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze
34
36
  INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze
35
37
 
38
+ # @private
39
+ # @type [::String] The type name for this credential.
40
+ CREDENTIAL_TYPE_NAME = "external_account".freeze
41
+
36
42
  # Create a ExternalAccount::Credentials
37
43
  #
38
- # @param json_key_io [IO] an IO from which the JSON key can be read
39
- # @param scope [String,Array,nil] the scope(s) to access
44
+ # @note Warning:
45
+ # This method does not validate the credential configuration. A security
46
+ # risk occurs when a credential configuration configured with malicious urls
47
+ # is used.
48
+ # When the credential configuration is accepted from an
49
+ # untrusted source, you should validate it before using with this method.
50
+ # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
51
+ # for more details.
52
+ #
53
+ # @param options [Hash] Options for creating credentials
54
+ # @option options [IO] :json_key_io (required) An IO object containing the JSON key
55
+ # @option options [String,Array,nil] :scope The scope(s) to access
56
+ # @return [Google::Auth::ExternalAccount::AwsCredentials,
57
+ # Google::Auth::ExternalAccount::IdentityPoolCredentials,
58
+ # Google::Auth::ExternalAccount::PluggableAuthCredentials]
59
+ # The appropriate external account credentials based on the credential source
60
+ # @raise [Google::Auth::InitializationError] If the json file is missing, lacks required fields,
61
+ # or does not contain a supported credential source
40
62
  def self.make_creds options = {}
41
63
  json_key_io, scope = options.values_at :json_key_io, :scope
42
64
 
43
- raise "A json file is required for external account credentials." unless json_key_io
65
+ raise InitializationError, "A json file is required for external account credentials." unless json_key_io
66
+ json_key = JSON.parse json_key_io.read, symbolize_names: true
67
+ if json_key.key? :type
68
+ json_key_io.rewind
69
+ else # Defaults to class credential 'type' if missing.
70
+ json_key[:type] = CREDENTIAL_TYPE_NAME
71
+ json_key_io = StringIO.new JSON.generate(json_key)
72
+ end
73
+ CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME
44
74
  user_creds = read_json_key json_key_io
45
75
 
46
76
  # AWS credentials is determined by aws subject token type
47
77
  return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE
48
78
 
49
- raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
79
+ raise InitializationError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
50
80
  user_creds[:scope] = scope
51
81
  make_external_account_credentials user_creds
52
82
  end
53
83
 
54
84
  # Reads the required fields from the JSON.
85
+ #
86
+ # @param json_key_io [IO] An IO object containing the JSON key
87
+ # @return [Hash] The parsed JSON key
88
+ # @raise [Google::Auth::InitializationError] If the JSON is missing required fields
55
89
  def self.read_json_key json_key_io
56
- json_key = MultiJson.load json_key_io.read, symbolize_keys: true
90
+ json_key = JSON.parse json_key_io.read, symbolize_names: true
57
91
  wanted = [
58
92
  :audience, :subject_token_type, :token_url, :credential_source
59
93
  ]
60
94
  wanted.each do |key|
61
- raise "the json is missing the #{key} field" unless json_key.key? key
95
+ raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key
62
96
  end
63
97
  json_key
64
98
  end
@@ -66,6 +100,11 @@ module Google
66
100
  class << self
67
101
  private
68
102
 
103
+ # Creates AWS credentials from the provided user credentials
104
+ #
105
+ # @param user_creds [Hash] The user credentials containing AWS credential source information
106
+ # @param scope [String,Array,nil] The scope(s) to access
107
+ # @return [Google::Auth::ExternalAccount::AwsCredentials] The AWS credentials
69
108
  def make_aws_credentials user_creds, scope
70
109
  Google::Auth::ExternalAccount::AwsCredentials.new(
71
110
  audience: user_creds[:audience],
@@ -78,6 +117,13 @@ module Google
78
117
  )
79
118
  end
80
119
 
120
+ # Creates the appropriate external account credentials based on the credential source type
121
+ #
122
+ # @param user_creds [Hash] The user credentials containing credential source information
123
+ # @return [Google::Auth::ExternalAccount::IdentityPoolCredentials,
124
+ # Google::Auth::ExternalAccount::PluggableAuthCredentials]
125
+ # The appropriate external account credentials
126
+ # @raise [Google::Auth::InitializationError] If the credential source is not a supported type
81
127
  def make_external_account_credentials user_creds
82
128
  unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
83
129
  return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
@@ -85,7 +131,7 @@ module Google
85
131
  unless user_creds[:credential_source][:executable].nil?
86
132
  return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
87
133
  end
88
- raise INVALID_EXTERNAL_ACCOUNT_TYPE
134
+ raise InitializationError, INVALID_EXTERNAL_ACCOUNT_TYPE
89
135
  end
90
136
  end
91
137
  end
@@ -14,7 +14,7 @@
14
14
 
15
15
  require "googleauth/signet"
16
16
  require "googleauth/credentials_loader"
17
- require "multi_json"
17
+ require "json"
18
18
 
19
19
  module Google
20
20
  # Module Auth provides classes that provide Google-specific authorization
@@ -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,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "googleauth/signet"
15
+ require "json"
16
16
  require "googleauth/base_client"
17
+ require "googleauth/errors"
17
18
  require "googleauth/helpers/connection"
18
19
 
19
20
  module Google
@@ -23,6 +24,9 @@ module Google
23
24
  # and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
24
25
  # The short-lived token and its expiration time are cached.
25
26
  class ImpersonatedServiceAccountCredentials
27
+ # @private
28
+ CREDENTIAL_TYPE_NAME = "impersonated_service_account".freeze
29
+
26
30
  # @private
27
31
  ERROR_SUFFIX = <<~ERROR.freeze
28
32
  when trying to get security access token
@@ -69,6 +73,15 @@ module Google
69
73
  # and request short-lived credentials for a service account
70
74
  # that has the authorization that your use case requires.
71
75
  #
76
+ # @note Warning:
77
+ # This method does not validate the credential configuration. A security
78
+ # risk occurs when a credential configuration configured with malicious urls
79
+ # is used.
80
+ # When the credential configuration is accepted from an
81
+ # untrusted source, you should validate it before using with this method.
82
+ # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
83
+ # for more details.
84
+ #
72
85
  # @param options [Hash] A hash of options to configure the credentials.
73
86
  # @option options [Object] :base_credentials (required) The authenticated principal.
74
87
  # It will be used as following:
@@ -84,11 +97,50 @@ module Google
84
97
  # defining the permissions required for the token.
85
98
  # @option options [Object] :source_credentials The authenticated principal that will be used
86
99
  # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
100
+ # @option options [IO] :json_key_io The IO object that contains the credential configuration.
101
+ # It is exclusive with `:base_credentials` and `:source_credentials` options.
87
102
  #
88
103
  # @return [Google::Auth::ImpersonatedServiceAccountCredentials]
89
104
  def self.make_creds options = {}
90
- new options
105
+ if options[:json_key_io]
106
+ make_creds_from_json options
107
+ else
108
+ new options
109
+ end
110
+ end
111
+
112
+ # @private
113
+ def self.make_creds_from_json options
114
+ json_key_io = options[:json_key_io]
115
+ if options[:base_credentials] || options[:source_credentials]
116
+ raise Google::Auth::InitializationError,
117
+ "json_key_io is not compatible with base_credentials or source_credentials"
118
+ end
119
+
120
+ require "googleauth/default_credentials"
121
+ impersonated_json = JSON.parse json_key_io.read
122
+ source_credentials_info = impersonated_json["source_credentials"]
123
+
124
+ if source_credentials_info["type"] == CREDENTIAL_TYPE_NAME
125
+ raise Google::Auth::InitializationError,
126
+ "Source credentials can't be of type impersonated_service_account, " \
127
+ "use delegates to chain impersonation."
128
+ end
129
+
130
+ source_credentials = DefaultCredentials.make_creds(
131
+ json_key_io: StringIO.new(JSON.generate(source_credentials_info))
132
+ )
133
+
134
+ impersonation_url = impersonated_json["service_account_impersonation_url"]
135
+ scope = options[:scope] || impersonated_json["scopes"]
136
+
137
+ new(
138
+ source_credentials: source_credentials,
139
+ impersonation_url: impersonation_url,
140
+ scope: scope
141
+ )
91
142
  end
143
+ private_class_method :make_creds_from_json
92
144
 
93
145
  # Initializes a new instance of ImpersonatedServiceAccountCredentials.
94
146
  #
@@ -105,6 +157,7 @@ module Google
105
157
  # - `{source_sa_email}` is the email address of the service account to impersonate.
106
158
  # @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
107
159
  # defining the permissions required for the token.
160
+ # It will override the scope from the `json_key_io` file if provided.
108
161
  # @option options [Object] :source_credentials The authenticated principal that will be used
109
162
  # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
110
163
  # It is redundant to provide both source and base credentials as only source will be used,
@@ -191,6 +244,20 @@ module Google
191
244
  self.class.new options
192
245
  end
193
246
 
247
+ # The principal behind the credentials. This class allows custom source credentials type
248
+ # that might not implement `principal`, in which case `:unknown` is returned.
249
+ #
250
+ # @private
251
+ # @return [String, Symbol] The string representation of the principal,
252
+ # the token type in lieu of the principal, or :unknown if source principal is unknown.
253
+ def principal
254
+ if @source_credentials.respond_to? :principal
255
+ @source_credentials.principal
256
+ else
257
+ :unknown
258
+ end
259
+ end
260
+
194
261
  private
195
262
 
196
263
  # Generates a new impersonation access token by exchanging the source credentials' token
@@ -200,37 +267,69 @@ module Google
200
267
  # for an impersonation token using the specified impersonation URL. The generated token and
201
268
  # its expiration time are cached for subsequent use.
202
269
  #
270
+ # @private
203
271
  # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
204
272
  #
205
- # @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
206
- # @raise [Signet::AuthorizationError] For other unexpected response statuses.
273
+ # @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500.
274
+ # @raise [Google::Auth::AuthorizationError] For other unexpected response statuses.
207
275
  #
208
276
  # @return [String] The newly generated impersonation access token.
209
277
  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
278
+ auth_header = prepare_auth_header
279
+ resp = make_impersonation_request auth_header
218
280
 
219
281
  case resp.status
220
282
  when 200
221
- response = MultiJson.load resp.body
283
+ response = JSON.parse resp.body
222
284
  self.expires_at = response["expireTime"]
223
285
  @access_token = response["accessToken"]
224
286
  access_token
225
287
  when 403, 500
226
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
227
- raise Signet::UnexpectedStatusError, msg
288
+ handle_error_response resp, UnexpectedStatusError
228
289
  else
229
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
230
- raise Signet::AuthorizationError, msg
290
+ handle_error_response resp, AuthorizationError
231
291
  end
232
292
  end
233
293
 
294
+ # Prepares the authorization header for the impersonation request
295
+ # by fetching a token from source credentials.
296
+ #
297
+ # @private
298
+ # @return [Hash] The authorization header with the source credentials' token
299
+ def prepare_auth_header
300
+ auth_header = {}
301
+ @source_credentials.updater_proc.call auth_header
302
+ auth_header
303
+ end
304
+
305
+ # Makes the HTTP request to the impersonation endpoint.
306
+ #
307
+ # @private
308
+ # @param [Hash] auth_header The authorization header containing the source token
309
+ # @return [Faraday::Response] The HTTP response from the impersonation endpoint
310
+ def make_impersonation_request auth_header
311
+ connection.post @impersonation_url do |req|
312
+ req.headers.merge! auth_header
313
+ req.headers["Content-Type"] = "application/json"
314
+ req.body = JSON.generate({ scope: @scope })
315
+ end
316
+ end
317
+
318
+ # Creates and raises an appropriate error based on the response.
319
+ #
320
+ # @private
321
+ # @param [Faraday::Response] resp The HTTP response
322
+ # @param [Class] error_class The error class to instantiate
323
+ # @raise [StandardError] The appropriate error with details
324
+ def handle_error_response resp, error_class
325
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
326
+ raise error_class.with_details(
327
+ msg,
328
+ credential_type_name: self.class.name,
329
+ principal: principal
330
+ )
331
+ end
332
+
234
333
  # Setter for the expires_at value that makes sure it is converted
235
334
  # to Time object.
236
335
  def expires_at= new_expires_at
@@ -249,7 +348,7 @@ module Google
249
348
  #
250
349
  # @return [Time, nil] The normalized Time object, or nil if the input is nil.
251
350
  #
252
- # @raise [RuntimeError] If the input is not a Time, String, or nil.
351
+ # @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil.
253
352
  def normalize_timestamp time
254
353
  case time
255
354
  when NilClass
@@ -259,7 +358,8 @@ module Google
259
358
  when String
260
359
  Time.parse time
261
360
  else
262
- raise "Invalid time value #{time}"
361
+ message = "Invalid time value #{time}"
362
+ raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal)
263
363
  end
264
364
  end
265
365
 
@@ -12,6 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "json"
16
+ require "googleauth/errors"
17
+
15
18
  module Google
16
19
  # Module Auth provides classes that provide Google-specific authorization
17
20
  # used to access Google APIs.
@@ -19,10 +22,21 @@ module Google
19
22
  # JsonKeyReader contains the behaviour used to read private key and
20
23
  # client email fields from the service account
21
24
  module JsonKeyReader
25
+ # Reads a JSON key from an IO object and extracts common fields.
26
+ #
27
+ # @param json_key_io [IO] An IO object containing the JSON key
28
+ # @return [Array(String, String, String, String, String)] An array containing:
29
+ # private_key, client_email, project_id, quota_project_id, and universe_domain
30
+ # @raise [Google::Auth::InitializationError] If client_email or private_key
31
+ # fields are missing from the JSON
22
32
  def read_json_key json_key_io
23
- 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
+ begin
34
+ json_key = JSON.parse json_key_io.read
35
+ rescue JSON::ParserError => e
36
+ raise InitializationError, "Invalid JSON keyfile format: #{e.message}"
37
+ end
38
+ raise InitializationError, "missing client_email" unless json_key.key? "client_email"
39
+ raise InitializationError, "missing private_key" unless json_key.key? "private_key"
26
40
  [
27
41
  json_key["private_key"],
28
42
  json_key["client_email"],
@@ -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 "json"
16
+ require "googleauth/errors"
15
17
  require "googleauth/helpers/connection"
16
18
 
17
19
  module Google
@@ -36,10 +38,12 @@ module Google
36
38
 
37
39
  # Create a new instance of the STSClient.
38
40
  #
39
- # @param [String] token_exchange_endpoint
40
- # The token exchange endpoint.
41
+ # @param [Hash] options Configuration options
42
+ # @option options [String] :token_exchange_endpoint The token exchange endpoint
43
+ # @option options [Faraday::Connection] :connection The Faraday connection to use
44
+ # @raise [Google::Auth::InitializationError] If token_exchange_endpoint is nil
41
45
  def initialize options = {}
42
- raise "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
46
+ raise InitializationError, "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
43
47
  self.default_connection = options[:connection]
44
48
  @token_exchange_endpoint = options[:token_exchange_endpoint]
45
49
  end
@@ -67,6 +71,8 @@ module Google
67
71
  # The optional additional headers to pass to the token exchange endpoint.
68
72
  #
69
73
  # @return [Hash] A hash containing the token exchange response.
74
+ # @raise [ArgumentError] If required options are missing
75
+ # @raise [Google::Auth::AuthorizationError] If the token exchange request fails
70
76
  def exchange_token options = {}
71
77
  missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys
72
78
  unless missing_required_opts.empty?
@@ -81,10 +87,10 @@ module Google
81
87
  response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
82
88
 
83
89
  if response.status != 200
84
- raise "Token exchange failed with status #{response.status}"
90
+ raise AuthorizationError, "Token exchange failed with status #{response.status}"
85
91
  end
86
92
 
87
- MultiJson.load response.body
93
+ JSON.parse response.body
88
94
  end
89
95
 
90
96
  private
@@ -99,7 +105,7 @@ module Google
99
105
  subject_token_type: options[:subject_token_type]
100
106
  }
101
107
  unless options[:additional_options].nil?
102
- request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true)
108
+ request_body[:options] = CGI.escape JSON.generate(options[:additional_options])
103
109
  end
104
110
  request_body
105
111
  end
@@ -14,7 +14,7 @@
14
14
 
15
15
  require "googleauth/signet"
16
16
  require "googleauth/credentials_loader"
17
- require "multi_json"
17
+ require "json"
18
18
 
19
19
  module Google
20
20
  module Auth
@@ -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