googleauth 0.11.0 → 0.15.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.
@@ -51,20 +51,43 @@ module Google
51
51
  class GCECredentials < Signet::OAuth2::Client
52
52
  # The IP Address is used in the URIs to speed up failures on non-GCE
53
53
  # systems.
54
- COMPUTE_AUTH_TOKEN_URI = "http://169.254.169.254/computeMetadata/v1/"\
55
- "instance/service-accounts/default/token".freeze
54
+ DEFAULT_METADATA_HOST = "169.254.169.254".freeze
55
+
56
+ # @private Unused and deprecated
57
+ COMPUTE_AUTH_TOKEN_URI =
58
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
59
+ # @private Unused and deprecated
60
+ COMPUTE_ID_TOKEN_URI =
61
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
62
+ # @private Unused and deprecated
56
63
  COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
57
64
 
58
65
  class << self
59
66
  extend Memoist
60
67
 
68
+ def metadata_host
69
+ ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
70
+ end
71
+
72
+ def compute_check_uri
73
+ "http://#{metadata_host}".freeze
74
+ end
75
+
76
+ def compute_auth_token_uri
77
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
78
+ end
79
+
80
+ def compute_id_token_uri
81
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
82
+ end
83
+
61
84
  # Detect if this appear to be a GCE instance, by checking if metadata
62
85
  # is available.
63
86
  def on_gce? options = {}
64
87
  # TODO: This should use google-cloud-env instead.
65
88
  c = options[:connection] || Faraday.default_connection
66
89
  headers = { "Metadata-Flavor" => "Google" }
67
- resp = c.get COMPUTE_CHECK_URI, nil, headers do |req|
90
+ resp = c.get compute_check_uri, nil, headers do |req|
68
91
  req.options.timeout = 1.0
69
92
  req.options.open_timeout = 0.1
70
93
  end
@@ -82,17 +105,25 @@ module Google
82
105
  def fetch_access_token options = {}
83
106
  c = options[:connection] || Faraday.default_connection
84
107
  retry_with_error do
85
- headers = { "Metadata-Flavor" => "Google" }
86
- resp = c.get COMPUTE_AUTH_TOKEN_URI, nil, headers
108
+ uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
109
+ query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
110
+ query[:scopes] = Array(scope).join "," if scope
111
+ resp = c.get uri, query, "Metadata-Flavor" => "Google"
87
112
  case resp.status
88
113
  when 200
89
- Signet::OAuth2.parse_credentials(resp.body,
90
- resp.headers["content-type"])
114
+ content_type = resp.headers["content-type"]
115
+ if content_type == "text/html"
116
+ { (target_audience ? "id_token" : "access_token") => resp.body }
117
+ else
118
+ Signet::OAuth2.parse_credentials resp.body, content_type
119
+ end
120
+ when 403, 500
121
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
122
+ raise Signet::UnexpectedStatusError, msg
91
123
  when 404
92
124
  raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
93
125
  else
94
- msg = "Unexpected error code #{resp.status}" \
95
- "#{UNEXPECTED_ERROR_SUFFIX}"
126
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
96
127
  raise Signet::AuthorizationError, msg
97
128
  end
98
129
  end
@@ -36,9 +36,46 @@ require "googleauth/credentials_loader"
36
36
  module Google
37
37
  module Auth
38
38
  ##
39
- # Credentials is responsible for representing the authentication when connecting to an API. This
40
- # class is also intended to be inherited by API-specific classes.
41
- class Credentials
39
+ # Credentials is a high-level base class used by Google's API client
40
+ # libraries to represent the authentication when connecting to an API.
41
+ # In most cases, it is subclassed by API-specific credential classes that
42
+ # can be instantiated by clients.
43
+ #
44
+ # ## Options
45
+ #
46
+ # Credentials classes are configured with options that dictate default
47
+ # values for parameters such as scope and audience. These defaults are
48
+ # expressed as class attributes, and may differ from endpoint to endpoint.
49
+ # Normally, an API client will provide subclasses specific to each
50
+ # endpoint, configured with appropriate values.
51
+ #
52
+ # Note that these options inherit up the class hierarchy. If a particular
53
+ # options is not set for a subclass, its superclass is queried.
54
+ #
55
+ # Some older users of this class set options via constants. This usage is
56
+ # deprecated. For example, instead of setting the `AUDIENCE` constant on
57
+ # your subclass, call the `audience=` method.
58
+ #
59
+ # ## Example
60
+ #
61
+ # class MyCredentials < Google::Auth::Credentials
62
+ # # Set the default scope for these credentials
63
+ # self.scope = "http://example.com/my_scope"
64
+ # end
65
+ #
66
+ # # creds is a credentials object suitable for Google API clients
67
+ # creds = MyCredentials.default
68
+ # creds.scope # => ["http://example.com/my_scope"]
69
+ #
70
+ # class SubCredentials < MyCredentials
71
+ # # Override the default scope for this subclass
72
+ # self.scope = "http://example.com/sub_scope"
73
+ # end
74
+ #
75
+ # creds2 = SubCredentials.default
76
+ # creds2.scope # => ["http://example.com/sub_scope"]
77
+ #
78
+ class Credentials # rubocop:disable Metrics/ClassLength
42
79
  ##
43
80
  # The default token credential URI to be used when none is provided during initialization.
44
81
  TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze
@@ -47,6 +84,8 @@ module Google
47
84
  # The default target audience ID to be used when none is provided during initialization.
48
85
  AUDIENCE = "https://oauth2.googleapis.com/token".freeze
49
86
 
87
+ @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil
88
+
50
89
  ##
51
90
  # The default token credential URI to be used when none is provided during initialization.
52
91
  # The URI is the authorization server's HTTP endpoint capable of issuing tokens and
@@ -55,9 +94,9 @@ module Google
55
94
  # @return [String]
56
95
  #
57
96
  def self.token_credential_uri
58
- return @token_credential_uri unless @token_credential_uri.nil?
59
-
60
- const_get :TOKEN_CREDENTIAL_URI if const_defined? :TOKEN_CREDENTIAL_URI
97
+ lookup_auth_param :token_credential_uri do
98
+ lookup_local_constant :TOKEN_CREDENTIAL_URI
99
+ end
61
100
  end
62
101
 
63
102
  ##
@@ -77,9 +116,9 @@ module Google
77
116
  # @return [String]
78
117
  #
79
118
  def self.audience
80
- return @audience unless @audience.nil?
81
-
82
- const_get :AUDIENCE if const_defined? :AUDIENCE
119
+ lookup_auth_param :audience do
120
+ lookup_local_constant :AUDIENCE
121
+ end
83
122
  end
84
123
 
85
124
  ##
@@ -97,20 +136,26 @@ module Google
97
136
  # A scope is an access range defined by the authorization server.
98
137
  # The scope can be a single value or a list of values.
99
138
  #
139
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
140
+ # If {#scope} is set, this credential will produce access tokens.
141
+ # If {#target_audience} is set, this credential will produce ID tokens.
142
+ #
100
143
  # @return [String, Array<String>]
101
144
  #
102
145
  def self.scope
103
- return @scope unless @scope.nil?
104
-
105
- tmp_scope = []
106
- # Pull in values is the SCOPE constant exists.
107
- tmp_scope << const_get(:SCOPE) if const_defined? :SCOPE
108
- tmp_scope.flatten.uniq
146
+ lookup_auth_param :scope do
147
+ vals = lookup_local_constant :SCOPE
148
+ vals ? Array(vals).flatten.uniq : nil
149
+ end
109
150
  end
110
151
 
111
152
  ##
112
153
  # Sets the default scope to be used when none is provided during initialization.
113
154
  #
155
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
156
+ # If {#scope} is set, this credential will produce access tokens.
157
+ # If {#target_audience} is set, this credential will produce ID tokens.
158
+ #
114
159
  # @param [String, Array<String>] new_scope
115
160
  # @return [String, Array<String>]
116
161
  #
@@ -119,6 +164,34 @@ module Google
119
164
  @scope = new_scope
120
165
  end
121
166
 
167
+ ##
168
+ # The default final target audience for ID tokens, to be used when none
169
+ # is provided during initialization.
170
+ #
171
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
172
+ # If {#scope} is set, this credential will produce access tokens.
173
+ # If {#target_audience} is set, this credential will produce ID tokens.
174
+ #
175
+ # @return [String]
176
+ #
177
+ def self.target_audience
178
+ lookup_auth_param :target_audience
179
+ end
180
+
181
+ ##
182
+ # Sets the default final target audience for ID tokens, to be used when none
183
+ # is provided during initialization.
184
+ #
185
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
186
+ # If {#scope} is set, this credential will produce access tokens.
187
+ # If {#target_audience} is set, this credential will produce ID tokens.
188
+ #
189
+ # @param [String] new_target_audience
190
+ #
191
+ def self.target_audience= new_target_audience
192
+ @target_audience = new_target_audience
193
+ end
194
+
122
195
  ##
123
196
  # The environment variables to search for credentials. Values can either be a file path to the
124
197
  # credentials file, or the JSON contents of the credentials file.
@@ -126,13 +199,12 @@ module Google
126
199
  # @return [Array<String>]
127
200
  #
128
201
  def self.env_vars
129
- return @env_vars unless @env_vars.nil?
130
-
131
- # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
132
- tmp_env_vars = []
133
- tmp_env_vars << const_get(:PATH_ENV_VARS) if const_defined? :PATH_ENV_VARS
134
- tmp_env_vars << const_get(:JSON_ENV_VARS) if const_defined? :JSON_ENV_VARS
135
- tmp_env_vars.flatten.uniq
202
+ lookup_auth_param :env_vars do
203
+ # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
204
+ path_env_vars = lookup_local_constant :PATH_ENV_VARS
205
+ json_env_vars = lookup_local_constant :JSON_ENV_VARS
206
+ (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars
207
+ end
136
208
  end
137
209
 
138
210
  ##
@@ -152,12 +224,11 @@ module Google
152
224
  # @return [Array<String>]
153
225
  #
154
226
  def self.paths
155
- return @paths unless @paths.nil?
156
-
157
- tmp_paths = []
158
- # Pull in values is the DEFAULT_PATHS constant exists.
159
- tmp_paths << const_get(:DEFAULT_PATHS) if const_defined? :DEFAULT_PATHS
160
- tmp_paths.flatten.uniq
227
+ lookup_auth_param :paths do
228
+ # Pull in values if the DEFAULT_PATHS constant exists.
229
+ vals = lookup_local_constant :DEFAULT_PATHS
230
+ vals ? Array(vals).flatten.uniq : nil
231
+ end
161
232
  end
162
233
 
163
234
  ##
@@ -171,6 +242,39 @@ module Google
171
242
  @paths = new_paths
172
243
  end
173
244
 
245
+ ##
246
+ # @private
247
+ # Return the given parameter value, defaulting up the class hierarchy.
248
+ #
249
+ # First returns the value of the instance variable, if set.
250
+ # Next, calls the given block if provided. (This is generally used to
251
+ # look up legacy constant-based values.)
252
+ # Otherwise, calls the superclass method if present.
253
+ # Returns nil if all steps fail.
254
+ #
255
+ # @param [Symbol] The parameter name
256
+ # @return [Object] The value
257
+ #
258
+ def self.lookup_auth_param name
259
+ val = instance_variable_get "@#{name}".to_sym
260
+ val = yield if val.nil? && block_given?
261
+ return val unless val.nil?
262
+ return superclass.send name if superclass.respond_to? name
263
+ nil
264
+ end
265
+
266
+ ##
267
+ # @private
268
+ # Return the value of the given constant if it is defined directly in
269
+ # this class, or nil if not.
270
+ #
271
+ # @param [Symbol] Name of the constant
272
+ # @return [Object] The value
273
+ #
274
+ def self.lookup_local_constant name
275
+ const_defined?(name, false) ? const_get(name) : nil
276
+ end
277
+
174
278
  ##
175
279
  # The Signet::OAuth2::Client object the Credentials instance is using.
176
280
  #
@@ -185,6 +289,13 @@ module Google
185
289
  #
186
290
  attr_reader :project_id
187
291
 
292
+ ##
293
+ # Identifier for a separate project used for billing/quota, if any.
294
+ #
295
+ # @return [String,nil]
296
+ #
297
+ attr_reader :quota_project_id
298
+
188
299
  # @private Delegate client methods to the client object.
189
300
  extend Forwardable
190
301
 
@@ -201,6 +312,9 @@ module Google
201
312
  # @return [String, Array<String>] The scope for this client. A scope is an access range
202
313
  # defined by the authorization server. The scope can be a single value or a list of values.
203
314
  #
315
+ # @!attribute [r] target_audience
316
+ # @return [String] The final target audience for ID tokens returned by this credential.
317
+ #
204
318
  # @!attribute [r] issuer
205
319
  # @return [String] The issuer ID associated with this client.
206
320
  #
@@ -213,9 +327,7 @@ module Google
213
327
  #
214
328
  def_delegators :@client,
215
329
  :token_credential_uri, :audience,
216
- :scope, :issuer, :signing_key, :updater_proc
217
-
218
- # rubocop:disable Metrics/AbcSize
330
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience
219
331
 
220
332
  ##
221
333
  # Creates a new Credentials instance with the provided auth credentials, and with the default
@@ -236,23 +348,15 @@ module Google
236
348
  # * +:default_connection+ - the default connection to use for the client
237
349
  #
238
350
  def initialize keyfile, options = {}
239
- scope = options[:scope]
240
351
  verify_keyfile_provided! keyfile
241
352
  @project_id = options["project_id"] || options["project"]
353
+ @quota_project_id = options["quota_project_id"]
242
354
  if keyfile.is_a? Signet::OAuth2::Client
243
- @client = keyfile
244
- @project_id ||= keyfile.project_id if keyfile.respond_to? :project_id
355
+ update_from_signet keyfile
245
356
  elsif keyfile.is_a? Hash
246
- hash = stringify_hash_keys keyfile
247
- hash["scope"] ||= scope
248
- @client = init_client hash, options
249
- @project_id ||= (hash["project_id"] || hash["project"])
357
+ update_from_hash keyfile, options
250
358
  else
251
- verify_keyfile_exists! keyfile
252
- json = JSON.parse ::File.read(keyfile)
253
- json["scope"] ||= scope
254
- @project_id ||= (json["project_id"] || json["project"])
255
- @client = init_client json, options
359
+ update_from_filepath keyfile, options
256
360
  end
257
361
  CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
258
362
  @project_id ||= CredentialsLoader.load_gcloud_project_id
@@ -261,7 +365,6 @@ module Google
261
365
  @paths = nil
262
366
  @scope = nil
263
367
  end
264
- # rubocop:enable Metrics/AbcSize
265
368
 
266
369
  ##
267
370
  # Creates a new Credentials instance with auth credentials acquired by searching the
@@ -302,8 +405,15 @@ module Google
302
405
  env_vars.each do |env_var|
303
406
  str = ENV[env_var]
304
407
  next if str.nil?
305
- return new str, options if ::File.file? str
306
- return new ::JSON.parse(str), options rescue nil
408
+ io =
409
+ if ::File.file? str
410
+ ::StringIO.new ::File.read str
411
+ else
412
+ json = ::JSON.parse str rescue nil
413
+ json ? ::StringIO.new(str) : nil
414
+ end
415
+ next if io.nil?
416
+ return from_io io, options
307
417
  end
308
418
  nil
309
419
  end
@@ -311,11 +421,11 @@ module Google
311
421
  ##
312
422
  # @private Lookup Credentials from default file paths.
313
423
  def self.from_default_paths options
314
- paths
315
- .select { |p| ::File.file? p }
316
- .each do |file|
317
- return new file, options
318
- end
424
+ paths.each do |path|
425
+ next unless path && ::File.file?(path)
426
+ io = ::StringIO.new ::File.read path
427
+ return from_io io, options
428
+ end
319
429
  nil
320
430
  end
321
431
 
@@ -323,13 +433,34 @@ module Google
323
433
  # @private Lookup Credentials using Google::Auth.get_application_default.
324
434
  def self.from_application_default options
325
435
  scope = options[:scope] || self.scope
326
- client = Google::Auth.get_application_default scope
436
+ auth_opts = {
437
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
438
+ audience: options[:audience] || audience,
439
+ target_audience: options[:target_audience] || target_audience,
440
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?
441
+ }
442
+ client = Google::Auth.get_application_default scope, auth_opts
327
443
  new client, options
328
444
  end
329
445
 
446
+ # @private Read credentials from a JSON stream.
447
+ def self.from_io io, options
448
+ creds_input = {
449
+ json_key_io: io,
450
+ scope: options[:scope] || scope,
451
+ target_audience: options[:target_audience] || target_audience,
452
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?,
453
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
454
+ audience: options[:audience] || audience
455
+ }
456
+ client = Google::Auth::DefaultCredentials.make_creds creds_input
457
+ new client
458
+ end
459
+
330
460
  private_class_method :from_env_vars,
331
461
  :from_default_paths,
332
- :from_application_default
462
+ :from_application_default,
463
+ :from_io
333
464
 
334
465
  protected
335
466
 
@@ -362,14 +493,46 @@ module Google
362
493
  options["token_credential_uri"] ||= self.class.token_credential_uri
363
494
  options["audience"] ||= self.class.audience
364
495
  options["scope"] ||= self.class.scope
496
+ options["target_audience"] ||= self.class.target_audience
497
+
498
+ if !Array(options["scope"]).empty? && options["target_audience"]
499
+ raise ArgumentError, "Cannot specify both scope and target_audience"
500
+ end
365
501
 
502
+ needs_scope = options["target_audience"].nil?
366
503
  # client options for initializing signet client
367
504
  { token_credential_uri: options["token_credential_uri"],
368
505
  audience: options["audience"],
369
- scope: Array(options["scope"]),
506
+ scope: (needs_scope ? Array(options["scope"]) : nil),
507
+ target_audience: options["target_audience"],
370
508
  issuer: options["client_email"],
371
509
  signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
372
510
  end
511
+
512
+ def update_from_signet client
513
+ @project_id ||= client.project_id if client.respond_to? :project_id
514
+ @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
515
+ @client = client
516
+ end
517
+
518
+ def update_from_hash hash, options
519
+ hash = stringify_hash_keys hash
520
+ hash["scope"] ||= options[:scope]
521
+ hash["target_audience"] ||= options[:target_audience]
522
+ @project_id ||= (hash["project_id"] || hash["project"])
523
+ @quota_project_id ||= hash["quota_project_id"]
524
+ @client = init_client hash, options
525
+ end
526
+
527
+ def update_from_filepath path, options
528
+ verify_keyfile_exists! path
529
+ json = JSON.parse ::File.read(path)
530
+ json["scope"] ||= options[:scope]
531
+ json["target_audience"] ||= options[:target_audience]
532
+ @project_id ||= (json["project_id"] || json["project"])
533
+ @quota_project_id ||= json["quota_project_id"]
534
+ @client = init_client json, options
535
+ end
373
536
  end
374
537
  end
375
538
  end