signet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
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 'signet/version'
16
+
17
+ module Signet #:nodoc:
18
+ end
@@ -0,0 +1,38 @@
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
+ module Signet #:nodoc:
16
+ class AuthorizationError < StandardError
17
+ ##
18
+ # Creates a new authentication error.
19
+ #
20
+ # @param [String] message
21
+ # A message describing the error.
22
+ # @param [Array] request
23
+ # A tuple of method, uri, headers, and body. Optional.
24
+ # @param [Array] response
25
+ # A tuple of status, headers, and body. Optional.
26
+ def initialize(message, request=nil, response=nil)
27
+ super(message)
28
+ @request = request
29
+ @response = response
30
+ end
31
+
32
+ ##
33
+ # The HTTP response that triggered this authentication error.
34
+ #
35
+ # @return [Array] A tuple of status, headers, and body.
36
+ attr_reader :response
37
+ end
38
+ end
@@ -0,0 +1,456 @@
1
+ require 'addressable/uri'
2
+
3
+ begin
4
+ require 'securerandom'
5
+ rescue LoadError
6
+ require 'compat/securerandom'
7
+ end
8
+
9
+ module Signet #:nodoc:
10
+ module OAuth1
11
+ OUT_OF_BAND = 'oob'
12
+
13
+ ##
14
+ # Converts a value to a percent-encoded <code>String</code> according to
15
+ # the rules given in RFC 5849. All non-unreserved characters are
16
+ # percent-encoded.
17
+ #
18
+ # @param [Symbol, #to_str] value The value to be encoded.
19
+ #
20
+ # @return [String] The percent-encoded value.
21
+ def self.encode(value)
22
+ value = value.to_s if value.kind_of?(Symbol)
23
+ return Addressable::URI.encode_component(
24
+ value,
25
+ Addressable::URI::CharacterClasses::UNRESERVED
26
+ )
27
+ end
28
+
29
+ ##
30
+ # Converts a percent-encoded String to an unencoded value.
31
+ #
32
+ # @param [#to_str] value
33
+ # The percent-encoded <code>String</code> to be unencoded.
34
+ #
35
+ # @return [String] The unencoded value.
36
+ def self.unencode(value)
37
+ return Addressable::URI.unencode_component(value)
38
+ end
39
+
40
+ ##
41
+ # Returns a timestamp suitable for use as an <code>'oauth_timestamp'</code>
42
+ # value.
43
+ #
44
+ # @return [String] The current timestamp.
45
+ def self.generate_timestamp()
46
+ return Time.now.to_i.to_s
47
+ end
48
+
49
+ ##
50
+ # Returns a nonce suitable for use as an <code>'oauth_nonce'</code>
51
+ # value.
52
+ #
53
+ # @return [String] A random nonce.
54
+ def self.generate_nonce()
55
+ return SecureRandom.random_bytes(16).unpack('H*').join('')
56
+ end
57
+
58
+ ##
59
+ # Processes an options <code>Hash</code> to find a credential key value.
60
+ # Allows for greater flexibility in configuration.
61
+ #
62
+ # @param [Symbol] credential_type
63
+ # One of <code>:client</code>, <code>:temporary</code>,
64
+ # <code>:token</code>, <code>:consumer</code>, <code>:request</code>,
65
+ # or <code>:access</code>.
66
+ #
67
+ # @return [String] The credential key value.
68
+ def self.extract_credential_key_option(credential_type, options)
69
+ credential_key_symbol =
70
+ ("#{credential_type}_credential_key").to_sym
71
+ credential_symbol =
72
+ ("#{credential_type}_credential").to_sym
73
+ if options[credential_key_symbol]
74
+ credential_key = options[credential_key_symbol]
75
+ elsif options[credential_symbol]
76
+ require 'signet/oauth_1/credential'
77
+ if !options[credential_symbol].respond_to?(:key)
78
+ raise TypeError,
79
+ "Expected Signet::OAuth1::Credential, " +
80
+ "got #{options[credential_symbol].class}."
81
+ end
82
+ credential_key = options[credential_symbol].key
83
+ elsif options[:client]
84
+ require 'signet/oauth_1/client'
85
+ if !options[:client].kind_of?(::Signet::OAuth1::Client)
86
+ raise TypeError,
87
+ "Expected Signet::OAuth1::Client, got #{options[:client].class}."
88
+ end
89
+ credential_key = options[:client].send(credential_key_symbol)
90
+ else
91
+ credential_key = nil
92
+ end
93
+ if credential_key != nil && !credential_key.kind_of?(String)
94
+ raise TypeError,
95
+ "Expected String, got #{credential_key.class}."
96
+ end
97
+ return credential_key
98
+ end
99
+
100
+ ##
101
+ # Processes an options <code>Hash</code> to find a credential secret value.
102
+ # Allows for greater flexibility in configuration.
103
+ #
104
+ # @param [Symbol] credential_type
105
+ # One of <code>:client</code>, <code>:temporary</code>,
106
+ # <code>:token</code>, <code>:consumer</code>, <code>:request</code>,
107
+ # or <code>:access</code>.
108
+ #
109
+ # @return [String] The credential secret value.
110
+ def self.extract_credential_secret_option(credential_type, options)
111
+ credential_secret_symbol =
112
+ ("#{credential_type}_credential_secret").to_sym
113
+ credential_symbol =
114
+ ("#{credential_type}_credential").to_sym
115
+ if options[credential_secret_symbol]
116
+ credential_secret = options[credential_secret_symbol]
117
+ elsif options[credential_symbol]
118
+ require 'signet/oauth_1/credential'
119
+ if !options[credential_symbol].respond_to?(:secret)
120
+ raise TypeError,
121
+ "Expected Signet::OAuth1::Credential, " +
122
+ "got #{options[credential_symbol].class}."
123
+ end
124
+ credential_secret = options[credential_symbol].secret
125
+ elsif options[:client]
126
+ require 'signet/oauth_1/client'
127
+ if !options[:client].kind_of?(::Signet::OAuth1::Client)
128
+ raise TypeError,
129
+ "Expected Signet::OAuth1::Client, got #{options[:client].class}."
130
+ end
131
+ credential_secret = options[:client].send(credential_secret_symbol)
132
+ else
133
+ credential_secret = nil
134
+ end
135
+ if credential_secret != nil && !credential_secret.kind_of?(String)
136
+ raise TypeError,
137
+ "Expected String, got #{credential_secret.class}."
138
+ end
139
+ return credential_secret
140
+ end
141
+
142
+ ##
143
+ # Normalizes a set of OAuth parameters according to the algorithm given
144
+ # in RFC 5849. Sorts key/value pairs lexically by byte order, first by
145
+ # key, then by value, joins key/value pairs with the '=' character, then
146
+ # joins the entire parameter list with '&' characters.
147
+ #
148
+ # @param [Enumerable] parameters The OAuth parameter list.
149
+ #
150
+ # @return [String] The normalized parameter list.
151
+ def self.normalize_parameters(parameters)
152
+ if !parameters.kind_of?(Enumerable)
153
+ raise TypeError, "Expected Enumerable, got #{parameters.class}."
154
+ end
155
+ parameter_list = parameters.map do |k, v|
156
+ next if k == "oauth_signature"
157
+ # This is probably the wrong place to try to exclude the realm
158
+ "#{self.encode(k)}=#{self.encode(v)}"
159
+ end
160
+ return parameter_list.compact.sort.join("&")
161
+ end
162
+
163
+ ##
164
+ # Generates a signature base string according to the algorithm given in
165
+ # RFC 5849. Joins the method, URI, and normalized parameter string with
166
+ # '&' characters.
167
+ #
168
+ # @param [String] method The HTTP method.
169
+ # @param [Addressable::URI, String, #to_str] The URI.
170
+ # @param [Enumerable] parameters The OAuth parameter list.
171
+ #
172
+ # @return [String] The signature base string.
173
+ def self.generate_base_string(method, uri, parameters)
174
+ if !parameters.kind_of?(Enumerable)
175
+ raise TypeError, "Expected Enumerable, got #{parameters.class}."
176
+ end
177
+ method = method.to_s.upcase
178
+ uri = Addressable::URI.parse(uri).normalize
179
+ uri_parameters = uri.query_values.to_a
180
+ uri = uri.omit(:query, :fragment).to_s
181
+ merged_parameters =
182
+ uri_parameters.concat(parameters.map { |k, v| [k, v] })
183
+ parameter_string = self.normalize_parameters(merged_parameters)
184
+ return [
185
+ self.encode(method),
186
+ self.encode(uri),
187
+ self.encode(parameter_string)
188
+ ].join('&')
189
+ end
190
+
191
+ ##
192
+ # Generates an <code>Authorization</code> header from a parameter list
193
+ # according to the rules given in RFC 5849.
194
+ #
195
+ # @param [Enumerable] parameters The OAuth parameter list.
196
+ # @param [String] realm
197
+ # The <code>Authorization</code> realm. See RFC 2617.
198
+ #
199
+ # @return [String] The <code>Authorization</code> header.
200
+ def self.generate_authorization_header(parameters, realm=nil)
201
+ if !parameters.kind_of?(Enumerable) || parameters.kind_of?(String)
202
+ raise TypeError, "Expected Enumerable, got #{parameters.class}."
203
+ end
204
+ parameter_list = parameters.map do |k, v|
205
+ if k == 'realm'
206
+ raise ArgumentError,
207
+ 'The "realm" parameter must be specified as a separate argument.'
208
+ end
209
+ "#{self.encode(k)}=\"#{self.encode(v)}\""
210
+ end
211
+ if realm
212
+ realm = realm.gsub('"', '\"')
213
+ parameter_list.unshift("realm=\"#{realm}\"")
214
+ end
215
+ return 'OAuth ' + parameter_list.join(", ")
216
+ end
217
+
218
+ ##
219
+ # Parses an <code>Authorization</code> header into its component
220
+ # parameters. Parameter keys and values are decoded according to the
221
+ # rules given in RFC 5849.
222
+ def self.parse_authorization_header(header)
223
+ if !header.kind_of?(String)
224
+ raise TypeError, "Expected String, got #{header.class}."
225
+ end
226
+ unless header[0...6] == 'OAuth '
227
+ raise ArgumentError,
228
+ 'Parsing non-OAuth Authorization headers is out of scope.'
229
+ end
230
+ header = header.gsub(/^OAuth /, '')
231
+ return header.split(/,\s*/).inject([]) do |accu, pair|
232
+ k = pair[/^(.*?)=\"[^\"]*\"/, 1]
233
+ v = pair[/^.*?=\"([^\"]*)\"/, 1]
234
+ if k != 'realm'
235
+ k = self.unencode(k)
236
+ v = self.unencode(v)
237
+ else
238
+ v = v.gsub('\"', '"')
239
+ end
240
+ accu << [k, v]
241
+ accu
242
+ end
243
+ end
244
+
245
+ ##
246
+ # Parses an <code>application/x-www-form-urlencoded</code> HTTP response
247
+ # body into an OAuth key/secret pair.
248
+ #
249
+ # @param [String] body The response body.
250
+ #
251
+ # @return [Signet::OAuth1::Credential] The OAuth credentials.
252
+ def self.parse_form_encoded_credentials(body)
253
+ if !body.kind_of?(String)
254
+ raise TypeError, "Expected String, got #{body.class}."
255
+ end
256
+ return Signet::OAuth1::Credential.new(
257
+ Addressable::URI.form_unencode(body)
258
+ )
259
+ end
260
+
261
+ ##
262
+ # Generates an OAuth signature using the signature method indicated in the
263
+ # parameter list. Unsupported signature methods will result in a
264
+ # <code>NotImplementedError</code> exception being raised.
265
+ #
266
+ # @param [String] method The HTTP method.
267
+ # @param [Addressable::URI, String, #to_str] The URI.
268
+ # @param [Enumerable] parameters The OAuth parameter list.
269
+ # @param [String] client_credential_secret The client credential secret.
270
+ # @param [String] token_credential_secret
271
+ # The token credential secret. Omitted when unavailable.
272
+ #
273
+ # @return [String] The signature.
274
+ def self.sign_parameters(method, uri, parameters,
275
+ client_credential_secret, token_credential_secret=nil)
276
+ # Technically, the token_credential_secret parameter here may actually
277
+ # be a temporary credential secret when obtaining a token credential
278
+ # for the first time
279
+ base_string = self.generate_base_string(method, uri, parameters)
280
+ signature_method = Hash[parameters]['oauth_signature_method']
281
+ case signature_method
282
+ when 'HMAC-SHA1'
283
+ require 'signet/oauth_1/signature_methods/hmac_sha1'
284
+ return Signet::OAuth1::HMACSHA1.generate_signature(
285
+ base_string, client_credential_secret, token_credential_secret
286
+ )
287
+ else
288
+ raise NotImplementedError,
289
+ "Unsupported signature method: #{signature_method}"
290
+ end
291
+ end
292
+
293
+ ##
294
+ # Generates an OAuth parameter list to be used when obtaining a set of
295
+ # temporary credentials.
296
+ #
297
+ # @param [Hash] options
298
+ # The configuration parameters for the request.
299
+ # - <code>:client_credential_key</code> —
300
+ # The client credential key.
301
+ # - <code>:callback</code> —
302
+ # The OAuth callback. Defaults to {Signet::OAuth1::OUT_OF_BAND}.
303
+ # - <code>:signature_method</code> —
304
+ # The signature method. Defaults to <code>'HMAC-SHA1'</code>.
305
+ # - <code>:additional_parameters</code> —
306
+ # Non-standard additional parameters.
307
+ #
308
+ # @return [Array]
309
+ # The parameter list as an <code>Array</code> of key/value pairs.
310
+ def self.unsigned_temporary_credential_parameters(options={})
311
+ options = {
312
+ :callback => ::Signet::OAuth1::OUT_OF_BAND,
313
+ :signature_method => 'HMAC-SHA1',
314
+ :additional_parameters => []
315
+ }.merge(options)
316
+ client_credential_key =
317
+ self.extract_credential_key_option(:client, options)
318
+ if client_credential_key == nil
319
+ raise ArgumentError, "Missing :client_credential_key parameter."
320
+ end
321
+ parameters = [
322
+ ["oauth_consumer_key", client_credential_key],
323
+ ["oauth_signature_method", options[:signature_method]],
324
+ ["oauth_timestamp", self.generate_timestamp()],
325
+ ["oauth_nonce", self.generate_nonce()],
326
+ ["oauth_version", "1.0"],
327
+ ["oauth_callback", options[:callback]]
328
+ ]
329
+ # Works for any Enumerable
330
+ options[:additional_parameters].each do |key, value|
331
+ parameters << [key, value]
332
+ end
333
+ return parameters
334
+ end
335
+
336
+ ##
337
+ # Appends the optional 'oauth_token' and 'oauth_callback' parameters to
338
+ # the base authorization URI.
339
+ #
340
+ # @param [Addressable::URI, String, #to_str] authorization_uri
341
+ # The base authorization URI.
342
+ #
343
+ # @return [String] The authorization URI to redirect the user to.
344
+ def self.generate_authorization_uri(authorization_uri, options={})
345
+ options = {
346
+ :callback => nil,
347
+ :additional_parameters => {}
348
+ }.merge(options)
349
+ temporary_credential_key =
350
+ self.extract_credential_key_option(:temporary, options)
351
+ parsed_uri = Addressable::URI.parse(authorization_uri)
352
+ query_values = parsed_uri.query_values || {}
353
+ if options[:additional_parameters]
354
+ query_values =
355
+ query_values.merge(Hash[options[:additional_parameters]])
356
+ end
357
+ if temporary_credential_key
358
+ query_values['oauth_token'] = temporary_credential_key
359
+ end
360
+ if options[:callback]
361
+ query_values['oauth_callback'] = options[:callback]
362
+ end
363
+ parsed_uri.query_values = query_values
364
+ return parsed_uri.normalize.to_s
365
+ end
366
+
367
+ ##
368
+ # Generates an OAuth parameter list to be used when obtaining a set of
369
+ # token credentials.
370
+ #
371
+ # @param [Hash] options
372
+ # The configuration parameters for the request.
373
+ # - <code>:client_credential_key</code> —
374
+ # The client credential key.
375
+ # - <code>:temporary_credential_key</code> —
376
+ # The temporary credential key.
377
+ # - <code>:verifier</code> —
378
+ # The OAuth verifier.
379
+ # - <code>:signature_method</code> —
380
+ # The signature method. Defaults to <code>'HMAC-SHA1'</code>.
381
+ #
382
+ # @return [Array]
383
+ # The parameter list as an <code>Array</code> of key/value pairs.
384
+ def self.unsigned_token_credential_parameters(options={})
385
+ options = {
386
+ :signature_method => 'HMAC-SHA1',
387
+ :verifier => nil
388
+ }.merge(options)
389
+ client_credential_key =
390
+ self.extract_credential_key_option(:client, options)
391
+ temporary_credential_key =
392
+ self.extract_credential_key_option(:temporary, options)
393
+ if client_credential_key == nil
394
+ raise ArgumentError, "Missing :client_credential_key parameter."
395
+ end
396
+ if temporary_credential_key == nil
397
+ raise ArgumentError, "Missing :temporary_credential_key parameter."
398
+ end
399
+ if options[:verifier] == nil
400
+ raise ArgumentError, "Missing :verifier parameter."
401
+ end
402
+ parameters = [
403
+ ["oauth_consumer_key", client_credential_key],
404
+ ["oauth_token", temporary_credential_key],
405
+ ["oauth_signature_method", options[:signature_method]],
406
+ ["oauth_timestamp", self.generate_timestamp()],
407
+ ["oauth_nonce", self.generate_nonce()],
408
+ ["oauth_verifier", options[:verifier]],
409
+ ["oauth_version", "1.0"]
410
+ ]
411
+ # No additional parameters allowed here
412
+ return parameters
413
+ end
414
+
415
+ ##
416
+ # Generates an OAuth parameter list to be used when requesting a
417
+ # protected resource.
418
+ #
419
+ # @param [Hash] options
420
+ # The configuration parameters for the request.
421
+ # - <code>:client_credential_key</code> —
422
+ # The client credential key.
423
+ # - <code>:token_credential_key</code> —
424
+ # The token credential key.
425
+ # - <code>:signature_method</code> —
426
+ # The signature method. Defaults to <code>'HMAC-SHA1'</code>.
427
+ #
428
+ # @return [Array]
429
+ # The parameter list as an <code>Array</code> of key/value pairs.
430
+ def self.unsigned_resource_parameters(options={})
431
+ options = {
432
+ :signature_method => 'HMAC-SHA1'
433
+ }.merge(options)
434
+ client_credential_key =
435
+ self.extract_credential_key_option(:client, options)
436
+ token_credential_key =
437
+ self.extract_credential_key_option(:token, options)
438
+ if client_credential_key == nil
439
+ raise ArgumentError, "Missing :client_credential_key parameter."
440
+ end
441
+ if token_credential_key == nil
442
+ raise ArgumentError, "Missing :token_credential_key parameter."
443
+ end
444
+ parameters = [
445
+ ["oauth_consumer_key", client_credential_key],
446
+ ["oauth_token", token_credential_key],
447
+ ["oauth_signature_method", options[:signature_method]],
448
+ ["oauth_timestamp", self.generate_timestamp()],
449
+ ["oauth_nonce", self.generate_nonce()],
450
+ ["oauth_version", "1.0"]
451
+ ]
452
+ # No additional parameters allowed here
453
+ return parameters
454
+ end
455
+ end
456
+ end