signet 0.1.4 → 0.2.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.
- data/CHANGELOG +4 -0
- data/README +3 -0
- data/lib/signet.rb +54 -0
- data/lib/signet/errors.rb +44 -9
- data/lib/signet/oauth_1.rb +20 -19
- data/lib/signet/oauth_1/client.rb +23 -13
- data/lib/signet/oauth_2.rb +148 -0
- data/lib/signet/oauth_2/client.rb +851 -0
- data/lib/signet/version.rb +4 -4
- data/spec/signet/oauth_1_spec.rb +25 -2
- data/spec/signet/oauth_2/client_spec.rb +70 -0
- data/spec/signet/oauth_2_spec.rb +164 -0
- data/spec/signet_spec.rb +80 -0
- data/tasks/gem.rake +1 -0
- data/tasks/spec.rake +4 -2
- metadata +38 -17
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
# Copyright (C) 2010 Google Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
require 'stringio'
|
|
16
|
+
require 'addressable/uri'
|
|
17
|
+
require 'signet'
|
|
18
|
+
require 'signet/errors'
|
|
19
|
+
require 'signet/oauth_2'
|
|
20
|
+
|
|
21
|
+
module Signet
|
|
22
|
+
module OAuth2
|
|
23
|
+
class Client
|
|
24
|
+
##
|
|
25
|
+
# Creates an OAuth 2.0 client.
|
|
26
|
+
#
|
|
27
|
+
# @param [Hash] options
|
|
28
|
+
# The configuration parameters for the client.
|
|
29
|
+
# - <code>:authorization_uri</code> —
|
|
30
|
+
# The authorization server's HTTP endpoint capable of
|
|
31
|
+
# authenticating the end-user and obtaining authorization.
|
|
32
|
+
# - <code>:token_credential_uri</code> —
|
|
33
|
+
# The authorization server's HTTP endpoint capable of issuing
|
|
34
|
+
# tokens and refreshing expired tokens.
|
|
35
|
+
# - <code>:client_id</code> —
|
|
36
|
+
# A unique identifier issued to the client to identify itself to the
|
|
37
|
+
# authorization server.
|
|
38
|
+
# - <code>:client_secret</code> —
|
|
39
|
+
# A shared symmetric secret issued by the authorization server,
|
|
40
|
+
# which is used to authenticate the client.
|
|
41
|
+
# - <code>:scope</code> —
|
|
42
|
+
# The scope of the access request, expressed either as an Array
|
|
43
|
+
# or as a space-delimited String.
|
|
44
|
+
# - <code>:state</code> —
|
|
45
|
+
# An arbitrary string designed to allow the client to maintain state.
|
|
46
|
+
# - <code>:code</code> —
|
|
47
|
+
# The authorization code received from the authorization server.
|
|
48
|
+
# - <code>:redirect_uri</code> —
|
|
49
|
+
# The redirection URI used in the initial request.
|
|
50
|
+
# - <code>:username</code> —
|
|
51
|
+
# The resource owner's username.
|
|
52
|
+
# - <code>:password</code> —
|
|
53
|
+
# The resource owner's password.
|
|
54
|
+
# - <code>:assertion_type</code> —
|
|
55
|
+
# The format of the assertion as defined by the
|
|
56
|
+
# authorization server. The value must be an absolute URI.
|
|
57
|
+
# - <code>:assertion</code> —
|
|
58
|
+
# The raw assertion value.
|
|
59
|
+
# - <code>:refresh_token</code> —
|
|
60
|
+
# The refresh token associated with the access token
|
|
61
|
+
# to be refreshed.
|
|
62
|
+
# - <code>:access_token</code> —
|
|
63
|
+
# The current access token for this client.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# client = Signet::OAuth2::Client.new(
|
|
67
|
+
# :authorization_endpoint_uri =>
|
|
68
|
+
# 'https://example.server.com/authorization',
|
|
69
|
+
# :token_endpoint_uri =>
|
|
70
|
+
# 'https://example.server.com/token',
|
|
71
|
+
# :client_id => 'anonymous',
|
|
72
|
+
# :client_secret => 'anonymous',
|
|
73
|
+
# :scope => 'example',
|
|
74
|
+
# :redirect_uri => 'https://example.client.com/oauth'
|
|
75
|
+
# )
|
|
76
|
+
#
|
|
77
|
+
# @see Signet::OAuth2::Client#update!
|
|
78
|
+
def initialize(options={})
|
|
79
|
+
self.update!(options)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# Updates an OAuth 2.0 client.
|
|
84
|
+
#
|
|
85
|
+
# @param [Hash] options
|
|
86
|
+
# The configuration parameters for the client.
|
|
87
|
+
# - <code>:authorization_uri</code> —
|
|
88
|
+
# The authorization server's HTTP endpoint capable of
|
|
89
|
+
# authenticating the end-user and obtaining authorization.
|
|
90
|
+
# - <code>:token_credential_uri</code> —
|
|
91
|
+
# The authorization server's HTTP endpoint capable of issuing
|
|
92
|
+
# tokens and refreshing expired tokens.
|
|
93
|
+
# - <code>:client_id</code> —
|
|
94
|
+
# A unique identifier issued to the client to identify itself to the
|
|
95
|
+
# authorization server.
|
|
96
|
+
# - <code>:client_secret</code> —
|
|
97
|
+
# A shared symmetric secret issued by the authorization server,
|
|
98
|
+
# which is used to authenticate the client.
|
|
99
|
+
# - <code>:scope</code> —
|
|
100
|
+
# The scope of the access request, expressed either as an Array
|
|
101
|
+
# or as a space-delimited String.
|
|
102
|
+
# - <code>:state</code> —
|
|
103
|
+
# An arbitrary string designed to allow the client to maintain state.
|
|
104
|
+
# - <code>:code</code> —
|
|
105
|
+
# The authorization code received from the authorization server.
|
|
106
|
+
# - <code>:redirect_uri</code> —
|
|
107
|
+
# The redirection URI used in the initial request.
|
|
108
|
+
# - <code>:username</code> —
|
|
109
|
+
# The resource owner's username.
|
|
110
|
+
# - <code>:password</code> —
|
|
111
|
+
# The resource owner's password.
|
|
112
|
+
# - <code>:assertion_type</code> —
|
|
113
|
+
# The format of the assertion as defined by the
|
|
114
|
+
# authorization server. The value must be an absolute URI.
|
|
115
|
+
# - <code>:assertion</code> —
|
|
116
|
+
# The raw assertion value.
|
|
117
|
+
# - <code>:refresh_token</code> —
|
|
118
|
+
# The refresh token associated with the access token
|
|
119
|
+
# to be refreshed.
|
|
120
|
+
# - <code>:access_token</code> —
|
|
121
|
+
# The current access token for this client.
|
|
122
|
+
# - <code>:expires_in</code> —
|
|
123
|
+
# The current access token for this client.
|
|
124
|
+
#
|
|
125
|
+
# @example
|
|
126
|
+
# client.update!(
|
|
127
|
+
# :code => 'i1WsRn1uB1',
|
|
128
|
+
# :access_token => 'FJQbwq9',
|
|
129
|
+
# :expires_in => 3600
|
|
130
|
+
# )
|
|
131
|
+
#
|
|
132
|
+
# @see Signet::OAuth2::Client#initialize
|
|
133
|
+
# @see Signet::OAuth2::Client#update_token!
|
|
134
|
+
def update!(options={})
|
|
135
|
+
# Normalize key to String to allow indifferent access.
|
|
136
|
+
options = options.inject({}) do |accu, (key, value)|
|
|
137
|
+
accu[key.to_s] = value
|
|
138
|
+
accu
|
|
139
|
+
end
|
|
140
|
+
self.authorization_uri = options["authorization_uri"]
|
|
141
|
+
self.token_credential_uri = options["token_credential_uri"]
|
|
142
|
+
self.client_id = options["client_id"]
|
|
143
|
+
self.client_secret = options["client_secret"]
|
|
144
|
+
self.scope = options["scope"]
|
|
145
|
+
self.state = options["state"]
|
|
146
|
+
self.code = options["code"]
|
|
147
|
+
self.redirect_uri = options["redirect_uri"]
|
|
148
|
+
self.username = options["username"]
|
|
149
|
+
self.password = options["password"]
|
|
150
|
+
self.assertion_type = options["assertion_type"]
|
|
151
|
+
self.assertion = options["assertion"]
|
|
152
|
+
self.update_token!(options)
|
|
153
|
+
return self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# Updates an OAuth 2.0 client.
|
|
158
|
+
#
|
|
159
|
+
# @param [Hash] options
|
|
160
|
+
# The configuration parameters related to the token.
|
|
161
|
+
# - <code>:refresh_token</code> —
|
|
162
|
+
# The refresh token associated with the access token
|
|
163
|
+
# to be refreshed.
|
|
164
|
+
# - <code>:access_token</code> —
|
|
165
|
+
# The current access token for this client.
|
|
166
|
+
# - <code>:expires_in</code> —
|
|
167
|
+
# The current access token for this client.
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# client.update!(
|
|
171
|
+
# :refresh_token => 'n4E9O119d',
|
|
172
|
+
# :access_token => 'FJQbwq9',
|
|
173
|
+
# :expires_in => 3600
|
|
174
|
+
# )
|
|
175
|
+
#
|
|
176
|
+
# @see Signet::OAuth2::Client#initialize
|
|
177
|
+
# @see Signet::OAuth2::Client#update!
|
|
178
|
+
def update_token!(options={})
|
|
179
|
+
# Normalize key to String to allow indifferent access.
|
|
180
|
+
options = options.inject({}) do |accu, (key, value)|
|
|
181
|
+
accu[key.to_s] = value
|
|
182
|
+
accu
|
|
183
|
+
end
|
|
184
|
+
self.refresh_token = options["refresh_token"]
|
|
185
|
+
self.access_token = options["access_token"]
|
|
186
|
+
self.expires_in = options["expires_in"]
|
|
187
|
+
return self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
##
|
|
191
|
+
# Returns the authorization URI that the user should be redirected to.
|
|
192
|
+
#
|
|
193
|
+
# @return [Addressable::URI] The authorization URI.
|
|
194
|
+
#
|
|
195
|
+
# @see Signet::OAuth2.generate_authorization_uri
|
|
196
|
+
def authorization_uri(options={})
|
|
197
|
+
return nil if @authorization_uri == nil
|
|
198
|
+
unless options[:response_type]
|
|
199
|
+
options[:response_type] = :code
|
|
200
|
+
end
|
|
201
|
+
unless options[:client_id]
|
|
202
|
+
if self.client_id
|
|
203
|
+
options[:client_id] = self.client_id
|
|
204
|
+
else
|
|
205
|
+
raise ArgumentError, "Missing required client identifier."
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
unless options[:redirect_uri]
|
|
209
|
+
if self.redirect_uri
|
|
210
|
+
options[:redirect_uri] = self.redirect_uri
|
|
211
|
+
else
|
|
212
|
+
raise ArgumentError, "Missing required redirect URI."
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
if !options[:scope] && self.scope
|
|
216
|
+
options[:scope] = self.scope.join(' ')
|
|
217
|
+
end
|
|
218
|
+
options[:state] = self.state unless options[:state]
|
|
219
|
+
uri = Addressable::URI.parse(
|
|
220
|
+
::Signet::OAuth2.generate_authorization_uri(
|
|
221
|
+
@authorization_uri, options
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
if uri.normalized_scheme != 'https'
|
|
225
|
+
raise Signet::UnsafeOperationError,
|
|
226
|
+
'Authorization endpoint must be protected by TLS.'
|
|
227
|
+
end
|
|
228
|
+
return uri
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
##
|
|
232
|
+
# Sets the authorization URI for this client.
|
|
233
|
+
#
|
|
234
|
+
# @param [Addressable::URI, String, #to_str] new_authorization_uri
|
|
235
|
+
# The authorization URI.
|
|
236
|
+
def authorization_uri=(new_authorization_uri)
|
|
237
|
+
if new_authorization_uri != nil
|
|
238
|
+
new_authorization_uri =
|
|
239
|
+
Addressable::URI.parse(new_authorization_uri)
|
|
240
|
+
@authorization_uri = new_authorization_uri
|
|
241
|
+
else
|
|
242
|
+
@authorization_uri = nil
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
##
|
|
247
|
+
# Returns the token credential URI for this client.
|
|
248
|
+
#
|
|
249
|
+
# @return [Addressable::URI] The token credential URI.
|
|
250
|
+
def token_credential_uri
|
|
251
|
+
return @token_credential_uri
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
##
|
|
255
|
+
# Sets the token credential URI for this client.
|
|
256
|
+
#
|
|
257
|
+
# @param [Addressable::URI, String, #to_str] new_token_credential_uri
|
|
258
|
+
# The token credential URI.
|
|
259
|
+
def token_credential_uri=(new_token_credential_uri)
|
|
260
|
+
if new_token_credential_uri != nil
|
|
261
|
+
new_token_credential_uri =
|
|
262
|
+
Addressable::URI.parse(new_token_credential_uri)
|
|
263
|
+
@token_credential_uri = new_token_credential_uri
|
|
264
|
+
else
|
|
265
|
+
@token_credential_uri = nil
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
##
|
|
270
|
+
# Returns the client identifier for this client.
|
|
271
|
+
#
|
|
272
|
+
# @return [String] The client identifier.
|
|
273
|
+
def client_id
|
|
274
|
+
return @client_id
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
# Sets the client identifier for this client.
|
|
279
|
+
#
|
|
280
|
+
# @param [String] new_client_id
|
|
281
|
+
# The client identifier.
|
|
282
|
+
def client_id=(new_client_id)
|
|
283
|
+
@client_id = new_client_id
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
##
|
|
287
|
+
# Returns the client secret for this client.
|
|
288
|
+
#
|
|
289
|
+
# @return [String] The client secret.
|
|
290
|
+
def client_secret
|
|
291
|
+
return @client_secret
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
##
|
|
295
|
+
# Sets the client secret for this client.
|
|
296
|
+
#
|
|
297
|
+
# @param [String] new_client_secret
|
|
298
|
+
# The client secret.
|
|
299
|
+
def client_secret=(new_client_secret)
|
|
300
|
+
@client_secret = new_client_secret
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
# Returns the scope for this client. Scope is a list of access ranges
|
|
305
|
+
# defined by the authorization server.
|
|
306
|
+
#
|
|
307
|
+
# @return [Array] The scope of access the client is requesting.
|
|
308
|
+
def scope
|
|
309
|
+
return @scope
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
##
|
|
313
|
+
# Sets the scope for this client.
|
|
314
|
+
#
|
|
315
|
+
# @param [Array, String] new_scope
|
|
316
|
+
# The scope of access the client is requesting. This may be
|
|
317
|
+
# expressed as either an Array of String objects or as a
|
|
318
|
+
# space-delimited String.
|
|
319
|
+
def scope=(new_scope)
|
|
320
|
+
case new_scope
|
|
321
|
+
when Array
|
|
322
|
+
new_scope.each do |scope|
|
|
323
|
+
if scope.include?(' ')
|
|
324
|
+
raise Signet::ParseError,
|
|
325
|
+
"Individual scopes cannot contain the space character."
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
@scope = new_scope
|
|
329
|
+
when String
|
|
330
|
+
@scope = new_scope.split(' ')
|
|
331
|
+
when nil
|
|
332
|
+
@scope = nil
|
|
333
|
+
else
|
|
334
|
+
raise TypeError, "Expected Array or String, got #{new_scope.class}"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
##
|
|
339
|
+
# Returns the client's current state value.
|
|
340
|
+
#
|
|
341
|
+
# @return [String] The state value.
|
|
342
|
+
def state
|
|
343
|
+
return @state
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
##
|
|
347
|
+
# Sets the client's current state value.
|
|
348
|
+
#
|
|
349
|
+
# @param [String] new_state
|
|
350
|
+
# The state value.
|
|
351
|
+
def state=(new_state)
|
|
352
|
+
@state = new_state
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
##
|
|
356
|
+
# Returns the authorization code issued to this client.
|
|
357
|
+
# Used only by the authorization code access grant type.
|
|
358
|
+
#
|
|
359
|
+
# @return [String] The authorization code.
|
|
360
|
+
def code
|
|
361
|
+
return @code
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
##
|
|
365
|
+
# Sets the authorization code issued to this client.
|
|
366
|
+
# Used only by the authorization code access grant type.
|
|
367
|
+
#
|
|
368
|
+
# @param [String] new_code
|
|
369
|
+
# The authorization code.
|
|
370
|
+
def code=(new_code)
|
|
371
|
+
@code = new_code
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
##
|
|
375
|
+
# Returns the redirect URI for this client.
|
|
376
|
+
#
|
|
377
|
+
# @return [String] The redirect URI.
|
|
378
|
+
def redirect_uri
|
|
379
|
+
return @redirect_uri
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
##
|
|
383
|
+
# Sets the redirect URI for this client.
|
|
384
|
+
#
|
|
385
|
+
# @param [String] new_redirect_uri
|
|
386
|
+
# The redirect URI.
|
|
387
|
+
def redirect_uri=(new_redirect_uri)
|
|
388
|
+
new_redirect_uri = Addressable::URI.parse(new_redirect_uri)
|
|
389
|
+
if new_redirect_uri == nil || new_redirect_uri.absolute?
|
|
390
|
+
@redirect_uri = new_redirect_uri
|
|
391
|
+
else
|
|
392
|
+
raise ArgumentError, "Redirect URI must be an absolute URI."
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
##
|
|
397
|
+
# Returns the username associated with this client.
|
|
398
|
+
# Used only by the resource owner password credential access grant type.
|
|
399
|
+
#
|
|
400
|
+
# @return [String] The username.
|
|
401
|
+
def username
|
|
402
|
+
return @username
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
##
|
|
406
|
+
# Sets the username associated with this client.
|
|
407
|
+
# Used only by the resource owner password credential access grant type.
|
|
408
|
+
#
|
|
409
|
+
# @param [String] new_username
|
|
410
|
+
# The username.
|
|
411
|
+
def username=(new_username)
|
|
412
|
+
@username = new_username
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
##
|
|
416
|
+
# Returns the password associated with this client.
|
|
417
|
+
# Used only by the resource owner password credential access grant type.
|
|
418
|
+
#
|
|
419
|
+
# @return [String] The password.
|
|
420
|
+
def password
|
|
421
|
+
return @password
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
##
|
|
425
|
+
# Sets the password associated with this client.
|
|
426
|
+
# Used only by the resource owner password credential access grant type.
|
|
427
|
+
#
|
|
428
|
+
# @param [String] new_password
|
|
429
|
+
# The password.
|
|
430
|
+
def password=(new_password)
|
|
431
|
+
@password = new_password
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
##
|
|
435
|
+
# Returns the assertion type associated with this client.
|
|
436
|
+
# Used only by the assertion access grant type.
|
|
437
|
+
#
|
|
438
|
+
# @return [String] The assertion type.
|
|
439
|
+
def assertion_type
|
|
440
|
+
return @assertion_type
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
##
|
|
444
|
+
# Sets the assertion type associated with this client.
|
|
445
|
+
# Used only by the assertion access grant type.
|
|
446
|
+
#
|
|
447
|
+
# @param [String] new_assertion_type
|
|
448
|
+
# The password.
|
|
449
|
+
def assertion_type=(new_assertion_type)
|
|
450
|
+
new_assertion_type = Addressable::URI.parse(new_assertion_type)
|
|
451
|
+
if new_assertion_type == nil || new_assertion_type.absolute?
|
|
452
|
+
@assertion_type = new_assertion_type
|
|
453
|
+
else
|
|
454
|
+
raise ArgumentError, "Assertion type must be an absolute URI."
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
##
|
|
459
|
+
# Returns the assertion associated with this client.
|
|
460
|
+
# Used only by the assertion access grant type.
|
|
461
|
+
#
|
|
462
|
+
# @return [String] The assertion.
|
|
463
|
+
def assertion
|
|
464
|
+
return @assertion
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
##
|
|
468
|
+
# Sets the assertion associated with this client.
|
|
469
|
+
# Used only by the assertion access grant type.
|
|
470
|
+
#
|
|
471
|
+
# @param [String] new_assertion
|
|
472
|
+
# The assertion.
|
|
473
|
+
def assertion=(new_assertion)
|
|
474
|
+
@assertion = new_assertion
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
##
|
|
478
|
+
# Returns the refresh token associated with this client.
|
|
479
|
+
#
|
|
480
|
+
# @return [String] The refresh token.
|
|
481
|
+
def refresh_token
|
|
482
|
+
return @refresh_token
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
##
|
|
486
|
+
# Sets the refresh token associated with this client.
|
|
487
|
+
#
|
|
488
|
+
# @param [String] new_refresh_token
|
|
489
|
+
# The refresh token.
|
|
490
|
+
def refresh_token=(new_refresh_token)
|
|
491
|
+
@refresh_token = new_refresh_token
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
##
|
|
495
|
+
# Returns the access token associated with this client.
|
|
496
|
+
#
|
|
497
|
+
# @return [String] The access token.
|
|
498
|
+
def access_token
|
|
499
|
+
return @access_token
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
##
|
|
503
|
+
# Sets the access token associated with this client.
|
|
504
|
+
#
|
|
505
|
+
# @param [String] new_access_token
|
|
506
|
+
# The access token.
|
|
507
|
+
def access_token=(new_access_token)
|
|
508
|
+
@access_token = new_access_token
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
##
|
|
512
|
+
# Returns the lifetime of the access token in seconds.
|
|
513
|
+
#
|
|
514
|
+
# @return [Integer] The access token lifetime.
|
|
515
|
+
def expires_in
|
|
516
|
+
return @expires_in
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
##
|
|
520
|
+
# Sets the lifetime of the access token in seconds. Resets the issued
|
|
521
|
+
# timestamp.
|
|
522
|
+
#
|
|
523
|
+
# @param [String] new_expires_in
|
|
524
|
+
# The access token lifetime.
|
|
525
|
+
def expires_in=(new_expires_in)
|
|
526
|
+
@expires_in = new_expires_in.to_i
|
|
527
|
+
@issued_at = Time.now
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
##
|
|
531
|
+
# Returns the timestamp the access token was issued at.
|
|
532
|
+
#
|
|
533
|
+
# @return [Integer] The access token issuance time.
|
|
534
|
+
def issued_at
|
|
535
|
+
return @issued_at
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
##
|
|
539
|
+
# Sets the timestamp the access token was issued at.
|
|
540
|
+
#
|
|
541
|
+
# @param [String] new_issued_at
|
|
542
|
+
# The access token issuance time.
|
|
543
|
+
def issued_at=(new_issued_at)
|
|
544
|
+
@issued_at = new_issued_at
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
##
|
|
548
|
+
# Returns the timestamp the access token will expire at.
|
|
549
|
+
#
|
|
550
|
+
# @return [Integer] The access token lifetime.
|
|
551
|
+
def expires_at
|
|
552
|
+
if @issued_at && @expires_in
|
|
553
|
+
return @issued_at + @expires_in
|
|
554
|
+
else
|
|
555
|
+
return nil
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
##
|
|
560
|
+
# Returns true if the access token has expired.
|
|
561
|
+
#
|
|
562
|
+
# @return [TrueClass, FalseClass]
|
|
563
|
+
# The expiration state of the access token.
|
|
564
|
+
def expired?
|
|
565
|
+
return Time.now >= self.expires_at
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
##
|
|
569
|
+
# Returns the inferred grant type, based on the current state of the
|
|
570
|
+
# client object. Returns `"none"` if the client has insufficient
|
|
571
|
+
# information to make an in-band authorization request.
|
|
572
|
+
#
|
|
573
|
+
# @return [String]
|
|
574
|
+
# The inferred grant type.
|
|
575
|
+
def grant_type
|
|
576
|
+
if self.code && self.redirect_uri
|
|
577
|
+
return 'authorization_code'
|
|
578
|
+
elsif self.assertion && self.assertion_type
|
|
579
|
+
return 'assertion'
|
|
580
|
+
elsif self.refresh_token
|
|
581
|
+
return 'refresh_token'
|
|
582
|
+
elsif self.username && self.password
|
|
583
|
+
return 'password'
|
|
584
|
+
else
|
|
585
|
+
# We don't have sufficient auth information, assume an out-of-band
|
|
586
|
+
# authorization arrangement between the client and server.
|
|
587
|
+
return 'none'
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
##
|
|
592
|
+
# Generates a request for token credentials.
|
|
593
|
+
#
|
|
594
|
+
# @param [Hash] options
|
|
595
|
+
# The configuration parameters for the request.
|
|
596
|
+
# - <code>:code</code> —
|
|
597
|
+
# The authorization code.
|
|
598
|
+
#
|
|
599
|
+
# @return [Array] The request object.
|
|
600
|
+
def generate_access_token_request
|
|
601
|
+
if self.token_credential_uri == nil
|
|
602
|
+
raise ArgumentError, 'Missing token endpoint URI.'
|
|
603
|
+
end
|
|
604
|
+
if self.client_id == nil
|
|
605
|
+
raise ArgumentError, 'Missing client identifier.'
|
|
606
|
+
end
|
|
607
|
+
if self.client_secret == nil
|
|
608
|
+
raise ArgumentError, 'Missing client secret.'
|
|
609
|
+
end
|
|
610
|
+
if self.redirect_uri && !self.code
|
|
611
|
+
# Grant type can be assumed to be `authorization_code` because of
|
|
612
|
+
# the presence of the redirect URI.
|
|
613
|
+
raise ArgumentError, 'Missing authorization code.'
|
|
614
|
+
end
|
|
615
|
+
method = 'POST'
|
|
616
|
+
parameters = {"grant_type" => self.grant_type}
|
|
617
|
+
case self.grant_type
|
|
618
|
+
when 'authorization_code'
|
|
619
|
+
parameters['code'] = self.code
|
|
620
|
+
parameters['redirect_uri'] = self.redirect_uri
|
|
621
|
+
when 'password'
|
|
622
|
+
parameters['username'] = self.username
|
|
623
|
+
parameters['password'] = self.password
|
|
624
|
+
when 'assertion'
|
|
625
|
+
parameters['assertion_type'] = self.assertion_type
|
|
626
|
+
parameters['assertion'] = self.assertion
|
|
627
|
+
when 'refresh_token'
|
|
628
|
+
parameters['refresh_token'] = self.refresh_token
|
|
629
|
+
end
|
|
630
|
+
parameters['client_id'] = self.client_id
|
|
631
|
+
parameters['client_secret'] = self.client_secret
|
|
632
|
+
headers = [
|
|
633
|
+
['Cache-Control', 'no-store'],
|
|
634
|
+
['Content-Type', 'application/x-www-form-urlencoded']
|
|
635
|
+
]
|
|
636
|
+
return [
|
|
637
|
+
method,
|
|
638
|
+
self.token_credential_uri.to_str,
|
|
639
|
+
headers,
|
|
640
|
+
[Addressable::URI.form_encode(parameters)]
|
|
641
|
+
]
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def fetch_access_token(options={})
|
|
645
|
+
adapter = options[:adapter]
|
|
646
|
+
unless adapter
|
|
647
|
+
require 'httpadapter'
|
|
648
|
+
require 'httpadapter/adapters/net_http'
|
|
649
|
+
adapter = HTTPAdapter::NetHTTPRequestAdapter
|
|
650
|
+
end
|
|
651
|
+
connection = options[:connection]
|
|
652
|
+
request = self.generate_access_token_request
|
|
653
|
+
response = HTTPAdapter.transmit(request, adapter, connection)
|
|
654
|
+
status, headers, body = response
|
|
655
|
+
merged_body = StringIO.new
|
|
656
|
+
body.each do |chunk|
|
|
657
|
+
merged_body.write(chunk)
|
|
658
|
+
end
|
|
659
|
+
body = merged_body.string
|
|
660
|
+
if status.to_i == 200
|
|
661
|
+
return ::Signet::OAuth2.parse_json_credentials(body)
|
|
662
|
+
elsif [400, 401, 403].include?(status.to_i)
|
|
663
|
+
message = 'Authorization failed.'
|
|
664
|
+
if body.strip.length > 0
|
|
665
|
+
message += " Server message:\n#{body.strip}"
|
|
666
|
+
end
|
|
667
|
+
raise ::Signet::AuthorizationError.new(
|
|
668
|
+
message, :request => request, :response => response
|
|
669
|
+
)
|
|
670
|
+
else
|
|
671
|
+
message = "Unexpected status code: #{status}."
|
|
672
|
+
if body.strip.length > 0
|
|
673
|
+
message += " Server message:\n#{body.strip}"
|
|
674
|
+
end
|
|
675
|
+
raise ::Signet::AuthorizationError.new(
|
|
676
|
+
message, :request => request, :response => response
|
|
677
|
+
)
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def fetch_access_token!(options={})
|
|
682
|
+
token_hash = self.fetch_access_token(options)
|
|
683
|
+
if token_hash
|
|
684
|
+
# No-op for grant types other than `authorization_code`.
|
|
685
|
+
# An authorization code is a one-time use token and is immediately
|
|
686
|
+
# revoked after usage.
|
|
687
|
+
self.code = nil
|
|
688
|
+
self.issued_at = Time.now
|
|
689
|
+
self.update_token!(token_hash)
|
|
690
|
+
end
|
|
691
|
+
return token_hash
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
##
|
|
695
|
+
# Generates an authenticated request for protected resources.
|
|
696
|
+
#
|
|
697
|
+
# @param [Hash] options
|
|
698
|
+
# The configuration parameters for the request.
|
|
699
|
+
# - <code>:request</code> —
|
|
700
|
+
# A pre-constructed request. An OAuth 2 Authorization header
|
|
701
|
+
# will be added to it, as well as an explicit Cache-Control
|
|
702
|
+
# `no-store` directive.
|
|
703
|
+
# - <code>:method</code> —
|
|
704
|
+
# The HTTP method for the request. Defaults to 'GET'.
|
|
705
|
+
# - <code>:uri</code> —
|
|
706
|
+
# The URI for the request.
|
|
707
|
+
# - <code>:headers</code> —
|
|
708
|
+
# The HTTP headers for the request.
|
|
709
|
+
# - <code>:body</code> —
|
|
710
|
+
# The HTTP body for the request.
|
|
711
|
+
# - <code>:realm</code> —
|
|
712
|
+
# The Authorization realm. See RFC 2617.
|
|
713
|
+
#
|
|
714
|
+
# @return [Array] The request object.
|
|
715
|
+
def generate_authenticated_request(options={})
|
|
716
|
+
if self.access_token == nil
|
|
717
|
+
raise ArgumentError, 'Missing access token.'
|
|
718
|
+
end
|
|
719
|
+
options = {
|
|
720
|
+
:realm => nil
|
|
721
|
+
}.merge(options)
|
|
722
|
+
if options[:request]
|
|
723
|
+
if options[:request].kind_of?(Array)
|
|
724
|
+
request = options[:request]
|
|
725
|
+
elsif options[:adapter] || options[:request].respond_to?(:to_ary)
|
|
726
|
+
request =
|
|
727
|
+
HTTPAdapter.adapt_request(options[:request], options[:adapter])
|
|
728
|
+
end
|
|
729
|
+
method, uri, headers, body = request
|
|
730
|
+
else
|
|
731
|
+
method = options[:method] || 'GET'
|
|
732
|
+
uri = options[:uri]
|
|
733
|
+
headers = options[:headers] || []
|
|
734
|
+
body = options[:body] || ''
|
|
735
|
+
end
|
|
736
|
+
headers = headers.to_a if headers.kind_of?(Hash)
|
|
737
|
+
request_components = {
|
|
738
|
+
:method => method,
|
|
739
|
+
:uri => uri,
|
|
740
|
+
:headers => headers,
|
|
741
|
+
:body => body
|
|
742
|
+
}
|
|
743
|
+
# Verify that we have all pieces required to return an HTTP request
|
|
744
|
+
request_components.each do |(key, value)|
|
|
745
|
+
unless value
|
|
746
|
+
raise ArgumentError, "Missing :#{key} parameter."
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
if !body.kind_of?(String) && body.respond_to?(:each)
|
|
750
|
+
# Just in case we get a chunked body
|
|
751
|
+
merged_body = StringIO.new
|
|
752
|
+
body.each do |chunk|
|
|
753
|
+
merged_body.write(chunk)
|
|
754
|
+
end
|
|
755
|
+
body = merged_body.string
|
|
756
|
+
end
|
|
757
|
+
if !body.kind_of?(String)
|
|
758
|
+
raise TypeError, "Expected String, got #{body.class}."
|
|
759
|
+
end
|
|
760
|
+
method = method.to_s.upcase
|
|
761
|
+
headers << [
|
|
762
|
+
'Authorization',
|
|
763
|
+
::Signet::OAuth2.generate_bearer_authorization_header(
|
|
764
|
+
self.access_token,
|
|
765
|
+
options[:realm] ? ['realm', options[:realm]] : nil
|
|
766
|
+
)
|
|
767
|
+
]
|
|
768
|
+
headers << ['Cache-Control', 'no-store']
|
|
769
|
+
return [method, uri.to_str, headers, [body]]
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
##
|
|
773
|
+
# Transmits a request for a protected resource.
|
|
774
|
+
#
|
|
775
|
+
# @param [Hash] options
|
|
776
|
+
# The configuration parameters for the request.
|
|
777
|
+
# - <code>:request</code> —
|
|
778
|
+
# A pre-constructed request. An OAuth 2 Authorization header
|
|
779
|
+
# will be added to it, as well as an explicit Cache-Control
|
|
780
|
+
# `no-store` directive.
|
|
781
|
+
# - <code>:method</code> —
|
|
782
|
+
# The HTTP method for the request. Defaults to 'GET'.
|
|
783
|
+
# - <code>:uri</code> —
|
|
784
|
+
# The URI for the request.
|
|
785
|
+
# - <code>:headers</code> —
|
|
786
|
+
# The HTTP headers for the request.
|
|
787
|
+
# - <code>:body</code> —
|
|
788
|
+
# The HTTP body for the request.
|
|
789
|
+
# - <code>:realm</code> —
|
|
790
|
+
# The Authorization realm. See RFC 2617.
|
|
791
|
+
# - <code>:adapter</code> —
|
|
792
|
+
# The HTTP adapter.
|
|
793
|
+
# Defaults to <code>HTTPAdapter::NetHTTPRequestAdapter</code>.
|
|
794
|
+
# - <code>:connection</code> —
|
|
795
|
+
# An open, manually managed HTTP connection.
|
|
796
|
+
# Must be of type <code>HTTPAdapter::Connection</code> and the
|
|
797
|
+
# internal connection representation must match the HTTP adapter
|
|
798
|
+
# being used.
|
|
799
|
+
#
|
|
800
|
+
# @example
|
|
801
|
+
# # Using Net::HTTP
|
|
802
|
+
# response = client.fetch_protected_resource(
|
|
803
|
+
# :uri => 'http://www.example.com/protected/resource'
|
|
804
|
+
# )
|
|
805
|
+
# status, headers, body = response
|
|
806
|
+
#
|
|
807
|
+
# @example
|
|
808
|
+
# # Using Typhoeus
|
|
809
|
+
# response = client.fetch_protected_resource(
|
|
810
|
+
# :request => Typhoeus::Request.new(
|
|
811
|
+
# 'http://www.example.com/protected/resource'
|
|
812
|
+
# ),
|
|
813
|
+
# :adapter => HTTPAdapter::TyphoeusRequestAdapter,
|
|
814
|
+
# :connection => connection
|
|
815
|
+
# )
|
|
816
|
+
# status, headers, body = response
|
|
817
|
+
#
|
|
818
|
+
# @return [Array] The response object.
|
|
819
|
+
def fetch_protected_resource(options={})
|
|
820
|
+
adapter = options[:adapter]
|
|
821
|
+
unless adapter
|
|
822
|
+
require 'httpadapter'
|
|
823
|
+
require 'httpadapter/adapters/net_http'
|
|
824
|
+
adapter = HTTPAdapter::NetHTTPRequestAdapter
|
|
825
|
+
end
|
|
826
|
+
connection = options[:connection]
|
|
827
|
+
request = self.generate_authenticated_request(options)
|
|
828
|
+
response = HTTPAdapter.transmit(request, adapter, connection)
|
|
829
|
+
status, headers, body = response
|
|
830
|
+
merged_body = StringIO.new
|
|
831
|
+
body.each do |chunk|
|
|
832
|
+
merged_body.write(chunk)
|
|
833
|
+
end
|
|
834
|
+
body = merged_body.string
|
|
835
|
+
if status.to_i == 401
|
|
836
|
+
# When accessing a protected resource, we only want to raise an
|
|
837
|
+
# error for 401 responses.
|
|
838
|
+
message = 'Authorization failed.'
|
|
839
|
+
if body.strip.length > 0
|
|
840
|
+
message += " Server message:\n#{body.strip}"
|
|
841
|
+
end
|
|
842
|
+
raise ::Signet::AuthorizationError.new(
|
|
843
|
+
message, :request => request, :response => response
|
|
844
|
+
)
|
|
845
|
+
else
|
|
846
|
+
return response
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
end
|