googleauth 1.9.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: 5950bbb2b0bb696d85ded5cfd31bc2a44192b9a5e42c774260300e467a821316
4
- data.tar.gz: 937e9ef7634d2667a6e552276e0dc7716511a3597b33de2c20174a865bcb3598
3
+ metadata.gz: 4912a601c0a234fa9faf150d7461cab993f775b715f6ac7d19db017ceae74e6d
4
+ data.tar.gz: e08da21e12d58260944079d068a04099fa0c812eb486e760a3ab12dd002700f4
5
5
  SHA512:
6
- metadata.gz: 94c41b28ce5fdd2ed5437cf30aeb67d53f04b5863711fd74b1403612cd0747f9e2856fda14678927ef1c026af0f45e6c15a2eef68415a77585b752845f7852a6
7
- data.tar.gz: 46d4ace82973f0df463733bad339ea00786baa90e1adbb665bdadf3d71999dfeb67177f98b4b26b13464307d02a5ef7b649793bb56dc7619b7f9a076118b3942
6
+ metadata.gz: b0346fcaf38cb783fd4d22f0734994298d63dfa1a89fd34df4d4f42b87160de410e34c93ab4773c3bbaf03b41160f10e3fd5bc0fa137d1ab6fb5dce15f72ba53
7
+ data.tar.gz: c5ff10a04491e9f56dff9bcea24c67398e67713efc89c2d378075621da837b59c5fdbd36c0b97d5753fd0358befe8ce2ceacc86af1cce0a1c54d609d916903c6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
3
25
  ### 1.9.1 (2023-12-12)
4
26
 
5
27
  #### Bug Fixes
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.
@@ -80,6 +80,14 @@ module Google
80
80
  alias unmemoize_all reset_cache
81
81
  end
82
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
+
83
91
  # Overrides the super class method to change how access tokens are
84
92
  # fetched.
85
93
  def fetch_access_token _options = {}
@@ -119,9 +127,11 @@ module Google
119
127
  else
120
128
  Signet::OAuth2.parse_credentials body, content_type
121
129
  end
122
- universe_domain = Google::Cloud.env.lookup_metadata "universe", "universe_domain"
123
- universe_domain = "googleapis.com" if !universe_domain || universe_domain.empty?
124
- hash["universe_domain"] = universe_domain.strip
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
125
135
  # The response might have been cached, which means expires_in might be
126
136
  # stale. Update it based on the time since the data was retrieved.
127
137
  # We also ensure expires_in is conservative; subtracting at least 1
@@ -356,8 +356,9 @@ module Google
356
356
  #
357
357
  def initialize keyfile, options = {}
358
358
  verify_keyfile_provided! keyfile
359
- @project_id = options["project_id"] || options["project"]
360
- @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]
361
362
  case keyfile
362
363
  when Google::Auth::BaseClient
363
364
  update_from_signet keyfile
@@ -484,10 +485,11 @@ module Google
484
485
  end
485
486
 
486
487
  # Initializes the Signet client.
487
- def init_client keyfile, connection_options = {}
488
- client_opts = client_options keyfile
489
- Signet::OAuth2::Client.new(client_opts)
490
- .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
491
493
  end
492
494
 
493
495
  # returns a new Hash with string keys instead of symbol keys.
@@ -495,34 +497,28 @@ module Google
495
497
  hash.to_h.transform_keys(&:to_s)
496
498
  end
497
499
 
498
- # 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
499
507
 
500
- def client_options options
501
- # Keyfile options have higher priority over constructor defaults
502
- options["token_credential_uri"] ||= self.class.token_credential_uri
503
- options["audience"] ||= self.class.audience
504
- options["scope"] ||= self.class.scope
505
- 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
506
513
 
507
- if !Array(options["scope"]).empty? && options["target_audience"]
514
+ if !Array(options[:scope]).empty? && options[:target_audience]
508
515
  raise ArgumentError, "Cannot specify both scope and target_audience"
509
516
  end
517
+ options.delete :scope unless options[:target_audience].nil?
510
518
 
511
- needs_scope = options["target_audience"].nil?
512
- # client options for initializing signet client
513
- {
514
- token_credential_uri: options["token_credential_uri"],
515
- audience: options["audience"],
516
- scope: (needs_scope ? Array(options["scope"]) : nil),
517
- target_audience: options["target_audience"],
518
- issuer: options["client_email"],
519
- signing_key: OpenSSL::PKey::RSA.new(options["private_key"]),
520
- universe_domain: options["universe_domain"] || "googleapis.com"
521
- }
519
+ options
522
520
  end
523
521
 
524
- # rubocop:enable Metrics/AbcSize
525
-
526
522
  def update_from_signet client
527
523
  @project_id ||= client.project_id if client.respond_to? :project_id
528
524
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
@@ -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.
@@ -95,17 +99,18 @@ module Google
95
99
  # Extends the base class to use a transient
96
100
  # ServiceAccountJwtHeaderCredentials for certain cases.
97
101
  def apply! a_hash, opts = {}
98
- # Use a self-singed JWT if there's no information that can be used to
99
- # obtain an OAuth token, OR if there are scopes but also an assertion
100
- # that they are default scopes that shouldn't be used to fetch a token,
101
- # OR we are not in the default universe and thus OAuth isn't supported.
102
- if target_audience.nil? && (scope.nil? || enable_self_signed_jwt? || universe_domain != "googleapis.com")
102
+ if enable_self_signed_jwt?
103
103
  apply_self_signed_jwt! a_hash
104
104
  else
105
105
  super
106
106
  end
107
107
  end
108
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
+
109
114
  private
110
115
 
111
116
  def apply_self_signed_jwt! a_hash
@@ -216,6 +221,11 @@ module Google
216
221
 
217
222
  JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
218
223
  end
224
+
225
+ # Duck-types the corresponding method from BaseClient
226
+ def needs_access_token?
227
+ false
228
+ end
219
229
  end
220
230
  end
221
231
  end
@@ -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
@@ -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.9.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.9.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-12-12 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
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0'
188
188
  requirements: []
189
- rubygems_version: 3.4.19
189
+ rubygems_version: 3.5.3
190
190
  signing_key:
191
191
  specification_version: 4
192
192
  summary: Google Auth Library for Ruby