googleauth 0.5.1 → 0.5.2
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 +4 -4
- data/.rubocop.yml +26 -1
- data/.travis.yml +3 -1
- data/CHANGELOG.md +13 -6
- data/Gemfile +6 -6
- data/README.md +17 -11
- data/googleauth.gemspec +2 -1
- data/lib/googleauth.rb +6 -6
- data/lib/googleauth/client_id.rb +10 -10
- data/lib/googleauth/compute_engine.rb +18 -14
- data/lib/googleauth/credentials_loader.rb +14 -13
- data/lib/googleauth/iam.rb +4 -4
- data/lib/googleauth/scope_util.rb +2 -2
- data/lib/googleauth/service_account.rb +7 -7
- data/lib/googleauth/signet.rb +22 -1
- data/lib/googleauth/stores/redis_token_store.rb +7 -7
- data/lib/googleauth/token_store.rb +3 -3
- data/lib/googleauth/user_authorizer.rb +36 -24
- data/lib/googleauth/user_refresh.rb +16 -13
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +30 -25
- data/spec/googleauth/apply_auth_examples.rb +5 -4
- data/spec/googleauth/client_id_spec.rb +6 -3
- data/spec/googleauth/compute_engine_spec.rb +19 -5
- data/spec/googleauth/get_application_default_spec.rb +10 -13
- data/spec/googleauth/scope_util_spec.rb +4 -2
- data/spec/googleauth/service_account_spec.rb +7 -4
- data/spec/googleauth/signet_spec.rb +4 -3
- data/spec/googleauth/stores/file_token_store_spec.rb +1 -2
- data/spec/googleauth/user_authorizer_spec.rb +22 -12
- data/spec/googleauth/user_refresh_spec.rb +21 -3
- data/spec/googleauth/web_user_authorizer_spec.rb +15 -8
- metadata +5 -7
- data/.rubocop_todo.yml +0 -32
@@ -46,7 +46,7 @@ module Google
|
|
46
46
|
#
|
47
47
|
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
48
48
|
class ServiceAccountCredentials < Signet::OAuth2::Client
|
49
|
-
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/
|
49
|
+
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v4/token'.freeze
|
50
50
|
extend CredentialsLoader
|
51
51
|
|
52
52
|
# Creates a ServiceAccountCredentials.
|
@@ -73,8 +73,8 @@ module Google
|
|
73
73
|
# JSON key.
|
74
74
|
def self.read_json_key(json_key_io)
|
75
75
|
json_key = MultiJson.load(json_key_io.read)
|
76
|
-
|
77
|
-
|
76
|
+
raise 'missing client_email' unless json_key.key?('client_email')
|
77
|
+
raise 'missing private_key' unless json_key.key?('private_key')
|
78
78
|
[json_key['private_key'], json_key['client_email']]
|
79
79
|
end
|
80
80
|
|
@@ -119,8 +119,8 @@ module Google
|
|
119
119
|
class ServiceAccountJwtHeaderCredentials
|
120
120
|
JWT_AUD_URI_KEY = :jwt_aud_uri
|
121
121
|
AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
|
122
|
-
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/
|
123
|
-
SIGNING_ALGORITHM = 'RS256'
|
122
|
+
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v4/token'.freeze
|
123
|
+
SIGNING_ALGORITHM = 'RS256'.freeze
|
124
124
|
EXPIRY = 60
|
125
125
|
extend CredentialsLoader
|
126
126
|
|
@@ -139,8 +139,8 @@ module Google
|
|
139
139
|
# JSON key.
|
140
140
|
def self.read_json_key(json_key_io)
|
141
141
|
json_key = MultiJson.load(json_key_io.read)
|
142
|
-
|
143
|
-
|
142
|
+
raise 'missing client_email' unless json_key.key?('client_email')
|
143
|
+
raise 'missing private_key' unless json_key.key?('private_key')
|
144
144
|
[json_key['private_key'], json_key['client_email']]
|
145
145
|
end
|
146
146
|
|
data/lib/googleauth/signet.rb
CHANGED
@@ -64,7 +64,7 @@ module Signet
|
|
64
64
|
@refresh_listeners << block
|
65
65
|
end
|
66
66
|
|
67
|
-
|
67
|
+
alias orig_fetch_access_token! fetch_access_token!
|
68
68
|
def fetch_access_token!(options = {})
|
69
69
|
info = orig_fetch_access_token!(options)
|
70
70
|
notify_refresh_listeners
|
@@ -77,6 +77,27 @@ module Signet
|
|
77
77
|
block.call(self)
|
78
78
|
end
|
79
79
|
end
|
80
|
+
|
81
|
+
def retry_with_error(max_retry_count = 5)
|
82
|
+
retry_count = 0
|
83
|
+
|
84
|
+
begin
|
85
|
+
yield
|
86
|
+
rescue => e
|
87
|
+
if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
|
88
|
+
raise e
|
89
|
+
end
|
90
|
+
|
91
|
+
if retry_count < max_retry_count
|
92
|
+
retry_count += 1
|
93
|
+
sleep retry_count * 0.3
|
94
|
+
retry
|
95
|
+
else
|
96
|
+
msg = "Unexpected error: #{e.inspect}"
|
97
|
+
raise(Signet::AuthorizationError, msg)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
80
101
|
end
|
81
102
|
end
|
82
103
|
end
|
@@ -37,7 +37,7 @@ module Google
|
|
37
37
|
# are stored as JSON using the supplied key, prefixed with
|
38
38
|
# `g-user-token:`
|
39
39
|
class RedisTokenStore < Google::Auth::TokenStore
|
40
|
-
DEFAULT_KEY_PREFIX = 'g-user-token:'
|
40
|
+
DEFAULT_KEY_PREFIX = 'g-user-token:'.freeze
|
41
41
|
|
42
42
|
# Create a new store with the supplied redis client.
|
43
43
|
#
|
@@ -51,12 +51,12 @@ module Google
|
|
51
51
|
def initialize(options = {})
|
52
52
|
redis = options.delete(:redis)
|
53
53
|
prefix = options.delete(:prefix)
|
54
|
-
case redis
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
54
|
+
@redis = case redis
|
55
|
+
when Redis
|
56
|
+
redis
|
57
|
+
else
|
58
|
+
Redis.new(options)
|
59
|
+
end
|
60
60
|
@prefix = prefix || DEFAULT_KEY_PREFIX
|
61
61
|
end
|
62
62
|
|
@@ -44,7 +44,7 @@ module Google
|
|
44
44
|
# @return [String]
|
45
45
|
# The loaded token data.
|
46
46
|
def load(_id)
|
47
|
-
|
47
|
+
raise 'Not implemented'
|
48
48
|
end
|
49
49
|
|
50
50
|
# Put the token data into storage for the given ID.
|
@@ -54,7 +54,7 @@ module Google
|
|
54
54
|
# @param [String] token
|
55
55
|
# The token data to store.
|
56
56
|
def store(_id, _token)
|
57
|
-
|
57
|
+
raise 'Not implemented'
|
58
58
|
end
|
59
59
|
|
60
60
|
# Remove the token data from storage for the given ID.
|
@@ -62,7 +62,7 @@ module Google
|
|
62
62
|
# @param [String] id
|
63
63
|
# ID of the token data to delete
|
64
64
|
def delete(_id)
|
65
|
-
|
65
|
+
raise 'Not implemented'
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
@@ -53,13 +53,13 @@ module Google
|
|
53
53
|
# ...
|
54
54
|
class UserAuthorizer
|
55
55
|
MISMATCHED_CLIENT_ID_ERROR =
|
56
|
-
'Token client ID of %s does not match configured client id %s'
|
57
|
-
NIL_CLIENT_ID_ERROR = 'Client id can not be nil.'
|
58
|
-
NIL_SCOPE_ERROR = 'Scope can not be nil.'
|
59
|
-
NIL_USER_ID_ERROR = 'User ID can not be nil.'
|
60
|
-
NIL_TOKEN_STORE_ERROR = 'Can not call method if token store is nil'
|
56
|
+
'Token client ID of %s does not match configured client id %s'.freeze
|
57
|
+
NIL_CLIENT_ID_ERROR = 'Client id can not be nil.'.freeze
|
58
|
+
NIL_SCOPE_ERROR = 'Scope can not be nil.'.freeze
|
59
|
+
NIL_USER_ID_ERROR = 'User ID can not be nil.'.freeze
|
60
|
+
NIL_TOKEN_STORE_ERROR = 'Can not call method if token store is nil'.freeze
|
61
61
|
MISSING_ABSOLUTE_URL_ERROR =
|
62
|
-
'Absolute base url required for relative callback url "%s"'
|
62
|
+
'Absolute base url required for relative callback url "%s"'.freeze
|
63
63
|
|
64
64
|
# Initialize the authorizer
|
65
65
|
#
|
@@ -73,8 +73,8 @@ module Google
|
|
73
73
|
# URL (either absolute or relative) of the auth callback.
|
74
74
|
# Defaults to '/oauth2callback'
|
75
75
|
def initialize(client_id, scope, token_store, callback_uri = nil)
|
76
|
-
|
77
|
-
|
76
|
+
raise NIL_CLIENT_ID_ERROR if client_id.nil?
|
77
|
+
raise NIL_SCOPE_ERROR if scope.nil?
|
78
78
|
|
79
79
|
@client_id = client_id
|
80
80
|
@scope = Array(scope)
|
@@ -102,7 +102,8 @@ module Google
|
|
102
102
|
credentials = UserRefreshCredentials.new(
|
103
103
|
client_id: @client_id.id,
|
104
104
|
client_secret: @client_id.secret,
|
105
|
-
scope: scope
|
105
|
+
scope: scope
|
106
|
+
)
|
106
107
|
redirect_uri = redirect_uri_for(options[:base_url])
|
107
108
|
url = credentials.authorization_uri(access_type: 'offline',
|
108
109
|
redirect_uri: redirect_uri,
|
@@ -123,17 +124,13 @@ module Google
|
|
123
124
|
# @return [Google::Auth::UserRefreshCredentials]
|
124
125
|
# Stored credentials, nil if none present
|
125
126
|
def get_credentials(user_id, scope = nil)
|
126
|
-
|
127
|
-
fail NIL_TOKEN_STORE_ERROR if @token_store.nil?
|
128
|
-
|
129
|
-
scope ||= @scope
|
130
|
-
saved_token = @token_store.load(user_id)
|
127
|
+
saved_token = stored_token(user_id)
|
131
128
|
return nil if saved_token.nil?
|
132
129
|
data = MultiJson.load(saved_token)
|
133
130
|
|
134
131
|
if data.fetch('client_id', @client_id.id) != @client_id.id
|
135
|
-
|
136
|
-
|
132
|
+
raise sprintf(MISMATCHED_CLIENT_ID_ERROR,
|
133
|
+
data['client_id'], @client_id.id)
|
137
134
|
end
|
138
135
|
|
139
136
|
credentials = UserRefreshCredentials.new(
|
@@ -142,10 +139,11 @@ module Google
|
|
142
139
|
scope: data['scope'] || @scope,
|
143
140
|
access_token: data['access_token'],
|
144
141
|
refresh_token: data['refresh_token'],
|
145
|
-
expires_at: data.fetch('expiration_time_millis', 0) / 1000
|
142
|
+
expires_at: data.fetch('expiration_time_millis', 0) / 1000
|
143
|
+
)
|
144
|
+
scope ||= @scope
|
146
145
|
if credentials.includes_scope?(scope)
|
147
|
-
monitor_credentials(user_id, credentials)
|
148
|
-
return credentials
|
146
|
+
return monitor_credentials(user_id, credentials)
|
149
147
|
end
|
150
148
|
nil
|
151
149
|
end
|
@@ -174,7 +172,8 @@ module Google
|
|
174
172
|
client_id: @client_id.id,
|
175
173
|
client_secret: @client_id.secret,
|
176
174
|
redirect_uri: redirect_uri_for(base_url),
|
177
|
-
scope: scope
|
175
|
+
scope: scope
|
176
|
+
)
|
178
177
|
credentials.code = code
|
179
178
|
credentials.fetch_access_token!({})
|
180
179
|
monitor_credentials(user_id, credentials)
|
@@ -234,13 +233,26 @@ module Google
|
|
234
233
|
access_token: credentials.access_token,
|
235
234
|
refresh_token: credentials.refresh_token,
|
236
235
|
scope: credentials.scope,
|
237
|
-
expiration_time_millis:
|
236
|
+
expiration_time_millis: credentials.expires_at.to_i * 1000
|
237
|
+
)
|
238
238
|
@token_store.store(user_id, json)
|
239
239
|
credentials
|
240
240
|
end
|
241
241
|
|
242
242
|
private
|
243
243
|
|
244
|
+
# @private Fetch stored token with given user_id
|
245
|
+
#
|
246
|
+
# @param [String] user_id
|
247
|
+
# Unique ID of the user for loading/storing credentials.
|
248
|
+
# @return [String] The saved token from @token_store
|
249
|
+
def stored_token(user_id)
|
250
|
+
raise NIL_USER_ID_ERROR if user_id.nil?
|
251
|
+
raise NIL_TOKEN_STORE_ERROR if @token_store.nil?
|
252
|
+
|
253
|
+
@token_store.load(user_id)
|
254
|
+
end
|
255
|
+
|
244
256
|
# Begin watching a credential for refreshes so the access token can be
|
245
257
|
# saved.
|
246
258
|
#
|
@@ -263,9 +275,9 @@ module Google
|
|
263
275
|
# Redirect URI
|
264
276
|
def redirect_uri_for(base_url)
|
265
277
|
return @callback_uri unless URI(@callback_uri).scheme.nil?
|
266
|
-
|
267
|
-
|
268
|
-
|
278
|
+
if base_url.nil? || URI(base_url).scheme.nil?
|
279
|
+
raise sprintf(ISSING_ABSOLUTE_URL_ERROR, @callback_uri)
|
280
|
+
end
|
269
281
|
URI.join(base_url, @callback_uri).to_s
|
270
282
|
end
|
271
283
|
end
|
@@ -46,9 +46,9 @@ module Google
|
|
46
46
|
#
|
47
47
|
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
48
48
|
class UserRefreshCredentials < Signet::OAuth2::Client
|
49
|
-
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/
|
50
|
-
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'
|
51
|
-
REVOKE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
49
|
+
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v4/token'.freeze
|
50
|
+
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'.freeze
|
51
|
+
REVOKE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/revoke'.freeze
|
52
52
|
extend CredentialsLoader
|
53
53
|
|
54
54
|
# Create a UserRefreshCredentials.
|
@@ -77,7 +77,7 @@ module Google
|
|
77
77
|
json_key = MultiJson.load(json_key_io.read)
|
78
78
|
wanted = %w(client_id client_secret refresh_token)
|
79
79
|
wanted.each do |key|
|
80
|
-
|
80
|
+
raise "the json is missing the #{key} field" unless json_key.key?(key)
|
81
81
|
end
|
82
82
|
json_key
|
83
83
|
end
|
@@ -92,15 +92,18 @@ module Google
|
|
92
92
|
# Revokes the credential
|
93
93
|
def revoke!(options = {})
|
94
94
|
c = options[:connection] || Faraday.default_connection
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
95
|
+
|
96
|
+
retry_with_error do
|
97
|
+
resp = c.get(REVOKE_TOKEN_URI, token: refresh_token || access_token)
|
98
|
+
case resp.status
|
99
|
+
when 200
|
100
|
+
self.access_token = nil
|
101
|
+
self.refresh_token = nil
|
102
|
+
self.expires_at = 0
|
103
|
+
else
|
104
|
+
raise(Signet::AuthorizationError,
|
105
|
+
"Unexpected error code #{resp.status}")
|
106
|
+
end
|
104
107
|
end
|
105
108
|
end
|
106
109
|
|
data/lib/googleauth/version.rb
CHANGED
@@ -66,20 +66,21 @@ module Google
|
|
66
66
|
# @see {Google::Auth::ControllerHelpers}
|
67
67
|
# @note Requires sessions are enabled
|
68
68
|
class WebUserAuthorizer < Google::Auth::UserAuthorizer
|
69
|
-
STATE_PARAM = 'state'
|
70
|
-
AUTH_CODE_KEY = 'code'
|
71
|
-
ERROR_CODE_KEY = 'error'
|
72
|
-
SESSION_ID_KEY = 'session_id'
|
73
|
-
CALLBACK_STATE_KEY = 'g-auth-callback'
|
74
|
-
CURRENT_URI_KEY = 'current_uri'
|
75
|
-
XSRF_KEY = 'g-xsrf-token'
|
76
|
-
SCOPE_KEY = 'scope'
|
69
|
+
STATE_PARAM = 'state'.freeze
|
70
|
+
AUTH_CODE_KEY = 'code'.freeze
|
71
|
+
ERROR_CODE_KEY = 'error'.freeze
|
72
|
+
SESSION_ID_KEY = 'session_id'.freeze
|
73
|
+
CALLBACK_STATE_KEY = 'g-auth-callback'.freeze
|
74
|
+
CURRENT_URI_KEY = 'current_uri'.freeze
|
75
|
+
XSRF_KEY = 'g-xsrf-token'.freeze
|
76
|
+
SCOPE_KEY = 'scope'.freeze
|
77
77
|
|
78
|
-
NIL_REQUEST_ERROR = 'Request is required.'
|
79
|
-
NIL_SESSION_ERROR = 'Sessions must be enabled'
|
80
|
-
MISSING_AUTH_CODE_ERROR = 'Missing authorization code in request'
|
81
|
-
AUTHORIZATION_ERROR = 'Authorization error: %s'
|
82
|
-
INVALID_STATE_TOKEN_ERROR =
|
78
|
+
NIL_REQUEST_ERROR = 'Request is required.'.freeze
|
79
|
+
NIL_SESSION_ERROR = 'Sessions must be enabled'.freeze
|
80
|
+
MISSING_AUTH_CODE_ERROR = 'Missing authorization code in request'.freeze
|
81
|
+
AUTHORIZATION_ERROR = 'Authorization error: %s'.freeze
|
82
|
+
INVALID_STATE_TOKEN_ERROR =
|
83
|
+
'State token does not match expected value'.freeze
|
83
84
|
|
84
85
|
class << self
|
85
86
|
attr_accessor :default
|
@@ -128,13 +129,15 @@ module Google
|
|
128
129
|
# credentials & next URL to redirect to
|
129
130
|
def handle_auth_callback(user_id, request)
|
130
131
|
callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state(
|
131
|
-
request
|
132
|
+
request
|
133
|
+
)
|
132
134
|
WebUserAuthorizer.validate_callback_state(callback_state, request)
|
133
135
|
credentials = get_and_store_credentials_from_code(
|
134
136
|
user_id: user_id,
|
135
137
|
code: callback_state[AUTH_CODE_KEY],
|
136
138
|
scope: callback_state[SCOPE_KEY],
|
137
|
-
base_url: request.url
|
139
|
+
base_url: request.url
|
140
|
+
)
|
138
141
|
[credentials, redirect_uri]
|
139
142
|
end
|
140
143
|
|
@@ -156,14 +159,15 @@ module Google
|
|
156
159
|
def get_authorization_url(options = {})
|
157
160
|
options = options.dup
|
158
161
|
request = options[:request]
|
159
|
-
|
160
|
-
|
162
|
+
raise NIL_REQUEST_ERROR if request.nil?
|
163
|
+
raise NIL_SESSION_ERROR if request.session.nil?
|
161
164
|
|
162
165
|
redirect_to = options[:redirect_to] || request.url
|
163
166
|
request.session[XSRF_KEY] = SecureRandom.base64
|
164
167
|
options[:state] = MultiJson.dump(
|
165
168
|
SESSION_ID_KEY => request.session[XSRF_KEY],
|
166
|
-
CURRENT_URI_KEY => redirect_to
|
169
|
+
CURRENT_URI_KEY => redirect_to
|
170
|
+
)
|
167
171
|
options[:base_url] = request.url
|
168
172
|
super(options)
|
169
173
|
end
|
@@ -193,7 +197,8 @@ module Google
|
|
193
197
|
user_id: user_id,
|
194
198
|
code: callback_state[AUTH_CODE_KEY],
|
195
199
|
scope: callback_state[SCOPE_KEY],
|
196
|
-
base_url: request.url
|
200
|
+
base_url: request.url
|
201
|
+
)
|
197
202
|
else
|
198
203
|
super(user_id, scope)
|
199
204
|
end
|
@@ -204,7 +209,7 @@ module Google
|
|
204
209
|
redirect_uri = state[CURRENT_URI_KEY]
|
205
210
|
callback_state = {
|
206
211
|
AUTH_CODE_KEY => request[AUTH_CODE_KEY],
|
207
|
-
ERROR_CODE_KEY =>
|
212
|
+
ERROR_CODE_KEY => request[ERROR_CODE_KEY],
|
208
213
|
SESSION_ID_KEY => state[SESSION_ID_KEY],
|
209
214
|
SCOPE_KEY => request[SCOPE_KEY]
|
210
215
|
}
|
@@ -223,12 +228,12 @@ module Google
|
|
223
228
|
# Current request
|
224
229
|
def self.validate_callback_state(state, request)
|
225
230
|
if state[AUTH_CODE_KEY].nil?
|
226
|
-
|
231
|
+
raise Signet::AuthorizationError, MISSING_AUTH_CODE_ERROR
|
227
232
|
elsif state[ERROR_CODE_KEY]
|
228
|
-
|
229
|
-
|
233
|
+
raise Signet::AuthorizationError,
|
234
|
+
sprintf(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY])
|
230
235
|
elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY]
|
231
|
-
|
236
|
+
raise Signet::AuthorizationError, INVALID_STATE_TOKEN_ERROR
|
232
237
|
end
|
233
238
|
end
|
234
239
|
|
@@ -254,7 +259,7 @@ module Google
|
|
254
259
|
#
|
255
260
|
# @see {Google::Auth::WebUserAuthorizer}
|
256
261
|
class CallbackApp
|
257
|
-
LOCATION_HEADER = 'Location'
|
262
|
+
LOCATION_HEADER = 'Location'.freeze
|
258
263
|
REDIR_STATUS = 302
|
259
264
|
ERROR_STATUS = 500
|
260
265
|
|
@@ -62,7 +62,8 @@ shared_examples 'apply/apply! are OK' do
|
|
62
62
|
@client.on_refresh(&b)
|
63
63
|
@client.fetch_access_token!
|
64
64
|
end.to yield_with_args(have_attributes(
|
65
|
-
access_token: '1/abcdef1234567890'
|
65
|
+
access_token: '1/abcdef1234567890'
|
66
|
+
))
|
66
67
|
expect(stub).to have_been_requested
|
67
68
|
end
|
68
69
|
end
|
@@ -131,10 +132,10 @@ shared_examples 'apply/apply! are OK' do
|
|
131
132
|
end
|
132
133
|
|
133
134
|
it 'should fetch a new token if the current one is expired' do
|
134
|
-
|
135
|
-
|
135
|
+
token1 = '1/abcdef1234567890'
|
136
|
+
token2 = '2/abcdef1234567891'
|
136
137
|
|
137
|
-
[
|
138
|
+
[token1, token2].each do |t|
|
138
139
|
make_auth_stubs access_token: t
|
139
140
|
md = { foo: 'bar' }
|
140
141
|
got = @client.apply(md)
|