oauthenticator 0.1.4 → 1.0.0

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