signet 0.1.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,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