oauthenticator 0.1.4 → 1.0.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.
@@ -1,4 +1,5 @@
1
- require 'simple_oauth'
1
+ require 'oauthenticator/signable_request'
2
+ require 'oauthenticator/parse_authorization'
2
3
 
3
4
  module OAuthenticator
4
5
  # this class represents an OAuth signed request. its primary user-facing method is {#errors}, which returns
@@ -26,26 +27,27 @@ module OAuthenticator
26
27
  end
27
28
  end
28
29
 
29
- ATTRIBUTE_KEYS = %w(request_method url body media_type authorization).map(&:freeze).freeze
30
- OAUTH_ATTRIBUTE_KEYS = %w(consumer_key token timestamp nonce version signature_method signature).map(&:to_sym).freeze
30
+ # attributes of a SignedRequest
31
+ ATTRIBUTE_KEYS = %w(request_method uri body media_type authorization).map(&:freeze).freeze
32
+
33
+ # oauth attributes parsed from the request authorization
34
+ OAUTH_ATTRIBUTE_KEYS = (SignableRequest::PROTOCOL_PARAM_KEYS + %w(signature body_hash)).freeze
31
35
 
32
36
  # readers
33
37
  ATTRIBUTE_KEYS.each { |attribute_key| define_method(attribute_key) { @attributes[attribute_key] } }
34
38
 
35
39
  # readers for oauth header parameters
36
- OAUTH_ATTRIBUTE_KEYS.each { |key| define_method(key) { oauth_header_params[key] } }
40
+ OAUTH_ATTRIBUTE_KEYS.each { |key| define_method(key) { oauth_header_params["oauth_#{key}"] } }
37
41
 
38
42
  # question methods to indicate whether oauth header parameters were included with a non-blank value in
39
43
  # the Authorization header
40
44
  OAUTH_ATTRIBUTE_KEYS.each do |key|
41
45
  define_method("#{key}?") do
42
- value = oauth_header_params[key]
46
+ value = oauth_header_params["oauth_#{key}"]
43
47
  value.is_a?(String) ? !value.empty? : !!value
44
48
  end
45
49
  end
46
50
 
47
- VALID_SIGNATURE_METHODS = %w(HMAC-SHA1 RSA-SHA1 PLAINTEXT).map(&:freeze).freeze
48
-
49
51
  class << self
50
52
  # instantiates a `OAuthenticator::SignedRequest` (subclass thereof, more precisely) representing a
51
53
  # request given as a Rack::Request.
@@ -57,7 +59,7 @@ module OAuthenticator
57
59
  def from_rack_request(request)
58
60
  new({
59
61
  :request_method => request.request_method,
60
- :url => request.url,
62
+ :uri => request.url,
61
63
  :body => request.body,
62
64
  :media_type => request.media_type,
63
65
  :authorization => request.env['HTTP_AUTHORIZATION'],
@@ -87,105 +89,134 @@ module OAuthenticator
87
89
  #
88
90
  # @return [nil, Hash<String, Array<String>>] either nil or a hash of errors
89
91
  def errors
90
- @errors ||= begin
92
+ return @errors if instance_variables.any? { |ivar| ivar.to_s == '@errors' }
93
+ @errors = catch(:errors) do
91
94
  if authorization.nil?
92
- {'Authorization' => ["Authorization header is missing"]}
95
+ throw(:errors, {'Authorization' => ["Authorization header is missing"]})
93
96
  elsif authorization !~ /\S/
94
- {'Authorization' => ["Authorization header is blank"]}
95
- elsif authorization !~ /\Aoauth\s/i
96
- {'Authorization' => ["Authorization scheme is not OAuth; received Authorization: #{authorization}"]}
97
+ throw(:errors, {'Authorization' => ["Authorization header is blank"]})
98
+ end
99
+
100
+ begin
101
+ oauth_header_params
102
+ rescue OAuthenticator::Error => parse_exception
103
+ throw(:errors, parse_exception.errors)
104
+ end
105
+
106
+ errors = Hash.new { |h,k| h[k] = [] }
107
+
108
+ # timestamp
109
+ if !timestamp?
110
+ unless signature_method == 'PLAINTEXT'
111
+ errors['Authorization oauth_timestamp'] << "is missing"
112
+ end
113
+ elsif timestamp !~ /\A\s*\d+\s*\z/
114
+ errors['Authorization oauth_timestamp'] << "is not an integer - got: #{timestamp}"
97
115
  else
98
- to_rescue = SimpleOAuth.const_defined?(:ParseError) ? SimpleOAuth::ParseError : StandardError
99
- begin
100
- oauth_header_params
101
- rescue to_rescue
102
- parse_exception = $!
116
+ timestamp_i = timestamp.to_i
117
+ if timestamp_i < Time.now.to_i - timestamp_valid_past
118
+ errors['Authorization oauth_timestamp'] << "is too old: #{timestamp}"
119
+ elsif timestamp_i > Time.now.to_i + timestamp_valid_future
120
+ errors['Authorization oauth_timestamp'] << "is too far in the future: #{timestamp}"
103
121
  end
104
- if parse_exception
105
- if parse_exception.class.name == 'SimpleOAuth::ParseError'
106
- message = parse_exception.message
107
- else
108
- message = "Authorization header is not a properly-formed OAuth 1.0 header."
109
- end
110
- {'Authorization' => [message]}
111
- else
112
- errors = Hash.new { |h,k| h[k] = [] }
122
+ end
113
123
 
114
- # timestamp
115
- if !timestamp?
116
- errors['Authorization oauth_timestamp'] << "is missing"
117
- elsif timestamp !~ /\A\s*\d+\s*\z/
118
- errors['Authorization oauth_timestamp'] << "is not an integer - got: #{timestamp}"
119
- else
120
- timestamp_i = timestamp.to_i
121
- if timestamp_i < Time.now.to_i - timestamp_valid_past
122
- errors['Authorization oauth_timestamp'] << "is too old: #{timestamp}"
123
- elsif timestamp_i > Time.now.to_i + timestamp_valid_future
124
- errors['Authorization oauth_timestamp'] << "is too far in the future: #{timestamp}"
125
- end
126
- end
124
+ # oauth version
125
+ if version? && version != '1.0'
126
+ errors['Authorization oauth_version'] << "must be 1.0; got: #{version}"
127
+ end
127
128
 
128
- # oauth version
129
- if version? && version != '1.0'
130
- errors['Authorization oauth_version'] << "must be 1.0; got: #{version}"
131
- end
129
+ # she's filled with secrets
130
+ secrets = {}
132
131
 
133
- # she's filled with secrets
134
- secrets = {}
132
+ # consumer / client application
133
+ if !consumer_key?
134
+ errors['Authorization oauth_consumer_key'] << "is missing"
135
+ else
136
+ secrets[:consumer_secret] = consumer_secret
137
+ if !secrets[:consumer_secret]
138
+ errors['Authorization oauth_consumer_key'] << 'is invalid'
139
+ end
140
+ end
135
141
 
136
- # consumer / client application
137
- if !consumer_key?
138
- errors['Authorization oauth_consumer_key'] << "is missing"
139
- else
140
- secrets[:consumer_secret] = consumer_secret
141
- if !secrets[:consumer_secret]
142
- errors['Authorization oauth_consumer_key'] << 'is invalid'
143
- end
144
- end
142
+ # token
143
+ if token?
144
+ secrets[:token_secret] = token_secret
145
+ if !secrets[:token_secret]
146
+ errors['Authorization oauth_token'] << 'is invalid'
147
+ elsif !token_belongs_to_consumer?
148
+ errors['Authorization oauth_token'] << 'does not belong to the specified consumer'
149
+ end
150
+ end
145
151
 
146
- # access token
147
- if token?
148
- secrets[:token_secret] = access_token_secret
149
- if !secrets[:token_secret]
150
- errors['Authorization oauth_token'] << 'is invalid'
151
- elsif !access_token_belongs_to_consumer?
152
- errors['Authorization oauth_token'] << 'does not belong to the specified consumer'
153
- end
154
- end
152
+ # nonce
153
+ if !nonce?
154
+ unless signature_method == 'PLAINTEXT'
155
+ errors['Authorization oauth_nonce'] << "is missing"
156
+ end
157
+ elsif nonce_used?
158
+ errors['Authorization oauth_nonce'] << "has already been used"
159
+ end
155
160
 
156
- # nonce
157
- if !nonce?
158
- errors['Authorization oauth_nonce'] << "is missing"
159
- elsif nonce_used?
160
- errors['Authorization oauth_nonce'] << "has already been used"
161
- end
161
+ # signature method
162
+ if !signature_method?
163
+ errors['Authorization oauth_signature_method'] << "is missing"
164
+ elsif !allowed_signature_methods.any? { |sm| signature_method.downcase == sm.downcase }
165
+ errors['Authorization oauth_signature_method'] << "must be one of " +
166
+ "#{allowed_signature_methods.join(', ')}; got: #{signature_method}"
167
+ end
162
168
 
163
- # signature method
164
- if !signature_method?
165
- errors['Authorization oauth_signature_method'] << "is missing"
166
- elsif !allowed_signature_methods.any? { |sm| signature_method.downcase == sm.downcase }
167
- errors['Authorization oauth_signature_method'] << "must be one of " +
168
- "#{allowed_signature_methods.join(', ')}; got: #{signature_method}"
169
- end
169
+ # signature
170
+ if !signature?
171
+ errors['Authorization oauth_signature'] << "is missing"
172
+ end
170
173
 
171
- # signature
172
- if !signature?
173
- errors['Authorization oauth_signature'] << "is missing"
174
- end
174
+ signable_request = SignableRequest.new(@attributes.merge(secrets).merge('authorization' => oauth_header_params))
175
175
 
176
- if errors.any?
177
- errors
178
- else
179
- # proceed to check signature
180
- if !simple_oauth_header.valid?(secrets)
181
- {'Authorization oauth_signature' => ['is invalid']}
176
+ # body hash
177
+
178
+ # present?
179
+ if body_hash?
180
+ # allowed?
181
+ if !signable_request.form_encoded?
182
+ # applicable?
183
+ if SignableRequest::BODY_HASH_METHODS.key?(signature_method)
184
+ # correct?
185
+ if body_hash == signable_request.body_hash
186
+ # all good
182
187
  else
183
- use_nonce!
184
- nil
188
+ errors['Authorization oauth_body_hash'] << "is invalid"
185
189
  end
190
+ else
191
+ # received a body hash with plaintext. weird situation - we will ignore it; signature will not
192
+ # be verified but it will be a part of the signature.
193
+ end
194
+ else
195
+ errors['Authorization oauth_body_hash'] << "must not be included with form-encoded requests"
196
+ end
197
+ else
198
+ # allowed?
199
+ if !signable_request.form_encoded?
200
+ # required?
201
+ if body_hash_required?
202
+ errors['Authorization oauth_body_hash'] << "is required (on non-form-encoded requests)"
203
+ else
204
+ # okay - not supported by client, but allowed
186
205
  end
206
+ else
207
+ # all good
187
208
  end
188
209
  end
210
+
211
+ throw(:errors, errors) if errors.any?
212
+
213
+ # proceed to check signature
214
+ unless self.signature == signable_request.signature
215
+ throw(:errors, {'Authorization oauth_signature' => ['is invalid']})
216
+ end
217
+
218
+ use_nonce!
219
+ nil
189
220
  end
190
221
  end
191
222
 
@@ -196,36 +227,16 @@ module OAuthenticator
196
227
 
197
228
  # hash of header params. keys should be a subset of OAUTH_ATTRIBUTE_KEYS.
198
229
  def oauth_header_params
199
- @oauth_header_params ||= SimpleOAuth::Header.parse(authorization)
200
- end
201
-
202
- # reads the request body, be it String or IO
203
- def read_body
204
- if body.is_a?(String)
205
- body
206
- elsif body.respond_to?(:read) && body.respond_to?(:rewind)
207
- body.rewind
208
- body.read.tap do
209
- body.rewind
210
- end
211
- else
212
- raise NotImplementedError, "body = #{body.inspect}"
213
- end
214
- end
215
-
216
- # SimpleOAuth::Header for this request
217
- def simple_oauth_header
218
- params = media_type == "application/x-www-form-urlencoded" ? CGI.parse(read_body).map{|k,vs| vs.map{|v| [k,v] } }.inject([], &:+) : nil
219
- simple_oauth_header = SimpleOAuth::Header.new(request_method, url, params, authorization)
230
+ @oauth_header_params ||= OAuthenticator.parse_authorization(authorization)
220
231
  end
221
232
 
222
233
  # raise a nice error message for a method that needs to be implemented on a module of config methods
223
234
  def config_method_not_implemented
224
235
  caller_name = caller[0].match(%r(in `(.*?)'))[1]
225
- using_middleware = caller.any? { |l| l =~ %r(oauthenticator/middleware.rb:.*`call') }
236
+ using_middleware = caller.any? { |l| l =~ %r(oauthenticator/rack_authenticator.rb:.*`call') }
226
237
  message = "method \##{caller_name} must be implemented on a module of oauth config methods, which is " + begin
227
238
  if using_middleware
228
- "passed to OAuthenticator::Middleware using the option :config_methods."
239
+ "passed to OAuthenticator::RackAuthenticator using the option :config_methods."
229
240
  else
230
241
  "included in a subclass of OAuthenticator::SignedRequest, typically by passing it to OAuthenticator::SignedRequest.including_config(your_module)."
231
242
  end
@@ -1,3 +1,5 @@
1
+ # OAuthenticator
1
2
  module OAuthenticator
2
- VERSION = "0.1.4"
3
+ # OAuthenticator::VERSION
4
+ VERSION = "1.0.0"
3
5
  end
@@ -3,7 +3,7 @@ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.cal
3
3
  require 'helper'
4
4
 
5
5
  describe OAuthenticator::SignedRequest do
6
- %w(timestamp_valid_period consumer_secret access_token_secret nonce_used? use_nonce! access_token_belongs_to_consumer?).each do |method_without_default|
6
+ %w(timestamp_valid_period consumer_secret token_secret nonce_used? use_nonce! token_belongs_to_consumer?).each do |method_without_default|
7
7
  it "complains when #{method_without_default} is not implemented" do
8
8
  exc = assert_raises(NotImplementedError) do
9
9
  OAuthenticator::SignedRequest.new({}).public_send(method_without_default)
@@ -17,15 +17,15 @@ describe OAuthenticator::SignedRequest do
17
17
  assert called
18
18
  end
19
19
  end
20
- it "complains when a method without a default is not implemented, using middleware" do
20
+ it "complains when a method without a default is not implemented, using RackAuthenticator" do
21
21
  exc = assert_raises(NotImplementedError) do
22
- OAuthenticator::Middleware.new(proc {}, {:config_methods => Module.new}).call({'HTTP_AUTHORIZATION' => %q(OAuth oauth_timestamp="1")})
22
+ OAuthenticator::RackAuthenticator.new(proc {}, {:config_methods => Module.new}).call({'HTTP_AUTHORIZATION' => %q(OAuth oauth_timestamp="1")})
23
23
  end
24
- assert_match /passed to OAuthenticator::Middleware using the option :config_methods./, exc.message
24
+ assert_match /passed to OAuthenticator::RackAuthenticator using the option :config_methods./, exc.message
25
25
  end
26
- it "complains middleware is not given config methods" do
26
+ it "complains RackAuthenticator is not given config methods" do
27
27
  assert_raises(ArgumentError) do
28
- OAuthenticator::Middleware.new(proc {})
28
+ OAuthenticator::RackAuthenticator.new(proc {})
29
29
  end
30
30
  end
31
31
  it 'uses timestamp_valid_period if that is implemented but timestamp_valid_past or timestamp_valid_future is not' do
@@ -36,6 +36,9 @@ describe OAuthenticator::SignedRequest do
36
36
  assert_equal 2, called
37
37
  end
38
38
  it 'uses the default value for allowed signature methods' do
39
- assert_equal OAuthenticator::SignedRequest::VALID_SIGNATURE_METHODS, OAuthenticator::SignedRequest.new({}).allowed_signature_methods
39
+ assert_equal %w(RSA-SHA1 HMAC-SHA1 PLAINTEXT), OAuthenticator::SignedRequest.new({}).allowed_signature_methods
40
+ end
41
+ it 'uses default value for body_hash_required?' do
42
+ assert_equal false, OAuthenticator::SignedRequest.new({}).body_hash_required?
40
43
  end
41
44
  end
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
3
+ require 'helper'
4
+
5
+ # not going to test a ton here, since the Faraday middleware mostly just calls to SignableRequest which is
6
+ # rather well-tested
7
+ describe OAuthenticator::FaradaySigner do
8
+ def assert_response(expected_status, expected_body, faraday_response)
9
+ assert_equal expected_status.to_i, faraday_response.status.to_i, "Expected status to be #{expected_status.inspect}" +
10
+ "; got #{faraday_response.status.inspect}. body was: #{faraday_response.body}"
11
+ assert expected_body === faraday_response.body, "Expected match for #{expected_body}; got #{faraday_response.body}"
12
+ end
13
+
14
+ it 'succeeds' do
15
+ signing_options = {
16
+ :signature_method => 'PLAINTEXT',
17
+ :consumer_key => consumer_key,
18
+ :consumer_secret => consumer_secret,
19
+ :token => token,
20
+ :token_secret => token_secret,
21
+ }
22
+
23
+ connection = Faraday.new(:url => 'http://example.com') do |faraday|
24
+ faraday.request :oauthenticator_signer, signing_options
25
+ faraday.adapter :rack, oapp
26
+ end
27
+ response = connection.get '/'
28
+ assert_response 200, '☺', response
29
+ end
30
+
31
+ it 'succeeds with form-encoded with HMAC' do
32
+ signing_options = {
33
+ :signature_method => 'HMAC-SHA1',
34
+ :consumer_key => consumer_key,
35
+ :consumer_secret => consumer_secret,
36
+ :token => token,
37
+ :token_secret => token_secret,
38
+ }
39
+
40
+ connection = Faraday.new(:url => 'http://example.com') do |faraday|
41
+ faraday.request :url_encoded
42
+ faraday.request :oauthenticator_signer, signing_options
43
+ faraday.adapter :rack, oapp
44
+ end
45
+ response = connection.put('/', :foo => {:bar => :baz})
46
+ assert_response 200, '☺', response
47
+ end
48
+
49
+ it 'is unauthorized' do
50
+ signing_options = {
51
+ :signature_method => 'PLAINTEXT',
52
+ :consumer_key => consumer_key,
53
+ :consumer_secret => 'nope',
54
+ :token => token,
55
+ :token_secret => 'definitelynot',
56
+ }
57
+
58
+ connection = Faraday.new(:url => 'http://example.com') do |faraday|
59
+ faraday.request :oauthenticator_signer, signing_options
60
+ faraday.adapter :rack, oapp
61
+ end
62
+ response = connection.get '/'
63
+ assert_response 401, /Authorization oauth_signature.*is invalid/m, response
64
+ end
65
+ end
data/test/helper.rb CHANGED
@@ -2,11 +2,10 @@ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.cal
2
2
 
3
3
  require 'simplecov'
4
4
 
5
- require 'debugger'
6
- Debugger.start
5
+ require 'byebug'
7
6
 
8
7
  # NO EXPECTATIONS
9
- ENV["MT_NO_EXPECTATIONS"]
8
+ ENV["MT_NO_EXPECTATIONS"] = ''
10
9
 
11
10
  require 'minitest/autorun'
12
11
  require 'minitest/reporters'
@@ -16,3 +15,16 @@ require 'rack/test'
16
15
  require 'timecop'
17
16
 
18
17
  require 'oauthenticator'
18
+
19
+ require 'test_config_methods'
20
+
21
+ class OAuthenticatorConfigSpec < Minitest::Spec
22
+ after do
23
+ Timecop.return
24
+ end
25
+
26
+ include TestHelperMethods
27
+ end
28
+
29
+ # register this to be the base class for specs instead of Minitest::Spec
30
+ Minitest::Spec.register_spec_type(//, OAuthenticatorConfigSpec)