googleauth 1.14.0 → 1.15.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Credentials.md +110 -0
  4. data/Errors.md +152 -0
  5. data/lib/googleauth/api_key.rb +9 -0
  6. data/lib/googleauth/application_default.rb +3 -1
  7. data/lib/googleauth/base_client.rb +5 -0
  8. data/lib/googleauth/bearer_token.rb +16 -2
  9. data/lib/googleauth/client_id.rb +9 -5
  10. data/lib/googleauth/compute_engine.rb +64 -18
  11. data/lib/googleauth/credentials.rb +67 -35
  12. data/lib/googleauth/credentials_loader.rb +24 -4
  13. data/lib/googleauth/default_credentials.rb +64 -32
  14. data/lib/googleauth/errors.rb +117 -0
  15. data/lib/googleauth/external_account/aws_credentials.rb +85 -18
  16. data/lib/googleauth/external_account/base_credentials.rb +31 -2
  17. data/lib/googleauth/external_account/external_account_utils.rb +15 -4
  18. data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
  19. data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
  20. data/lib/googleauth/external_account.rb +35 -6
  21. data/lib/googleauth/iam.rb +19 -3
  22. data/lib/googleauth/id_tokens/errors.rb +13 -7
  23. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  24. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  25. data/lib/googleauth/id_tokens.rb +4 -4
  26. data/lib/googleauth/impersonated_service_account.rb +64 -17
  27. data/lib/googleauth/json_key_reader.rb +11 -2
  28. data/lib/googleauth/oauth2/sts_client.rb +9 -4
  29. data/lib/googleauth/scope_util.rb +1 -1
  30. data/lib/googleauth/service_account.rb +37 -10
  31. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  32. data/lib/googleauth/signet.rb +24 -6
  33. data/lib/googleauth/user_authorizer.rb +35 -7
  34. data/lib/googleauth/user_refresh.rb +42 -16
  35. data/lib/googleauth/version.rb +1 -1
  36. data/lib/googleauth/web_user_authorizer.rb +46 -9
  37. data/lib/googleauth.rb +1 -0
  38. metadata +8 -5
@@ -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
@@ -191,6 +191,20 @@ module Google
191
191
  self.class.new options
192
192
  end
193
193
 
194
+ # The principal behind the credentials. This class allows custom source credentials type
195
+ # that might not implement `principal`, in which case `:unknown` is returned.
196
+ #
197
+ # @private
198
+ # @return [String, Symbol] The string representation of the principal,
199
+ # the token type in lieu of the principal, or :unknown if source principal is unknown.
200
+ def principal
201
+ if @source_credentials.respond_to? :principal
202
+ @source_credentials.principal
203
+ else
204
+ :unknown
205
+ end
206
+ end
207
+
194
208
  private
195
209
 
196
210
  # Generates a new impersonation access token by exchanging the source credentials' token
@@ -200,21 +214,16 @@ module Google
200
214
  # for an impersonation token using the specified impersonation URL. The generated token and
201
215
  # its expiration time are cached for subsequent use.
202
216
  #
217
+ # @private
203
218
  # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
204
219
  #
205
- # @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
206
- # @raise [Signet::AuthorizationError] For other unexpected response statuses.
220
+ # @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500.
221
+ # @raise [Google::Auth::AuthorizationError] For other unexpected response statuses.
207
222
  #
208
223
  # @return [String] The newly generated impersonation access token.
209
224
  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
225
+ auth_header = prepare_auth_header
226
+ resp = make_impersonation_request auth_header
218
227
 
219
228
  case resp.status
220
229
  when 200
@@ -223,14 +232,51 @@ module Google
223
232
  @access_token = response["accessToken"]
224
233
  access_token
225
234
  when 403, 500
226
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
227
- raise Signet::UnexpectedStatusError, msg
235
+ handle_error_response resp, UnexpectedStatusError
228
236
  else
229
- msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
230
- raise Signet::AuthorizationError, msg
237
+ handle_error_response resp, AuthorizationError
238
+ end
239
+ end
240
+
241
+ # Prepares the authorization header for the impersonation request
242
+ # by fetching a token from source credentials.
243
+ #
244
+ # @private
245
+ # @return [Hash] The authorization header with the source credentials' token
246
+ def prepare_auth_header
247
+ auth_header = {}
248
+ @source_credentials.updater_proc.call auth_header
249
+ auth_header
250
+ end
251
+
252
+ # Makes the HTTP request to the impersonation endpoint.
253
+ #
254
+ # @private
255
+ # @param [Hash] auth_header The authorization header containing the source token
256
+ # @return [Faraday::Response] The HTTP response from the impersonation endpoint
257
+ def make_impersonation_request auth_header
258
+ connection.post @impersonation_url do |req|
259
+ req.headers.merge! auth_header
260
+ req.headers["Content-Type"] = "application/json"
261
+ req.body = MultiJson.dump({ scope: @scope })
231
262
  end
232
263
  end
233
264
 
265
+ # Creates and raises an appropriate error based on the response.
266
+ #
267
+ # @private
268
+ # @param [Faraday::Response] resp The HTTP response
269
+ # @param [Class] error_class The error class to instantiate
270
+ # @raise [StandardError] The appropriate error with details
271
+ def handle_error_response resp, error_class
272
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
273
+ raise error_class.with_details(
274
+ msg,
275
+ credential_type_name: self.class.name,
276
+ principal: principal
277
+ )
278
+ end
279
+
234
280
  # Setter for the expires_at value that makes sure it is converted
235
281
  # to Time object.
236
282
  def expires_at= new_expires_at
@@ -249,7 +295,7 @@ module Google
249
295
  #
250
296
  # @return [Time, nil] The normalized Time object, or nil if the input is nil.
251
297
  #
252
- # @raise [RuntimeError] If the input is not a Time, String, or nil.
298
+ # @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil.
253
299
  def normalize_timestamp time
254
300
  case time
255
301
  when NilClass
@@ -259,7 +305,8 @@ module Google
259
305
  when String
260
306
  Time.parse time
261
307
  else
262
- raise "Invalid time value #{time}"
308
+ message = "Invalid time value #{time}"
309
+ raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal)
263
310
  end
264
311
  end
265
312
 
@@ -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
@@ -63,12 +63,14 @@ module Google
63
63
  # @param [String] code_verifier
64
64
  # Random string of 43-128 chars used to verify the key exchange using
65
65
  # PKCE.
66
+ # @raise [Google::Auth::InitializationError]
67
+ # If client_id is nil or scope is nil
66
68
  def initialize client_id, scope, token_store,
67
69
  legacy_callback_uri = nil,
68
70
  callback_uri: nil,
69
71
  code_verifier: nil
70
- raise NIL_CLIENT_ID_ERROR if client_id.nil?
71
- raise NIL_SCOPE_ERROR if scope.nil?
72
+ raise InitializationError, NIL_CLIENT_ID_ERROR if client_id.nil?
73
+ raise InitializationError, NIL_SCOPE_ERROR if scope.nil?
72
74
 
73
75
  @client_id = client_id
74
76
  @scope = Array(scope)
@@ -133,14 +135,19 @@ module Google
133
135
  # the requested scopes
134
136
  # @return [Google::Auth::UserRefreshCredentials]
135
137
  # Stored credentials, nil if none present
138
+ # @raise [Google::Auth::CredentialsError]
139
+ # If the client ID in the stored token doesn't match the configured client ID
136
140
  def get_credentials user_id, scope = nil
137
141
  saved_token = stored_token user_id
138
142
  return nil if saved_token.nil?
139
143
  data = MultiJson.load saved_token
140
144
 
141
145
  if data.fetch("client_id", @client_id.id) != @client_id.id
142
- raise format(MISMATCHED_CLIENT_ID_ERROR,
143
- data["client_id"], @client_id.id)
146
+ raise CredentialsError.with_details(
147
+ format(MISMATCHED_CLIENT_ID_ERROR, data["client_id"], @client_id.id),
148
+ credential_type_name: self.class.name,
149
+ principal: principal
150
+ )
144
151
  end
145
152
 
146
153
  credentials = UserRefreshCredentials.new(
@@ -240,6 +247,8 @@ module Google
240
247
  # Unique ID of the user for loading/storing credentials.
241
248
  # @param [Google::Auth::UserRefreshCredentials] credentials
242
249
  # Credentials to store.
250
+ # @return [Google::Auth::UserRefreshCredentials]
251
+ # The stored credentials
243
252
  def store_credentials user_id, credentials
244
253
  json = MultiJson.dump(
245
254
  client_id: credentials.client_id,
@@ -269,6 +278,15 @@ module Google
269
278
  SecureRandom.alphanumeric random_number
270
279
  end
271
280
 
281
+ # Returns the principal identifier for this authorizer
282
+ # The client ID is used as the principal for user authorizers
283
+ #
284
+ # @private
285
+ # @return [String] The client ID associated with this authorizer
286
+ def principal
287
+ @client_id.id
288
+ end
289
+
272
290
  private
273
291
 
274
292
  # @private Fetch stored token with given user_id
@@ -276,9 +294,11 @@ module Google
276
294
  # @param [String] user_id
277
295
  # Unique ID of the user for loading/storing credentials.
278
296
  # @return [String] The saved token from @token_store
297
+ # @raise [Google::Auth::InitializationError]
298
+ # If user_id is nil or token_store is nil
279
299
  def stored_token user_id
280
- raise NIL_USER_ID_ERROR if user_id.nil?
281
- raise NIL_TOKEN_STORE_ERROR if @token_store.nil?
300
+ raise InitializationError, NIL_USER_ID_ERROR if user_id.nil?
301
+ raise InitializationError, NIL_TOKEN_STORE_ERROR if @token_store.nil?
282
302
 
283
303
  @token_store.load user_id
284
304
  end
@@ -303,9 +323,17 @@ module Google
303
323
  # Absolute URL to resolve the callback against if necessary.
304
324
  # @return [String]
305
325
  # Redirect URI
326
+ # @raise [Google::Auth::CredentialsError]
327
+ # If the callback URI is relative and base_url is nil or not absolute
306
328
  def redirect_uri_for base_url
307
329
  return @callback_uri if uri_is_postmessage?(@callback_uri) || !URI(@callback_uri).scheme.nil?
308
- raise format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
330
+ if base_url.nil? || URI(base_url).scheme.nil?
331
+ raise CredentialsError.with_details(
332
+ format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri),
333
+ credential_type_name: self.class.name,
334
+ principal: principal
335
+ )
336
+ end
309
337
  URI.join(base_url, @callback_uri).to_s
310
338
  end
311
339
 
@@ -12,9 +12,10 @@
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/credentials_loader"
16
+ require "googleauth/errors"
17
17
  require "googleauth/scope_util"
18
+ require "googleauth/signet"
18
19
  require "multi_json"
19
20
 
20
21
  module Google
@@ -38,21 +39,29 @@ module Google
38
39
  attr_reader :project_id
39
40
  attr_reader :quota_project_id
40
41
 
42
+ # @private
43
+ # @type [::String] The type name for this credential.
44
+ CREDENTIAL_TYPE_NAME = "authorized_user".freeze
45
+
41
46
  # Create a UserRefreshCredentials.
42
47
  #
43
- # @param json_key_io [IO] an IO from which the JSON key can be read
48
+ # @param json_key_io [IO] An IO object containing the JSON key
44
49
  # @param scope [string|array|nil] the scope(s) to access
45
50
  def self.make_creds options = {}
46
51
  json_key_io, scope = options.values_at :json_key_io, :scope
47
- user_creds = read_json_key json_key_io if json_key_io
48
- user_creds ||= {
49
- "client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR],
50
- "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
51
- "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
52
- "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR],
53
- "quota_project_id" => nil,
54
- "universe_domain" => nil
55
- }
52
+ user_creds = if json_key_io
53
+ CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME
54
+ read_json_key json_key_io
55
+ else
56
+ {
57
+ "client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR],
58
+ "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
59
+ "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
60
+ "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR],
61
+ "quota_project_id" => nil,
62
+ "universe_domain" => nil
63
+ }
64
+ end
56
65
  new(token_credential_uri: TOKEN_CRED_URI,
57
66
  client_id: user_creds["client_id"],
58
67
  client_secret: user_creds["client_secret"],
@@ -64,13 +73,16 @@ module Google
64
73
  .configure_connection(options)
65
74
  end
66
75
 
67
- # Reads the client_id, client_secret and refresh_token fields from the
68
- # JSON key.
76
+ # Reads a JSON key from an IO object and extracts required fields.
77
+ #
78
+ # @param [IO] json_key_io An IO object containing the JSON key
79
+ # @return [Hash] The parsed JSON key
80
+ # @raise [Google::Auth::InitializationError] If the JSON is missing required fields
69
81
  def self.read_json_key json_key_io
70
82
  json_key = MultiJson.load json_key_io.read
71
83
  wanted = ["client_id", "client_secret", "refresh_token"]
72
84
  wanted.each do |key|
73
- raise "the json is missing the #{key} field" unless json_key.key? key
85
+ raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key
74
86
  end
75
87
  json_key
76
88
  end
@@ -106,6 +118,10 @@ module Google
106
118
  end
107
119
 
108
120
  # Revokes the credential
121
+ #
122
+ # @param [Hash] options Options for revoking the credential
123
+ # @option options [Faraday::Connection] :connection The connection to use
124
+ # @raise [Google::Auth::AuthorizationError] If the revocation request fails
109
125
  def revoke! options = {}
110
126
  c = options[:connection] || Faraday.default_connection
111
127
 
@@ -117,8 +133,11 @@ module Google
117
133
  self.refresh_token = nil
118
134
  self.expires_at = 0
119
135
  else
120
- raise(Signet::AuthorizationError,
121
- "Unexpected error code #{resp.status}")
136
+ raise AuthorizationError.with_details(
137
+ "Unexpected error code #{resp.status}",
138
+ credential_type_name: self.class.name,
139
+ principal: principal
140
+ )
122
141
  end
123
142
  end
124
143
  end
@@ -157,6 +176,13 @@ module Google
157
176
 
158
177
  self
159
178
  end
179
+
180
+ # Returns the client ID as the principal for user refresh credentials
181
+ # @private
182
+ # @return [String, Symbol] the client ID or :user_refresh if not available
183
+ def principal
184
+ @client_id || :user_refresh
185
+ end
160
186
  end
161
187
  end
162
188
  end
@@ -16,6 +16,6 @@ module Google
16
16
  # Module Auth provides classes that provide Google-specific authorization
17
17
  # used to access Google APIs.
18
18
  module Auth
19
- VERSION = "1.14.0".freeze
19
+ VERSION = "1.15.1".freeze
20
20
  end
21
21
  end