signet 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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