googleauth 0.1.0 → 0.16.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.github/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. data/Rakefile +0 -15
@@ -0,0 +1,129 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require "googleauth/signet"
31
+ require "googleauth/credentials_loader"
32
+ require "googleauth/scope_util"
33
+ require "multi_json"
34
+
35
+ module Google
36
+ # Module Auth provides classes that provide Google-specific authorization
37
+ # used to access Google APIs.
38
+ module Auth
39
+ # Authenticates requests using User Refresh credentials.
40
+ #
41
+ # This class allows authorizing requests from user refresh tokens.
42
+ #
43
+ # This the end of the result of a 3LO flow. E.g, the end result of
44
+ # 'gcloud auth login' saves a file with these contents in well known
45
+ # location
46
+ #
47
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
48
+ class UserRefreshCredentials < Signet::OAuth2::Client
49
+ TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze
50
+ AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze
51
+ REVOKE_TOKEN_URI = "https://oauth2.googleapis.com/revoke".freeze
52
+ extend CredentialsLoader
53
+ attr_reader :project_id
54
+
55
+ # Create a UserRefreshCredentials.
56
+ #
57
+ # @param json_key_io [IO] an IO from which the JSON key can be read
58
+ # @param scope [string|array|nil] the scope(s) to access
59
+ def self.make_creds options = {}
60
+ json_key_io, scope = options.values_at :json_key_io, :scope
61
+ user_creds = read_json_key json_key_io if json_key_io
62
+ user_creds ||= {
63
+ "client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR],
64
+ "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
65
+ "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR],
66
+ "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR]
67
+ }
68
+
69
+ new(token_credential_uri: TOKEN_CRED_URI,
70
+ client_id: user_creds["client_id"],
71
+ client_secret: user_creds["client_secret"],
72
+ refresh_token: user_creds["refresh_token"],
73
+ project_id: user_creds["project_id"],
74
+ scope: scope)
75
+ .configure_connection(options)
76
+ end
77
+
78
+ # Reads the client_id, client_secret and refresh_token fields from the
79
+ # JSON key.
80
+ def self.read_json_key json_key_io
81
+ json_key = MultiJson.load json_key_io.read
82
+ wanted = ["client_id", "client_secret", "refresh_token"]
83
+ wanted.each do |key|
84
+ raise "the json is missing the #{key} field" unless json_key.key? key
85
+ end
86
+ json_key
87
+ end
88
+
89
+ def initialize options = {}
90
+ options ||= {}
91
+ options[:token_credential_uri] ||= TOKEN_CRED_URI
92
+ options[:authorization_uri] ||= AUTHORIZATION_URI
93
+ @project_id = options[:project_id]
94
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
95
+ super options
96
+ end
97
+
98
+ # Revokes the credential
99
+ def revoke! options = {}
100
+ c = options[:connection] || Faraday.default_connection
101
+
102
+ retry_with_error do
103
+ resp = c.post(REVOKE_TOKEN_URI, token: refresh_token || access_token)
104
+ case resp.status
105
+ when 200
106
+ self.access_token = nil
107
+ self.refresh_token = nil
108
+ self.expires_at = 0
109
+ else
110
+ raise(Signet::AuthorizationError,
111
+ "Unexpected error code #{resp.status}")
112
+ end
113
+ end
114
+ end
115
+
116
+ # Verifies that a credential grants the requested scope
117
+ #
118
+ # @param [Array<String>, String] required_scope
119
+ # Scope to verify
120
+ # @return [Boolean]
121
+ # True if scope is granted
122
+ def includes_scope? required_scope
123
+ missing_scope = Google::Auth::ScopeUtil.normalize(required_scope) -
124
+ Google::Auth::ScopeUtil.normalize(scope)
125
+ missing_scope.empty?
126
+ end
127
+ end
128
+ end
129
+ end
@@ -31,6 +31,6 @@ module Google
31
31
  # Module Auth provides classes that provide Google-specific authorization
32
32
  # used to access Google APIs.
33
33
  module Auth
34
- VERSION = '0.1.0'
34
+ VERSION = "0.16.2".freeze
35
35
  end
36
36
  end
@@ -0,0 +1,295 @@
1
+ # Copyright 2014, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require "multi_json"
31
+ require "googleauth/signet"
32
+ require "googleauth/user_authorizer"
33
+ require "googleauth/user_refresh"
34
+ require "securerandom"
35
+
36
+ module Google
37
+ module Auth
38
+ # Varation on {Google::Auth::UserAuthorizer} adapted for Rack based
39
+ # web applications.
40
+ #
41
+ # Example usage:
42
+ #
43
+ # get('/') do
44
+ # user_id = request.session['user_email']
45
+ # credentials = authorizer.get_credentials(user_id, request)
46
+ # if credentials.nil?
47
+ # redirect authorizer.get_authorization_url(user_id: user_id,
48
+ # request: request)
49
+ # end
50
+ # # Credentials are valid, can call APIs
51
+ # ...
52
+ # end
53
+ #
54
+ # get('/oauth2callback') do
55
+ # url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(
56
+ # request)
57
+ # redirect url
58
+ # end
59
+ #
60
+ # Instead of implementing the callback directly, applications are
61
+ # encouraged to use {Google::Auth::WebUserAuthorizer::CallbackApp} instead.
62
+ #
63
+ # @see CallbackApp
64
+ # @note Requires sessions are enabled
65
+ class WebUserAuthorizer < Google::Auth::UserAuthorizer
66
+ STATE_PARAM = "state".freeze
67
+ AUTH_CODE_KEY = "code".freeze
68
+ ERROR_CODE_KEY = "error".freeze
69
+ SESSION_ID_KEY = "session_id".freeze
70
+ CALLBACK_STATE_KEY = "g-auth-callback".freeze
71
+ CURRENT_URI_KEY = "current_uri".freeze
72
+ XSRF_KEY = "g-xsrf-token".freeze
73
+ SCOPE_KEY = "scope".freeze
74
+
75
+ NIL_REQUEST_ERROR = "Request is required.".freeze
76
+ NIL_SESSION_ERROR = "Sessions must be enabled".freeze
77
+ MISSING_AUTH_CODE_ERROR = "Missing authorization code in request".freeze
78
+ AUTHORIZATION_ERROR = "Authorization error: %s".freeze
79
+ INVALID_STATE_TOKEN_ERROR =
80
+ "State token does not match expected value".freeze
81
+
82
+ class << self
83
+ attr_accessor :default
84
+ end
85
+
86
+ # Handle the result of the oauth callback. This version defers the
87
+ # exchange of the code by temporarily stashing the results in the user's
88
+ # session. This allows apps to use the generic
89
+ # {Google::Auth::WebUserAuthorizer::CallbackApp} handler for the callback
90
+ # without any additional customization.
91
+ #
92
+ # Apps that wish to handle the callback directly should use
93
+ # {#handle_auth_callback} instead.
94
+ #
95
+ # @param [Rack::Request] request
96
+ # Current request
97
+ def self.handle_auth_callback_deferred request
98
+ callback_state, redirect_uri = extract_callback_state request
99
+ request.session[CALLBACK_STATE_KEY] = MultiJson.dump callback_state
100
+ redirect_uri
101
+ end
102
+
103
+ # Initialize the authorizer
104
+ #
105
+ # @param [Google::Auth::ClientID] client_id
106
+ # Configured ID & secret for this application
107
+ # @param [String, Array<String>] scope
108
+ # Authorization scope to request
109
+ # @param [Google::Auth::Stores::TokenStore] token_store
110
+ # Backing storage for persisting user credentials
111
+ # @param [String] callback_uri
112
+ # URL (either absolute or relative) of the auth callback. Defaults
113
+ # to '/oauth2callback'
114
+ def initialize client_id, scope, token_store, callback_uri = nil
115
+ super client_id, scope, token_store, callback_uri
116
+ end
117
+
118
+ # Handle the result of the oauth callback. Exchanges the authorization
119
+ # code from the request and persists to storage.
120
+ #
121
+ # @param [String] user_id
122
+ # Unique ID of the user for loading/storing credentials.
123
+ # @param [Rack::Request] request
124
+ # Current request
125
+ # @return (Google::Auth::UserRefreshCredentials, String)
126
+ # credentials & next URL to redirect to
127
+ def handle_auth_callback user_id, request
128
+ callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state(
129
+ request
130
+ )
131
+ WebUserAuthorizer.validate_callback_state callback_state, request
132
+ credentials = get_and_store_credentials_from_code(
133
+ user_id: user_id,
134
+ code: callback_state[AUTH_CODE_KEY],
135
+ scope: callback_state[SCOPE_KEY],
136
+ base_url: request.url
137
+ )
138
+ [credentials, redirect_uri]
139
+ end
140
+
141
+ # Build the URL for requesting authorization.
142
+ #
143
+ # @param [String] login_hint
144
+ # Login hint if need to authorize a specific account. Should be a
145
+ # user's email address or unique profile ID.
146
+ # @param [Rack::Request] request
147
+ # Current request
148
+ # @param [String] redirect_to
149
+ # Optional URL to proceed to after authorization complete. Defaults to
150
+ # the current URL.
151
+ # @param [String, Array<String>] scope
152
+ # Authorization scope to request. Overrides the instance scopes if
153
+ # not nil.
154
+ # @param [Hash] state
155
+ # Optional key-values to be returned to the oauth callback.
156
+ # @return [String]
157
+ # Authorization url
158
+ def get_authorization_url options = {}
159
+ options = options.dup
160
+ request = options[:request]
161
+ raise NIL_REQUEST_ERROR if request.nil?
162
+ raise NIL_SESSION_ERROR if request.session.nil?
163
+
164
+ state = options[:state] || {}
165
+
166
+ redirect_to = options[:redirect_to] || request.url
167
+ request.session[XSRF_KEY] = SecureRandom.base64
168
+ options[:state] = MultiJson.dump(state.merge(
169
+ SESSION_ID_KEY => request.session[XSRF_KEY],
170
+ CURRENT_URI_KEY => redirect_to
171
+ ))
172
+ options[:base_url] = request.url
173
+ super options
174
+ end
175
+
176
+ # Fetch stored credentials for the user from the given request session.
177
+ #
178
+ # @param [String] user_id
179
+ # Unique ID of the user for loading/storing credentials.
180
+ # @param [Rack::Request] request
181
+ # Current request. Optional. If omitted, this will attempt to fall back
182
+ # on the base class behavior of reading from the token store.
183
+ # @param [Array<String>, String] scope
184
+ # If specified, only returns credentials that have all the \
185
+ # requested scopes
186
+ # @return [Google::Auth::UserRefreshCredentials]
187
+ # Stored credentials, nil if none present
188
+ # @raise [Signet::AuthorizationError]
189
+ # May raise an error if an authorization code is present in the session
190
+ # and exchange of the code fails
191
+ def get_credentials user_id, request = nil, scope = nil
192
+ if request&.session&.key? CALLBACK_STATE_KEY
193
+ # Note - in theory, no need to check required scope as this is
194
+ # expected to be called immediately after a return from authorization
195
+ state_json = request.session.delete CALLBACK_STATE_KEY
196
+ callback_state = MultiJson.load state_json
197
+ WebUserAuthorizer.validate_callback_state callback_state, request
198
+ get_and_store_credentials_from_code(
199
+ user_id: user_id,
200
+ code: callback_state[AUTH_CODE_KEY],
201
+ scope: callback_state[SCOPE_KEY],
202
+ base_url: request.url
203
+ )
204
+ else
205
+ super user_id, scope
206
+ end
207
+ end
208
+
209
+ def self.extract_callback_state request
210
+ state = MultiJson.load(request[STATE_PARAM] || "{}")
211
+ redirect_uri = state[CURRENT_URI_KEY]
212
+ callback_state = {
213
+ AUTH_CODE_KEY => request[AUTH_CODE_KEY],
214
+ ERROR_CODE_KEY => request[ERROR_CODE_KEY],
215
+ SESSION_ID_KEY => state[SESSION_ID_KEY],
216
+ SCOPE_KEY => request[SCOPE_KEY]
217
+ }
218
+ [callback_state, redirect_uri]
219
+ end
220
+
221
+ # Verifies the results of an authorization callback
222
+ #
223
+ # @param [Hash] state
224
+ # Callback state
225
+ # @option state [String] AUTH_CODE_KEY
226
+ # The authorization code
227
+ # @option state [String] ERROR_CODE_KEY
228
+ # Error message if failed
229
+ # @param [Rack::Request] request
230
+ # Current request
231
+ def self.validate_callback_state state, request
232
+ raise Signet::AuthorizationError, MISSING_AUTH_CODE_ERROR if state[AUTH_CODE_KEY].nil?
233
+ if state[ERROR_CODE_KEY]
234
+ raise Signet::AuthorizationError,
235
+ format(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY])
236
+ elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY]
237
+ raise Signet::AuthorizationError, INVALID_STATE_TOKEN_ERROR
238
+ end
239
+ end
240
+
241
+ # Small Rack app which acts as the default callback handler for the app.
242
+ #
243
+ # To configure in Rails, add to routes.rb:
244
+ #
245
+ # match '/oauth2callback',
246
+ # to: Google::Auth::WebUserAuthorizer::CallbackApp,
247
+ # via: :all
248
+ #
249
+ # With Rackup, add to config.ru:
250
+ #
251
+ # map '/oauth2callback' do
252
+ # run Google::Auth::WebUserAuthorizer::CallbackApp
253
+ # end
254
+ #
255
+ # Or in a classic Sinatra app:
256
+ #
257
+ # get('/oauth2callback') do
258
+ # Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
259
+ # end
260
+ #
261
+ # @see Google::Auth::WebUserAuthorizer
262
+ class CallbackApp
263
+ LOCATION_HEADER = "Location".freeze
264
+ REDIR_STATUS = 302
265
+ ERROR_STATUS = 500
266
+
267
+ # Handle a rack request. Simply stores the results the authorization
268
+ # in the session temporarily and redirects back to to the previously
269
+ # saved redirect URL. Credentials can be later retrieved by calling.
270
+ # {Google::Auth::Web::WebUserAuthorizer#get_credentials}
271
+ #
272
+ # See {Google::Auth::Web::WebUserAuthorizer#get_authorization_uri}
273
+ # for how to initiate authorization requests.
274
+ #
275
+ # @param [Hash] env
276
+ # Rack environment
277
+ # @return [Array]
278
+ # HTTP response
279
+ def self.call env
280
+ request = Rack::Request.new env
281
+ return_url = WebUserAuthorizer.handle_auth_callback_deferred request
282
+ if return_url
283
+ [REDIR_STATUS, { LOCATION_HEADER => return_url }, []]
284
+ else
285
+ [ERROR_STATUS, {}, ["No return URL is present in the request."]]
286
+ end
287
+ end
288
+
289
+ def call env
290
+ self.class.call env
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
@@ -27,141 +27,143 @@
27
27
  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
28
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
29
 
30
- spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
- $LOAD_PATH.unshift(spec_dir)
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
32
  $LOAD_PATH.uniq!
33
33
 
34
- require 'faraday'
35
- require 'spec_helper'
34
+ require "faraday"
35
+ require "spec_helper"
36
36
 
37
- def build_json_response(payload)
38
- [200,
39
- { 'Content-Type' => 'application/json; charset=utf-8' },
40
- MultiJson.dump(payload)]
41
- end
42
-
43
- def build_access_token_json(token)
44
- build_json_response('access_token' => token,
45
- 'token_type' => 'Bearer',
46
- 'expires_in' => 3600)
47
- end
37
+ shared_examples "apply/apply! are OK" do
38
+ let(:auth_key) { :authorization }
48
39
 
49
- WANTED_AUTH_KEY = :Authorization
50
-
51
- shared_examples 'apply/apply! are OK' do
52
40
  # tests that use these examples need to define
53
41
  #
54
42
  # @client which should be an auth client
55
43
  #
56
44
  # @make_auth_stubs, which should stub out the expected http behaviour of the
57
45
  # auth client
58
- describe '#fetch_access_token' do
59
- it 'should set access_token to the fetched value' do
60
- token = '1/abcdef1234567890'
61
- stubs = make_auth_stubs access_token: token
62
- c = Faraday.new do |b|
63
- b.adapter(:test, stubs)
64
- end
46
+ describe "#fetch_access_token" do
47
+ let(:token) { "1/abcdef1234567890" }
48
+ let :access_stub do
49
+ make_auth_stubs access_token: token
50
+ end
51
+ let :id_stub do
52
+ make_auth_stubs id_token: token
53
+ end
65
54
 
66
- @client.fetch_access_token!(connection: c)
55
+ it "should set access_token to the fetched value" do
56
+ access_stub
57
+ @client.fetch_access_token!
67
58
  expect(@client.access_token).to eq(token)
68
- stubs.verify_stubbed_calls
59
+ expect(access_stub).to have_been_requested
60
+ end
61
+
62
+ it "should set id_token to the fetched value" do
63
+ skip unless @id_client
64
+ id_stub
65
+ @id_client.fetch_access_token!
66
+ expect(@id_client.id_token).to eq(token)
67
+ expect(id_stub).to have_been_requested
68
+ end
69
+
70
+ it "should notify refresh listeners after updating" do
71
+ access_stub
72
+ expect do |b|
73
+ @client.on_refresh(&b)
74
+ @client.fetch_access_token!
75
+ end.to yield_with_args(have_attributes(
76
+ access_token: "1/abcdef1234567890"
77
+ ))
78
+ expect(access_stub).to have_been_requested
69
79
  end
70
80
  end
71
81
 
72
- describe '#apply!' do
73
- it 'should update the target hash with fetched access token' do
74
- token = '1/abcdef1234567890'
75
- stubs = make_auth_stubs access_token: token
76
- c = Faraday.new do |b|
77
- b.adapter(:test, stubs)
78
- end
82
+ describe "#apply!" do
83
+ it "should update the target hash with fetched access token" do
84
+ token = "1/abcdef1234567890"
85
+ stub = make_auth_stubs access_token: token
79
86
 
80
- md = { foo: 'bar' }
81
- @client.apply!(md, connection: c)
82
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
87
+ md = { foo: "bar" }
88
+ @client.apply! md
89
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
83
90
  expect(md).to eq(want)
84
- stubs.verify_stubbed_calls
91
+ expect(stub).to have_been_requested
85
92
  end
86
- end
87
93
 
88
- describe 'updater_proc' do
89
- it 'should provide a proc that updates a hash with the access token' do
90
- token = '1/abcdef1234567890'
91
- stubs = make_auth_stubs access_token: token
92
- c = Faraday.new do |b|
93
- b.adapter(:test, stubs)
94
- end
94
+ it "should update the target hash with fetched ID token" do
95
+ skip unless @id_client
96
+ token = "1/abcdef1234567890"
97
+ stub = make_auth_stubs id_token: token
95
98
 
96
- md = { foo: 'bar' }
99
+ md = { foo: "bar" }
100
+ @id_client.apply! md
101
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
102
+ expect(md).to eq(want)
103
+ expect(stub).to have_been_requested
104
+ end
105
+ end
106
+
107
+ describe "updater_proc" do
108
+ it "should provide a proc that updates a hash with the access token" do
109
+ token = "1/abcdef1234567890"
110
+ stub = make_auth_stubs access_token: token
111
+ md = { foo: "bar" }
97
112
  the_proc = @client.updater_proc
98
- got = the_proc.call(md, connection: c)
99
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
113
+ got = the_proc.call md
114
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
100
115
  expect(got).to eq(want)
101
- stubs.verify_stubbed_calls
116
+ expect(stub).to have_been_requested
102
117
  end
103
118
  end
104
119
 
105
- describe '#apply' do
106
- it 'should not update the original hash with the access token' do
107
- token = '1/abcdef1234567890'
108
- stubs = make_auth_stubs access_token: token
109
- c = Faraday.new do |b|
110
- b.adapter(:test, stubs)
111
- end
120
+ describe "#apply" do
121
+ it "should not update the original hash with the access token" do
122
+ token = "1/abcdef1234567890"
123
+ stub = make_auth_stubs access_token: token
112
124
 
113
- md = { foo: 'bar' }
114
- @client.apply(md, connection: c)
115
- want = { foo: 'bar' }
125
+ md = { foo: "bar" }
126
+ @client.apply md
127
+ want = { foo: "bar" }
116
128
  expect(md).to eq(want)
117
- stubs.verify_stubbed_calls
129
+ expect(stub).to have_been_requested
118
130
  end
119
131
 
120
- it 'should add the token to the returned hash' do
121
- token = '1/abcdef1234567890'
122
- stubs = make_auth_stubs access_token: token
123
- c = Faraday.new do |b|
124
- b.adapter(:test, stubs)
125
- end
132
+ it "should add the token to the returned hash" do
133
+ token = "1/abcdef1234567890"
134
+ stub = make_auth_stubs access_token: token
126
135
 
127
- md = { foo: 'bar' }
128
- got = @client.apply(md, connection: c)
129
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
136
+ md = { foo: "bar" }
137
+ got = @client.apply md
138
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
130
139
  expect(got).to eq(want)
131
- stubs.verify_stubbed_calls
140
+ expect(stub).to have_been_requested
132
141
  end
133
142
 
134
- it 'should not fetch a new token if the current is not expired' do
135
- token = '1/abcdef1234567890'
136
- stubs = make_auth_stubs access_token: token
137
- c = Faraday.new do |b|
138
- b.adapter(:test, stubs)
139
- end
143
+ it "should not fetch a new token if the current is not expired" do
144
+ token = "1/abcdef1234567890"
145
+ stub = make_auth_stubs access_token: token
140
146
 
141
147
  n = 5 # arbitrary
142
148
  n.times do |_t|
143
- md = { foo: 'bar' }
144
- got = @client.apply(md, connection: c)
145
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
149
+ md = { foo: "bar" }
150
+ got = @client.apply md
151
+ want = { :foo => "bar", auth_key => "Bearer #{token}" }
146
152
  expect(got).to eq(want)
147
153
  end
148
- stubs.verify_stubbed_calls
154
+ expect(stub).to have_been_requested
149
155
  end
150
156
 
151
- it 'should fetch a new token if the current one is expired' do
152
- token_1 = '1/abcdef1234567890'
153
- token_2 = '2/abcdef1234567890'
154
-
155
- [token_1, token_2].each do |t|
156
- stubs = make_auth_stubs access_token: t
157
- c = Faraday.new do |b|
158
- b.adapter(:test, stubs)
159
- end
160
- md = { foo: 'bar' }
161
- got = @client.apply(md, connection: c)
162
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
157
+ it "should fetch a new token if the current one is expired" do
158
+ token1 = "1/abcdef1234567890"
159
+ token2 = "2/abcdef1234567891"
160
+
161
+ [token1, token2].each do |t|
162
+ make_auth_stubs access_token: t
163
+ md = { foo: "bar" }
164
+ got = @client.apply md
165
+ want = { :foo => "bar", auth_key => "Bearer #{t}" }
163
166
  expect(got).to eq(want)
164
- stubs.verify_stubbed_calls
165
167
  @client.expires_at -= 3601 # default is to expire in 1hr
166
168
  end
167
169
  end