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.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/README.md +118 -35
- data/Rakefile.rb +11 -0
- data/lib/oauthenticator.rb +5 -6
- data/lib/oauthenticator/config_methods.rb +62 -21
- data/lib/oauthenticator/faraday_signer.rb +66 -0
- data/lib/oauthenticator/parse_authorization.rb +81 -0
- data/lib/oauthenticator/{middleware.rb → rack_authenticator.rb} +37 -9
- data/lib/oauthenticator/signable_request.rb +340 -0
- data/lib/oauthenticator/signed_request.rb +123 -112
- data/lib/oauthenticator/version.rb +3 -1
- data/test/config_methods_test.rb +10 -7
- data/test/faraday_signer_test.rb +65 -0
- data/test/helper.rb +15 -3
- data/test/parse_authorization_test.rb +86 -0
- data/test/{oauthenticator_test.rb → rack_authenticator_test.rb} +264 -136
- data/test/signable_request_test.rb +653 -0
- data/test/signed_request_test.rb +12 -0
- data/test/test_config_methods.rb +67 -0
- metadata +26 -11
@@ -1,4 +1,5 @@
|
|
1
|
-
require '
|
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
|
-
|
30
|
-
|
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
|
-
:
|
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
|
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
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
129
|
-
|
130
|
-
errors['Authorization oauth_version'] << "must be 1.0; got: #{version}"
|
131
|
-
end
|
129
|
+
# she's filled with secrets
|
130
|
+
secrets = {}
|
132
131
|
|
133
|
-
|
134
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
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 ||=
|
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/
|
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::
|
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
|
data/test/config_methods_test.rb
CHANGED
@@ -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
|
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
|
20
|
+
it "complains when a method without a default is not implemented, using RackAuthenticator" do
|
21
21
|
exc = assert_raises(NotImplementedError) do
|
22
|
-
OAuthenticator::
|
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::
|
24
|
+
assert_match /passed to OAuthenticator::RackAuthenticator using the option :config_methods./, exc.message
|
25
25
|
end
|
26
|
-
it "complains
|
26
|
+
it "complains RackAuthenticator is not given config methods" do
|
27
27
|
assert_raises(ArgumentError) do
|
28
|
-
OAuthenticator::
|
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
|
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 '
|
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)
|