googleauth 1.9.1 → 1.10.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: 5950bbb2b0bb696d85ded5cfd31bc2a44192b9a5e42c774260300e467a821316
4
- data.tar.gz: 937e9ef7634d2667a6e552276e0dc7716511a3597b33de2c20174a865bcb3598
3
+ metadata.gz: 3298a8e70480c26df728476ce2bc2723b28f571e2ffd56b5915d6df70f4d2beb
4
+ data.tar.gz: 8121dbd23bcc3b7458f9eab0d83210321109b7006bac1f53ce94801e544f3ee9
5
5
  SHA512:
6
- metadata.gz: 94c41b28ce5fdd2ed5437cf30aeb67d53f04b5863711fd74b1403612cd0747f9e2856fda14678927ef1c026af0f45e6c15a2eef68415a77585b752845f7852a6
7
- data.tar.gz: 46d4ace82973f0df463733bad339ea00786baa90e1adbb665bdadf3d71999dfeb67177f98b4b26b13464307d02a5ef7b649793bb56dc7619b7f9a076118b3942
6
+ metadata.gz: 779e31aafd092dc978ba405171502011d8b4828f614c8023e6fd7d8296fd6ec805650187cabade3b8b4528c5020ff1455f38b1db8d3b4da82c6a76a0a400f3fc
7
+ data.tar.gz: 51ea4d44575fc2f778d60c702c370d53c2f36b42f67c046c212fbe7a97ac9dc4da54f08a8d7fffd144a8b60e2db465233f22d3a2f318ea0814aaf42f05989aa8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Release History
2
2
 
3
+ ### 1.10.0 (2024-02-08)
4
+
5
+ #### Features
6
+
7
+ * add PKCE to 3 Legged OAuth exchange ([#471](https://github.com/googleapis/google-auth-library-ruby/issues/471))
8
+ #### Bug Fixes
9
+
10
+ * 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))
11
+ * Prioritize universe domain specified in GCECredentials arguments over metadata-fetched value ([#472](https://github.com/googleapis/google-auth-library-ruby/issues/472))
12
+
13
+ ### 1.9.2 (2024-01-25)
14
+
15
+ #### Bug Fixes
16
+
17
+ * 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))
18
+
3
19
  ### 1.9.1 (2023-12-12)
4
20
 
5
21
  #### 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
@@ -57,7 +58,11 @@ module Google
57
58
  # @param [String] callback_uri
58
59
  # URL (either absolute or relative) of the auth callback.
59
60
  # Defaults to '/oauth2callback'
60
- def initialize client_id, scope, token_store, callback_uri = nil
61
+ # @param [String] code_verifier
62
+ # Random string of 43-128 chars used to verify the key exchange using
63
+ # PKCE.
64
+ def initialize client_id, scope, token_store,
65
+ callback_uri = nil, code_verifier: nil
61
66
  raise NIL_CLIENT_ID_ERROR if client_id.nil?
62
67
  raise NIL_SCOPE_ERROR if scope.nil?
63
68
 
@@ -65,6 +70,7 @@ module Google
65
70
  @scope = Array(scope)
66
71
  @token_store = token_store
67
72
  @callback_uri = callback_uri || "/oauth2callback"
73
+ @code_verifier = code_verifier
68
74
  end
69
75
 
70
76
  # Build the URL for requesting authorization.
@@ -86,6 +92,18 @@ module Google
86
92
  # Authorization url
87
93
  def get_authorization_url options = {}
88
94
  scope = options[:scope] || @scope
95
+
96
+ options[:additional_parameters] ||= {}
97
+
98
+ if @code_verifier
99
+ options[:additional_parameters].merge!(
100
+ {
101
+ code_challenge: generate_code_challenge(@code_verifier),
102
+ code_challenge_method: code_challenge_method
103
+ }
104
+ )
105
+ end
106
+
89
107
  credentials = UserRefreshCredentials.new(
90
108
  client_id: @client_id.id,
91
109
  client_secret: @client_id.secret,
@@ -157,6 +175,8 @@ module Google
157
175
  code = options[:code]
158
176
  scope = options[:scope] || @scope
159
177
  base_url = options[:base_url]
178
+ options[:additional_parameters] ||= {}
179
+ options[:additional_parameters].merge!({ code_verifier: @code_verifier })
160
180
  credentials = UserRefreshCredentials.new(
161
181
  client_id: @client_id.id,
162
182
  client_secret: @client_id.secret,
@@ -228,6 +248,23 @@ module Google
228
248
  credentials
229
249
  end
230
250
 
251
+ # The code verifier for PKCE for OAuth 2.0. When set, the
252
+ # authorization URI will contain the Code Challenge and Code
253
+ # Challenge Method querystring parameters, and the token URI will
254
+ # contain the Code Verifier parameter.
255
+ #
256
+ # @param [String|nil] new_code_erifier
257
+ def code_verifier= new_code_verifier
258
+ @code_verifier = new_code_verifier
259
+ end
260
+
261
+ # Generate the code verifier needed to be sent while fetching
262
+ # authorization URL.
263
+ def self.generate_code_verifier
264
+ random_number = rand 32..96
265
+ SecureRandom.alphanumeric random_number
266
+ end
267
+
231
268
  private
232
269
 
233
270
  # @private Fetch stored token with given user_id
@@ -272,6 +309,15 @@ module Google
272
309
  def uri_is_postmessage? uri
273
310
  uri.to_s.casecmp("postmessage").zero?
274
311
  end
312
+
313
+ def generate_code_challenge code_verifier
314
+ digest = Digest::SHA256.digest code_verifier
315
+ Base64.urlsafe_encode64 digest, padding: false
316
+ end
317
+
318
+ def code_challenge_method
319
+ "S256"
320
+ end
275
321
  end
276
322
  end
277
323
  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.10.0".freeze
20
20
  end
21
21
  end
@@ -96,8 +96,12 @@ module Google
96
96
  # @param [String] callback_uri
97
97
  # URL (either absolute or relative) of the auth callback. Defaults
98
98
  # to '/oauth2callback'
99
- def initialize client_id, scope, token_store, callback_uri = nil
100
- super client_id, scope, token_store, callback_uri
99
+ # @param [String] code_verifier
100
+ # Random string of 43-128 chars used to verify the key exchange using
101
+ # PKCE.
102
+ def initialize client_id, scope, token_store,
103
+ callback_uri = nil, code_verifier: nil
104
+ super client_id, scope, token_store, callback_uri, code_verifier: code_verifier
101
105
  end
102
106
 
103
107
  # 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.10.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-08 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