googleauth 1.3.0 → 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.
@@ -18,27 +18,60 @@ require "multi_json"
18
18
 
19
19
  module Google
20
20
  module Auth
21
- # Small utility for normalizing scopes into canonical form
21
+ ##
22
+ # Small utility for normalizing scopes into canonical form.
23
+ #
24
+ # The canonical form of scopes is as an array of strings, each in the form
25
+ # of a full URL. This utility converts space-delimited scope strings into
26
+ # this form, and handles a small number of common aliases.
27
+ #
28
+ # This is used by UserRefreshCredentials to verify that a credential grants
29
+ # a requested scope.
30
+ #
22
31
  module ScopeUtil
32
+ ##
33
+ # Aliases understood by this utility
34
+ #
23
35
  ALIASES = {
24
36
  "email" => "https://www.googleapis.com/auth/userinfo.email",
25
37
  "profile" => "https://www.googleapis.com/auth/userinfo.profile",
26
38
  "openid" => "https://www.googleapis.com/auth/plus.me"
27
39
  }.freeze
28
40
 
41
+ ##
42
+ # Normalize the input, which may be an array of scopes or a whitespace-
43
+ # delimited scope string. The output is always an array, even if a single
44
+ # scope is input.
45
+ #
46
+ # @param scope [String,Array<String>] Input scope(s)
47
+ # @return [Array<String>] An array of scopes in canonical form.
48
+ #
29
49
  def self.normalize scope
30
50
  list = as_array scope
31
51
  list.map { |item| ALIASES[item] || item }
32
52
  end
33
53
 
54
+ ##
55
+ # Ensure the input is an array. If a single string is passed in, splits
56
+ # it via whitespace. Does not interpret aliases.
57
+ #
58
+ # @param scope [String,Array<String>] Input scope(s)
59
+ # @return [Array<String>] Always an array of strings
60
+ # @raise ArgumentError If the input is not a string or array of strings
61
+ #
34
62
  def self.as_array scope
35
63
  case scope
36
64
  when Array
65
+ scope.each do |item|
66
+ unless item.is_a? String
67
+ raise ArgumentError, "Invalid scope value: #{item.inspect}. Must be string or array"
68
+ end
69
+ end
37
70
  scope
38
71
  when String
39
72
  scope.split
40
73
  else
41
- raise "Invalid scope value. Must be string or array"
74
+ raise ArgumentError, "Invalid scope value: #{scope.inspect}. Must be string or array"
42
75
  end
43
76
  end
44
77
  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
@@ -130,7 +138,7 @@ module Google
130
138
  # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
131
139
  class ServiceAccountJwtHeaderCredentials
132
140
  JWT_AUD_URI_KEY = :jwt_aud_uri
133
- AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
141
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
134
142
  TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
135
143
  SIGNING_ALGORITHM = "RS256".freeze
136
144
  EXPIRY = 60
@@ -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]
@@ -192,8 +203,6 @@ module Google
192
203
  proc { |a_hash, opts = {}| apply a_hash, opts }
193
204
  end
194
205
 
195
- protected
196
-
197
206
  # Creates a jwt uri token.
198
207
  def new_jwt_token jwt_aud_uri = nil, options = {}
199
208
  now = Time.new
@@ -212,6 +221,11 @@ module Google
212
221
 
213
222
  JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
214
223
  end
224
+
225
+ # Duck-types the corresponding method from BaseClient
226
+ def needs_access_token?
227
+ false
228
+ end
215
229
  end
216
230
  end
217
231
  end
@@ -13,16 +13,27 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "signet/oauth_2/client"
16
+ require "googleauth/base_client"
16
17
 
17
18
  module Signet
18
19
  # OAuth2 supports OAuth2 authentication.
19
20
  module OAuth2
20
- AUTH_METADATA_KEY = :authorization
21
21
  # Signet::OAuth2::Client creates an OAuth2 client
22
22
  #
23
23
  # This reopens Client to add #apply and #apply! methods which update a
24
24
  # hash with the fetched authentication token.
25
25
  class Client
26
+ include Google::Auth::BaseClient
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
+
26
37
  def configure_connection options
27
38
  @connection_info =
28
39
  options[:connection_builder] || options[:default_connection]
@@ -34,36 +45,8 @@ module Signet
34
45
  target_audience ? :id_token : :access_token
35
46
  end
36
47
 
37
- # Whether the id_token or access_token is missing or about to expire.
38
- def needs_access_token?
39
- send(token_type).nil? || expires_within?(60)
40
- end
41
-
42
- # Updates a_hash updated with the authentication token
43
- def apply! a_hash, opts = {}
44
- # fetch the access token there is currently not one, or if the client
45
- # has expired
46
- fetch_access_token! opts if needs_access_token?
47
- a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
48
- end
49
-
50
- # Returns a clone of a_hash updated with the authentication token
51
- def apply a_hash, opts = {}
52
- a_copy = a_hash.clone
53
- apply! a_copy, opts
54
- a_copy
55
- end
56
-
57
- # Returns a reference to the #apply method, suitable for passing as
58
- # a closure
59
- def updater_proc
60
- proc { |a_hash, opts = {}| apply a_hash, opts }
61
- end
62
-
63
- def on_refresh &block
64
- @refresh_listeners = [] unless defined? @refresh_listeners
65
- @refresh_listeners << block
66
- end
48
+ # Set the universe domain
49
+ attr_accessor :universe_domain
67
50
 
68
51
  alias orig_fetch_access_token! fetch_access_token!
69
52
  def fetch_access_token! options = {}
@@ -78,13 +61,6 @@ module Signet
78
61
  info
79
62
  end
80
63
 
81
- def notify_refresh_listeners
82
- listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
83
- listeners.each do |block|
84
- block.call self
85
- end
86
- end
87
-
88
64
  def build_default_connection
89
65
  if !defined?(@connection_info)
90
66
  nil
@@ -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.
@@ -80,14 +90,29 @@ module Google
80
90
  # @param [String, Array<String>] scope
81
91
  # Authorization scope to request. Overrides the instance scopes if not
82
92
  # nil.
93
+ # @param [Hash] additional_parameters
94
+ # Additional query parameters to be added to the authorization URL.
83
95
  # @return [String]
84
96
  # Authorization url
85
97
  def get_authorization_url options = {}
86
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
+
87
111
  credentials = UserRefreshCredentials.new(
88
112
  client_id: @client_id.id,
89
113
  client_secret: @client_id.secret,
90
- scope: scope
114
+ scope: scope,
115
+ additional_parameters: options[:additional_parameters]
91
116
  )
92
117
  redirect_uri = redirect_uri_for options[:base_url]
93
118
  url = credentials.authorization_uri(access_type: "offline",
@@ -144,6 +169,9 @@ module Google
144
169
  # Absolute URL to resolve the configured callback uri against.
145
170
  # Required if the configured
146
171
  # callback uri is a relative.
172
+ # @param [Hash] additional_parameters
173
+ # Additional parameters to be added to the post body of token
174
+ # endpoint request.
147
175
  # @return [Google::Auth::UserRefreshCredentials]
148
176
  # Credentials if exchange is successful
149
177
  def get_credentials_from_code options = {}
@@ -151,11 +179,14 @@ module Google
151
179
  code = options[:code]
152
180
  scope = options[:scope] || @scope
153
181
  base_url = options[:base_url]
182
+ options[:additional_parameters] ||= {}
183
+ options[:additional_parameters].merge!({ code_verifier: @code_verifier })
154
184
  credentials = UserRefreshCredentials.new(
155
- client_id: @client_id.id,
156
- client_secret: @client_id.secret,
157
- redirect_uri: redirect_uri_for(base_url),
158
- scope: scope
185
+ client_id: @client_id.id,
186
+ client_secret: @client_id.secret,
187
+ redirect_uri: redirect_uri_for(base_url),
188
+ scope: scope,
189
+ additional_parameters: options[:additional_parameters]
159
190
  )
160
191
  credentials.code = code
161
192
  credentials.fetch_access_token!({})
@@ -221,6 +252,23 @@ module Google
221
252
  credentials
222
253
  end
223
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
+
224
272
  private
225
273
 
226
274
  # @private Fetch stored token with given user_id
@@ -265,6 +313,15 @@ module Google
265
313
  def uri_is_postmessage? uri
266
314
  uri.to_s.casecmp("postmessage").zero?
267
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
268
325
  end
269
326
  end
270
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.3.0".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
@@ -192,13 +203,13 @@ module Google
192
203
  end
193
204
 
194
205
  def self.extract_callback_state request
195
- state = MultiJson.load(request[STATE_PARAM] || "{}")
206
+ state = MultiJson.load(request.params[STATE_PARAM] || "{}")
196
207
  redirect_uri = state[CURRENT_URI_KEY]
197
208
  callback_state = {
198
- AUTH_CODE_KEY => request[AUTH_CODE_KEY],
199
- ERROR_CODE_KEY => request[ERROR_CODE_KEY],
209
+ AUTH_CODE_KEY => request.params[AUTH_CODE_KEY],
210
+ ERROR_CODE_KEY => request.params[ERROR_CODE_KEY],
200
211
  SESSION_ID_KEY => state[SESSION_ID_KEY],
201
- SCOPE_KEY => request[SCOPE_KEY]
212
+ SCOPE_KEY => request.params[SCOPE_KEY]
202
213
  }
203
214
  [callback_state, redirect_uri]
204
215
  end
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.3.0
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: 2022-10-18 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
@@ -50,20 +64,6 @@ dependencies:
50
64
  - - "<"
51
65
  - !ruby/object:Gem::Version
52
66
  version: '3.0'
53
- - !ruby/object:Gem::Dependency
54
- name: memoist
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '0.16'
60
- type: :runtime
61
- prerelease: false
62
- version_requirements: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '0.16'
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: multi_json
69
69
  requirement: !ruby/object:Gem::Requirement
@@ -134,17 +134,26 @@ files:
134
134
  - SECURITY.md
135
135
  - lib/googleauth.rb
136
136
  - lib/googleauth/application_default.rb
137
+ - lib/googleauth/base_client.rb
137
138
  - lib/googleauth/client_id.rb
138
139
  - lib/googleauth/compute_engine.rb
139
140
  - lib/googleauth/credentials.rb
140
141
  - lib/googleauth/credentials_loader.rb
141
142
  - lib/googleauth/default_credentials.rb
143
+ - lib/googleauth/external_account.rb
144
+ - lib/googleauth/external_account/aws_credentials.rb
145
+ - lib/googleauth/external_account/base_credentials.rb
146
+ - lib/googleauth/external_account/external_account_utils.rb
147
+ - lib/googleauth/external_account/identity_pool_credentials.rb
148
+ - lib/googleauth/external_account/pluggable_credentials.rb
149
+ - lib/googleauth/helpers/connection.rb
142
150
  - lib/googleauth/iam.rb
143
151
  - lib/googleauth/id_tokens.rb
144
152
  - lib/googleauth/id_tokens/errors.rb
145
153
  - lib/googleauth/id_tokens/key_sources.rb
146
154
  - lib/googleauth/id_tokens/verifier.rb
147
155
  - lib/googleauth/json_key_reader.rb
156
+ - lib/googleauth/oauth2/sts_client.rb
148
157
  - lib/googleauth/scope_util.rb
149
158
  - lib/googleauth/service_account.rb
150
159
  - lib/googleauth/signet.rb
@@ -170,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
170
179
  requirements:
171
180
  - - ">="
172
181
  - !ruby/object:Gem::Version
173
- version: '2.6'
182
+ version: '2.7'
174
183
  required_rubygems_version: !ruby/object:Gem::Requirement
175
184
  requirements:
176
185
  - - ">="
177
186
  - !ruby/object:Gem::Version
178
187
  version: '0'
179
188
  requirements: []
180
- rubygems_version: 3.3.14
189
+ rubygems_version: 3.5.3
181
190
  signing_key:
182
191
  specification_version: 4
183
192
  summary: Google Auth Library for Ruby