googleauth 1.8.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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