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.
@@ -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