googleauth 1.8.1 → 1.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37795e56189392a97d5941b4982730ffa2ee1dd53ec63593bbb7f5ad50be044e
4
- data.tar.gz: 72d6b584c89c321698485bda1e4aef33f361ed35da4762bd22b5dc4634556f8f
3
+ metadata.gz: 4912a601c0a234fa9faf150d7461cab993f775b715f6ac7d19db017ceae74e6d
4
+ data.tar.gz: e08da21e12d58260944079d068a04099fa0c812eb486e760a3ab12dd002700f4
5
5
  SHA512:
6
- metadata.gz: 1dcfc8f1e8e65f9b4b27c4933e71faffd2998d11766307cf464d0551d5f37e41c266e840a340a9aba41d49f44d05005006077526851b1b33e277ce3155d16370
7
- data.tar.gz: f036b3403998c93f41eae33c472ad714758e6c6d06e9a1b197b53d118d182c2abb9caf40636ff63c152e2186642430de824e7929dcd2978cd358a5bd03194c1c
6
+ metadata.gz: b0346fcaf38cb783fd4d22f0734994298d63dfa1a89fd34df4d4f42b87160de410e34c93ab4773c3bbaf03b41160f10e3fd5bc0fa137d1ab6fb5dce15f72ba53
7
+ data.tar.gz: c5ff10a04491e9f56dff9bcea24c67398e67713efc89c2d378075621da837b59c5fdbd36c0b97d5753fd0358befe8ce2ceacc86af1cce0a1c54d609d916903c6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Release History
2
2
 
3
+ ### 1.11.0 (2024-02-09)
4
+
5
+ #### Features
6
+
7
+ * Deprecate the positional argument for callback_uri, and introduce keyword argument instead ([#475](https://github.com/googleapis/google-auth-library-ruby/issues/475))
8
+
9
+ ### 1.10.0 (2024-02-08)
10
+
11
+ #### Features
12
+
13
+ * add PKCE to 3 Legged OAuth exchange ([#471](https://github.com/googleapis/google-auth-library-ruby/issues/471))
14
+ #### Bug Fixes
15
+
16
+ * Client library credentials provide correct self-signed JWT and external account behavior when loading from a file path or JSON data ([#474](https://github.com/googleapis/google-auth-library-ruby/issues/474))
17
+ * Prioritize universe domain specified in GCECredentials arguments over metadata-fetched value ([#472](https://github.com/googleapis/google-auth-library-ruby/issues/472))
18
+
19
+ ### 1.9.2 (2024-01-25)
20
+
21
+ #### Bug Fixes
22
+
23
+ * Prevent access tokens from being fetched at service account construction in the self-signed-jwt case ([#467](https://github.com/googleapis/google-auth-library-ruby/issues/467))
24
+
25
+ ### 1.9.1 (2023-12-12)
26
+
27
+ #### Bug Fixes
28
+
29
+ * update expires_in for cached metadata-retrieved tokens ([#464](https://github.com/googleapis/google-auth-library-ruby/issues/464))
30
+
31
+ ### 1.9.0 (2023-12-07)
32
+
33
+ #### Features
34
+
35
+ * Include universe_domain in credentials ([#460](https://github.com/googleapis/google-auth-library-ruby/issues/460))
36
+ * Use google-cloud-env for more robust Metadata Service access ([#459](https://github.com/googleapis/google-auth-library-ruby/issues/459))
37
+
3
38
  ### 1.8.1 (2023-09-19)
4
39
 
5
40
  #### Documentation
data/README.md CHANGED
@@ -97,6 +97,45 @@ get('/oauth2callback') do
97
97
  end
98
98
  ```
99
99
 
100
+ ### Example (Web with PKCE)
101
+
102
+ Proof Key for Code Exchange (PKCE) is an [RFC](https://www.rfc-editor.org/rfc/rfc7636) that aims to prevent malicious operating system processes from hijacking an OAUTH 2.0 exchange. PKCE mitigates the above vulnerability by including `code_challenge` and `code_challenge_method` parameters in the Authorization Request and a `code_verifier` parameter in the Access Token Request.
103
+
104
+ ```ruby
105
+ require 'googleauth'
106
+ require 'googleauth/web_user_authorizer'
107
+ require 'googleauth/stores/redis_token_store'
108
+ require 'redis'
109
+
110
+ client_id = Google::Auth::ClientId.from_file('/path/to/client_secrets.json')
111
+ scope = ['https://www.googleapis.com/auth/drive']
112
+ token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new)
113
+ authorizer = Google::Auth::WebUserAuthorizer.new(
114
+ client_id, scope, token_store, '/oauth2callback')
115
+
116
+
117
+ get('/authorize') do
118
+ # NOTE: Assumes the user is already authenticated to the app
119
+ user_id = request.session['user_id']
120
+ # User needs to take care of generating the code_verifier and storing it in
121
+ # the session.
122
+ request.session['code_verifier'] ||= Google::Auth::WebUserAuthorizer.generate_code_verifier
123
+ authorizer.code_verifier = request.session['code_verifier']
124
+ credentials = authorizer.get_credentials(user_id, request)
125
+ if credentials.nil?
126
+ redirect authorizer.get_authorization_url(login_hint: user_id, request: request)
127
+ end
128
+ # Credentials are valid, can call APIs
129
+ # ...
130
+ end
131
+
132
+ get('/oauth2callback') do
133
+ target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(
134
+ request)
135
+ redirect target_url
136
+ end
137
+ ```
138
+
100
139
  ### Example (Command Line) [Deprecated]
101
140
 
102
141
  The Google Auth OOB flow has been discontiued on January 31, 2023. The OOB flow is a legacy flow that is no longer considered secure. To continue using Google Auth, please migrate your applications to a more secure flow. For more information on how to do this, please refer to this [OOB Migration](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration) guide.
@@ -55,11 +55,7 @@ module Google
55
55
  DefaultCredentials.from_well_known_path(scope, options) ||
56
56
  DefaultCredentials.from_system_default_path(scope, options)
57
57
  return creds unless creds.nil?
58
- unless GCECredentials.on_gce? options
59
- # Clear cache of the result of GCECredentials.on_gce?
60
- GCECredentials.reset_cache
61
- raise NOT_FOUND_ERROR
62
- end
58
+ raise NOT_FOUND_ERROR unless GCECredentials.on_gce? options
63
59
  GCECredentials.new options.merge(scope: scope)
64
60
  end
65
61
  end
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "faraday"
15
+ require "google-cloud-env"
16
16
  require "googleauth/signet"
17
17
 
18
18
  module Google
@@ -33,83 +33,77 @@ module Google
33
33
  # Extends Signet::OAuth2::Client so that the auth token is obtained from
34
34
  # the GCE metadata server.
35
35
  class GCECredentials < Signet::OAuth2::Client
36
- # The IP Address is used in the URIs to speed up failures on non-GCE
37
- # systems.
36
+ # @private Unused and deprecated but retained to prevent breaking changes
38
37
  DEFAULT_METADATA_HOST = "169.254.169.254".freeze
39
38
 
40
- # @private Unused and deprecated
39
+ # @private Unused and deprecated but retained to prevent breaking changes
41
40
  COMPUTE_AUTH_TOKEN_URI =
42
41
  "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
43
- # @private Unused and deprecated
42
+ # @private Unused and deprecated but retained to prevent breaking changes
44
43
  COMPUTE_ID_TOKEN_URI =
45
44
  "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
46
- # @private Unused and deprecated
45
+ # @private Unused and deprecated but retained to prevent breaking changes
47
46
  COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
48
47
 
49
- @on_gce_cache = {}
50
-
51
48
  class << self
49
+ # @private Unused and deprecated
52
50
  def metadata_host
53
51
  ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
54
52
  end
55
53
 
54
+ # @private Unused and deprecated
56
55
  def compute_check_uri
57
56
  "http://#{metadata_host}".freeze
58
57
  end
59
58
 
59
+ # @private Unused and deprecated
60
60
  def compute_auth_token_uri
61
61
  "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
62
62
  end
63
63
 
64
+ # @private Unused and deprecated
64
65
  def compute_id_token_uri
65
66
  "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
66
67
  end
67
68
 
68
69
  # Detect if this appear to be a GCE instance, by checking if metadata
69
70
  # is available.
70
- def on_gce? options = {}, reload = false # rubocop:disable Style/OptionalBooleanParameter
71
- # We can follow OptionalBooleanParameter here because it's a public interface, we can't change it.
72
- @on_gce_cache.delete options if reload
73
- @on_gce_cache.fetch options do
74
- @on_gce_cache[options] = begin
75
- # TODO: This should use google-cloud-env instead.
76
- c = options[:connection] || Faraday.default_connection
77
- headers = { "Metadata-Flavor" => "Google" }
78
- resp = c.get compute_check_uri, nil, headers do |req|
79
- req.options.timeout = 1.0
80
- req.options.open_timeout = 0.1
81
- end
82
- return false unless resp.status == 200
83
- resp.headers["Metadata-Flavor"] == "Google"
84
- rescue Faraday::TimeoutError, Faraday::ConnectionFailed
85
- false
86
- end
87
- end
71
+ # The parameters are deprecated and unused.
72
+ def on_gce? _options = {}, _reload = false # rubocop:disable Style/OptionalBooleanParameter
73
+ Google::Cloud.env.metadata?
88
74
  end
89
75
 
90
76
  def reset_cache
91
- @on_gce_cache.clear
77
+ Google::Cloud.env.compute_metadata.reset_existence!
78
+ Google::Cloud.env.compute_metadata.cache.expire_all!
92
79
  end
93
80
  alias unmemoize_all reset_cache
94
81
  end
95
82
 
83
+ # Construct a GCECredentials
84
+ def initialize options = {}
85
+ # Override the constructor to remember whether the universe domain was
86
+ # overridden by a constructor argument.
87
+ @universe_domain_overridden = options["universe_domain"] || options[:universe_domain] ? true : false
88
+ super options
89
+ end
90
+
96
91
  # Overrides the super class method to change how access tokens are
97
92
  # fetched.
98
- def fetch_access_token options = {}
99
- c = options[:connection] || Faraday.default_connection
100
- retry_with_error do
101
- uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
102
- query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
103
- query[:scopes] = Array(scope).join "," if scope
104
- resp = c.get uri, query, "Metadata-Flavor" => "Google"
93
+ def fetch_access_token _options = {}
94
+ if token_type == :id_token
95
+ query = { "audience" => target_audience, "format" => "full" }
96
+ entry = "service-accounts/default/identity"
97
+ else
98
+ query = {}
99
+ entry = "service-accounts/default/token"
100
+ end
101
+ query[:scopes] = Array(scope).join "," if scope
102
+ begin
103
+ resp = Google::Cloud.env.lookup_metadata_response "instance", entry, query: query
105
104
  case resp.status
106
105
  when 200
107
- content_type = resp.headers["content-type"]
108
- if ["text/html", "application/text"].include? content_type
109
- { (target_audience ? "id_token" : "access_token") => resp.body }
110
- else
111
- Signet::OAuth2.parse_credentials resp.body, content_type
112
- end
106
+ build_token_hash resp.body, resp.headers["content-type"], resp.retrieval_monotonic_time
113
107
  when 403, 500
114
108
  msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
115
109
  raise Signet::UnexpectedStatusError, msg
@@ -119,7 +113,35 @@ module Google
119
113
  msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
120
114
  raise Signet::AuthorizationError, msg
121
115
  end
116
+ rescue Google::Cloud::Env::MetadataServerNotResponding => e
117
+ raise Signet::AuthorizationError, e.message
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def build_token_hash body, content_type, retrieval_time
124
+ hash =
125
+ if ["text/html", "application/text"].include? content_type
126
+ { token_type.to_s => body }
127
+ else
128
+ Signet::OAuth2.parse_credentials body, content_type
129
+ end
130
+ unless @universe_domain_overridden
131
+ universe_domain = Google::Cloud.env.lookup_metadata "universe", "universe_domain"
132
+ universe_domain = "googleapis.com" if !universe_domain || universe_domain.empty?
133
+ hash["universe_domain"] = universe_domain.strip
134
+ end
135
+ # The response might have been cached, which means expires_in might be
136
+ # stale. Update it based on the time since the data was retrieved.
137
+ # We also ensure expires_in is conservative; subtracting at least 1
138
+ # second to offset any skew from metadata server latency.
139
+ if hash["expires_in"].is_a? Numeric
140
+ offset = 1 + (Process.clock_gettime(Process::CLOCK_MONOTONIC) - retrieval_time).round
141
+ hash["expires_in"] -= offset if offset.positive?
142
+ hash["expires_in"] = 0 if hash["expires_in"].negative?
122
143
  end
144
+ hash
123
145
  end
124
146
  end
125
147
  end
@@ -259,7 +259,7 @@ module Google
259
259
  # @return [Object] The value
260
260
  #
261
261
  def self.lookup_auth_param name, method_name = name
262
- val = instance_variable_get "@#{name}".to_sym
262
+ val = instance_variable_get :"@#{name}"
263
263
  val = yield if val.nil? && block_given?
264
264
  return val unless val.nil?
265
265
  return superclass.send method_name if superclass.respond_to? method_name
@@ -328,9 +328,13 @@ module Google
328
328
  # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
329
329
  # suitable for passing as a closure.
330
330
  #
331
+ # @!attribute [rw] universe_domain
332
+ # @return [String] The universe domain issuing these credentials.
333
+ #
331
334
  def_delegators :@client,
332
335
  :token_credential_uri, :audience,
333
- :scope, :issuer, :signing_key, :updater_proc, :target_audience
336
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience,
337
+ :universe_domain, :universe_domain=
334
338
 
335
339
  ##
336
340
  # Creates a new Credentials instance with the provided auth credentials, and with the default
@@ -352,8 +356,9 @@ module Google
352
356
  #
353
357
  def initialize keyfile, options = {}
354
358
  verify_keyfile_provided! keyfile
355
- @project_id = options["project_id"] || options["project"]
356
- @quota_project_id = options["quota_project_id"]
359
+ options = symbolize_hash_keys options
360
+ @project_id = options[:project_id] || options[:project]
361
+ @quota_project_id = options[:quota_project_id]
357
362
  case keyfile
358
363
  when Google::Auth::BaseClient
359
364
  update_from_signet keyfile
@@ -480,10 +485,11 @@ module Google
480
485
  end
481
486
 
482
487
  # Initializes the Signet client.
483
- def init_client keyfile, connection_options = {}
484
- client_opts = client_options keyfile
485
- Signet::OAuth2::Client.new(client_opts)
486
- .configure_connection(connection_options)
488
+ def init_client hash, options = {}
489
+ options = update_client_options options
490
+ io = StringIO.new JSON.generate hash
491
+ options.merge! json_key_io: io
492
+ Google::Auth::DefaultCredentials.make_creds options
487
493
  end
488
494
 
489
495
  # returns a new Hash with string keys instead of symbol keys.
@@ -491,31 +497,28 @@ module Google
491
497
  hash.to_h.transform_keys(&:to_s)
492
498
  end
493
499
 
494
- # rubocop:disable Metrics/AbcSize
500
+ # returns a new Hash with symbol keys instead of string keys.
501
+ def symbolize_hash_keys hash
502
+ hash.to_h.transform_keys(&:to_sym)
503
+ end
504
+
505
+ def update_client_options options
506
+ options = options.dup
495
507
 
496
- def client_options options
497
- # Keyfile options have higher priority over constructor defaults
498
- options["token_credential_uri"] ||= self.class.token_credential_uri
499
- options["audience"] ||= self.class.audience
500
- options["scope"] ||= self.class.scope
501
- options["target_audience"] ||= self.class.target_audience
508
+ # options have higher priority over constructor defaults
509
+ options[:token_credential_uri] ||= self.class.token_credential_uri
510
+ options[:audience] ||= self.class.audience
511
+ options[:scope] ||= self.class.scope
512
+ options[:target_audience] ||= self.class.target_audience
502
513
 
503
- if !Array(options["scope"]).empty? && options["target_audience"]
514
+ if !Array(options[:scope]).empty? && options[:target_audience]
504
515
  raise ArgumentError, "Cannot specify both scope and target_audience"
505
516
  end
517
+ options.delete :scope unless options[:target_audience].nil?
506
518
 
507
- needs_scope = options["target_audience"].nil?
508
- # client options for initializing signet client
509
- { token_credential_uri: options["token_credential_uri"],
510
- audience: options["audience"],
511
- scope: (needs_scope ? Array(options["scope"]) : nil),
512
- target_audience: options["target_audience"],
513
- issuer: options["client_email"],
514
- signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
519
+ options
515
520
  end
516
521
 
517
- # rubocop:enable Metrics/AbcSize
518
-
519
522
  def update_from_signet client
520
523
  @project_id ||= client.project_id if client.respond_to? :project_id
521
524
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
@@ -526,7 +529,7 @@ module Google
526
529
  hash = stringify_hash_keys hash
527
530
  hash["scope"] ||= options[:scope]
528
531
  hash["target_audience"] ||= options[:target_audience]
529
- @project_id ||= (hash["project_id"] || hash["project"])
532
+ @project_id ||= hash["project_id"] || hash["project"]
530
533
  @quota_project_id ||= hash["quota_project_id"]
531
534
  @client = init_client hash, options
532
535
  end
@@ -536,7 +539,7 @@ module Google
536
539
  json = JSON.parse ::File.read(path)
537
540
  json["scope"] ||= options[:scope]
538
541
  json["target_audience"] ||= options[:target_audience]
539
- @project_id ||= (json["project_id"] || json["project"])
542
+ @project_id ||= json["project_id"] || json["project"]
540
543
  @quota_project_id ||= json["quota_project_id"]
541
544
  @client = init_client json, options
542
545
  end
@@ -42,6 +42,7 @@ module Google
42
42
 
43
43
  attr_reader :expires_at
44
44
  attr_accessor :access_token
45
+ attr_accessor :universe_domain
45
46
 
46
47
  def expires_within? seconds
47
48
  # This method is needed for BaseClient
@@ -85,8 +86,7 @@ module Google
85
86
  # true if the credentials represent a workforce pool.
86
87
  # false if they represent a workload.
87
88
  def is_workforce_pool?
88
- pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
89
- /#{pattern}/.match?(@audience || "")
89
+ %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
90
  end
91
91
 
92
92
  private
@@ -111,6 +111,7 @@ module Google
111
111
  @quota_project_id = options[:quota_project_id]
112
112
  @project_id = nil
113
113
  @workforce_pool_user_project = options[:workforce_pool_user_project]
114
+ @universe_domain = options[:universe_domain] || "googleapis.com"
114
115
 
115
116
  @expires_at = nil
116
117
  @access_token = nil
@@ -73,7 +73,8 @@ module Google
73
73
  subject_token_type: user_creds[:subject_token_type],
74
74
  token_url: user_creds[:token_url],
75
75
  credential_source: user_creds[:credential_source],
76
- service_account_impersonation_url: user_creds[:service_account_impersonation_url]
76
+ service_account_impersonation_url: user_creds[:service_account_impersonation_url],
77
+ universe_domain: user_creds[:universe_domain]
77
78
  )
78
79
  end
79
80
 
@@ -27,7 +27,8 @@ module Google
27
27
  json_key["private_key"],
28
28
  json_key["client_email"],
29
29
  json_key["project_id"],
30
- json_key["quota_project_id"]
30
+ json_key["quota_project_id"],
31
+ json_key["universe_domain"]
31
32
  ]
32
33
  end
33
34
  end
@@ -39,7 +39,11 @@ module Google
39
39
  attr_reader :quota_project_id
40
40
 
41
41
  def enable_self_signed_jwt?
42
- @enable_self_signed_jwt
42
+ # Use a self-singed JWT if there's no information that can be used to
43
+ # obtain an OAuth token, OR if there are scopes but also an assertion
44
+ # that they are default scopes that shouldn't be used to fetch a token,
45
+ # OR we are not in the default universe and thus OAuth isn't supported.
46
+ target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com")
43
47
  end
44
48
 
45
49
  # Creates a ServiceAccountCredentials.
@@ -53,12 +57,13 @@ module Google
53
57
  raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
54
58
 
55
59
  if json_key_io
56
- private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
60
+ private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
57
61
  else
58
62
  private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
59
63
  client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
60
64
  project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
61
65
  quota_project_id = nil
66
+ universe_domain = nil
62
67
  end
63
68
  project_id ||= CredentialsLoader.load_gcloud_project_id
64
69
 
@@ -70,7 +75,8 @@ module Google
70
75
  issuer: client_email,
71
76
  signing_key: OpenSSL::PKey::RSA.new(private_key),
72
77
  project_id: project_id,
73
- quota_project_id: quota_project_id)
78
+ quota_project_id: quota_project_id,
79
+ universe_domain: universe_domain || "googleapis.com")
74
80
  .configure_connection(options)
75
81
  end
76
82
 
@@ -93,16 +99,18 @@ module Google
93
99
  # Extends the base class to use a transient
94
100
  # ServiceAccountJwtHeaderCredentials for certain cases.
95
101
  def apply! a_hash, opts = {}
96
- # Use a self-singed JWT if there's no information that can be used to
97
- # obtain an OAuth token, OR if there are scopes but also an assertion
98
- # that they are default scopes that shouldn't be used to fetch a token.
99
- if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
102
+ if enable_self_signed_jwt?
100
103
  apply_self_signed_jwt! a_hash
101
104
  else
102
105
  super
103
106
  end
104
107
  end
105
108
 
109
+ # Modifies this logic so it also requires self-signed-jwt to be disabled
110
+ def needs_access_token?
111
+ super && !enable_self_signed_jwt?
112
+ end
113
+
106
114
  private
107
115
 
108
116
  def apply_self_signed_jwt! a_hash
@@ -138,6 +146,7 @@ module Google
138
146
  extend JsonKeyReader
139
147
  attr_reader :project_id
140
148
  attr_reader :quota_project_id
149
+ attr_accessor :universe_domain
141
150
 
142
151
  # Create a ServiceAccountJwtHeaderCredentials.
143
152
  #
@@ -154,14 +163,16 @@ module Google
154
163
  def initialize options = {}
155
164
  json_key_io = options[:json_key_io]
156
165
  if json_key_io
157
- @private_key, @issuer, @project_id, @quota_project_id =
166
+ @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
158
167
  self.class.read_json_key json_key_io
159
168
  else
160
169
  @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
161
170
  @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
162
171
  @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
163
172
  @quota_project_id = nil
173
+ @universe_domain = nil
164
174
  end
175
+ @universe_domain ||= "googleapis.com"
165
176
  @project_id ||= CredentialsLoader.load_gcloud_project_id
166
177
  @signing_key = OpenSSL::PKey::RSA.new @private_key
167
178
  @scope = options[:scope]
@@ -210,6 +221,11 @@ module Google
210
221
 
211
222
  JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
212
223
  end
224
+
225
+ # Duck-types the corresponding method from BaseClient
226
+ def needs_access_token?
227
+ false
228
+ end
213
229
  end
214
230
  end
215
231
  end
@@ -25,6 +25,15 @@ module Signet
25
25
  class Client
26
26
  include Google::Auth::BaseClient
27
27
 
28
+ alias update_token_signet_base update_token!
29
+
30
+ def update_token! options = {}
31
+ options = deep_hash_normalize options
32
+ update_token_signet_base options
33
+ self.universe_domain = options[:universe_domain] if options.key? :universe_domain
34
+ self
35
+ end
36
+
28
37
  def configure_connection options
29
38
  @connection_info =
30
39
  options[:connection_builder] || options[:default_connection]
@@ -36,6 +45,9 @@ module Signet
36
45
  target_audience ? :id_token : :access_token
37
46
  end
38
47
 
48
+ # Set the universe domain
49
+ attr_accessor :universe_domain
50
+
39
51
  alias orig_fetch_access_token! fetch_access_token!
40
52
  def fetch_access_token! options = {}
41
53
  unless options[:connection]
@@ -16,6 +16,7 @@ require "uri"
16
16
  require "multi_json"
17
17
  require "googleauth/signet"
18
18
  require "googleauth/user_refresh"
19
+ require "securerandom"
19
20
 
20
21
  module Google
21
22
  module Auth
@@ -54,17 +55,26 @@ module Google
54
55
  # Authorization scope to request
55
56
  # @param [Google::Auth::Stores::TokenStore] token_store
56
57
  # Backing storage for persisting user credentials
57
- # @param [String] callback_uri
58
+ # @param [String] legacy_callback_uri
58
59
  # URL (either absolute or relative) of the auth callback.
59
- # Defaults to '/oauth2callback'
60
- def initialize client_id, scope, token_store, callback_uri = nil
60
+ # Defaults to '/oauth2callback'.
61
+ # @deprecated This field is deprecated. Instead, use the keyword
62
+ # argument callback_uri.
63
+ # @param [String] code_verifier
64
+ # Random string of 43-128 chars used to verify the key exchange using
65
+ # PKCE.
66
+ def initialize client_id, scope, token_store,
67
+ legacy_callback_uri = nil,
68
+ callback_uri: nil,
69
+ code_verifier: nil
61
70
  raise NIL_CLIENT_ID_ERROR if client_id.nil?
62
71
  raise NIL_SCOPE_ERROR if scope.nil?
63
72
 
64
73
  @client_id = client_id
65
74
  @scope = Array(scope)
66
75
  @token_store = token_store
67
- @callback_uri = callback_uri || "/oauth2callback"
76
+ @callback_uri = legacy_callback_uri || callback_uri || "/oauth2callback"
77
+ @code_verifier = code_verifier
68
78
  end
69
79
 
70
80
  # Build the URL for requesting authorization.
@@ -86,6 +96,18 @@ module Google
86
96
  # Authorization url
87
97
  def get_authorization_url options = {}
88
98
  scope = options[:scope] || @scope
99
+
100
+ options[:additional_parameters] ||= {}
101
+
102
+ if @code_verifier
103
+ options[:additional_parameters].merge!(
104
+ {
105
+ code_challenge: generate_code_challenge(@code_verifier),
106
+ code_challenge_method: code_challenge_method
107
+ }
108
+ )
109
+ end
110
+
89
111
  credentials = UserRefreshCredentials.new(
90
112
  client_id: @client_id.id,
91
113
  client_secret: @client_id.secret,
@@ -157,6 +179,8 @@ module Google
157
179
  code = options[:code]
158
180
  scope = options[:scope] || @scope
159
181
  base_url = options[:base_url]
182
+ options[:additional_parameters] ||= {}
183
+ options[:additional_parameters].merge!({ code_verifier: @code_verifier })
160
184
  credentials = UserRefreshCredentials.new(
161
185
  client_id: @client_id.id,
162
186
  client_secret: @client_id.secret,
@@ -228,6 +252,23 @@ module Google
228
252
  credentials
229
253
  end
230
254
 
255
+ # The code verifier for PKCE for OAuth 2.0. When set, the
256
+ # authorization URI will contain the Code Challenge and Code
257
+ # Challenge Method querystring parameters, and the token URI will
258
+ # contain the Code Verifier parameter.
259
+ #
260
+ # @param [String|nil] new_code_erifier
261
+ def code_verifier= new_code_verifier
262
+ @code_verifier = new_code_verifier
263
+ end
264
+
265
+ # Generate the code verifier needed to be sent while fetching
266
+ # authorization URL.
267
+ def self.generate_code_verifier
268
+ random_number = rand 32..96
269
+ SecureRandom.alphanumeric random_number
270
+ end
271
+
231
272
  private
232
273
 
233
274
  # @private Fetch stored token with given user_id
@@ -272,6 +313,15 @@ module Google
272
313
  def uri_is_postmessage? uri
273
314
  uri.to_s.casecmp("postmessage").zero?
274
315
  end
316
+
317
+ def generate_code_challenge code_verifier
318
+ digest = Digest::SHA256.digest code_verifier
319
+ Base64.urlsafe_encode64 digest, padding: false
320
+ end
321
+
322
+ def code_challenge_method
323
+ "S256"
324
+ end
275
325
  end
276
326
  end
277
327
  end
@@ -50,7 +50,8 @@ module Google
50
50
  "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
51
51
  "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
52
52
  "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR],
53
- "quota_project_id" => nil
53
+ "quota_project_id" => nil,
54
+ "universe_domain" => nil
54
55
  }
55
56
  new(token_credential_uri: TOKEN_CRED_URI,
56
57
  client_id: user_creds["client_id"],
@@ -58,7 +59,8 @@ module Google
58
59
  refresh_token: user_creds["refresh_token"],
59
60
  project_id: user_creds["project_id"],
60
61
  quota_project_id: user_creds["quota_project_id"],
61
- scope: scope)
62
+ scope: scope,
63
+ universe_domain: user_creds["universe_domain"] || "googleapis.com")
62
64
  .configure_connection(options)
63
65
  end
64
66
 
@@ -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.8.1".freeze
19
+ VERSION = "1.11.0".freeze
20
20
  end
21
21
  end
@@ -93,11 +93,22 @@ module Google
93
93
  # Authorization scope to request
94
94
  # @param [Google::Auth::Stores::TokenStore] token_store
95
95
  # Backing storage for persisting user credentials
96
- # @param [String] callback_uri
96
+ # @param [String] legacy_callback_uri
97
97
  # URL (either absolute or relative) of the auth callback. Defaults
98
- # to '/oauth2callback'
99
- def initialize client_id, scope, token_store, callback_uri = nil
100
- super client_id, scope, token_store, callback_uri
98
+ # to '/oauth2callback'.
99
+ # @deprecated This field is deprecated. Instead, use the keyword
100
+ # argument callback_uri.
101
+ # @param [String] code_verifier
102
+ # Random string of 43-128 chars used to verify the key exchange using
103
+ # PKCE.
104
+ def initialize client_id, scope, token_store,
105
+ legacy_callback_uri = nil,
106
+ callback_uri: nil,
107
+ code_verifier: nil
108
+ super client_id, scope, token_store,
109
+ legacy_callback_uri,
110
+ code_verifier: code_verifier,
111
+ callback_uri: callback_uri
101
112
  end
102
113
 
103
114
  # Handle the result of the oauth callback. Exchanges the authorization
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googleauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Emiola
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-20 00:00:00.000000000 Z
11
+ date: 2024-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.17.3
19
+ version: '1.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: 3.a
@@ -26,10 +26,24 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.17.3
29
+ version: '1.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 3.a
33
+ - !ruby/object:Gem::Dependency
34
+ name: google-cloud-env
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.1'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: jwt
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -165,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
179
  requirements:
166
180
  - - ">="
167
181
  - !ruby/object:Gem::Version
168
- version: '2.6'
182
+ version: '2.7'
169
183
  required_rubygems_version: !ruby/object:Gem::Requirement
170
184
  requirements:
171
185
  - - ">="
172
186
  - !ruby/object:Gem::Version
173
187
  version: '0'
174
188
  requirements: []
175
- rubygems_version: 3.4.19
189
+ rubygems_version: 3.5.3
176
190
  signing_key:
177
191
  specification_version: 4
178
192
  summary: Google Auth Library for Ruby