api-auth 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -2
  3. data/.travis.yml +4 -0
  4. data/Appraisals +6 -0
  5. data/CHANGELOG.md +36 -0
  6. data/Gemfile.lock +77 -44
  7. data/README.md +15 -8
  8. data/VERSION +1 -1
  9. data/api_auth.gemspec +4 -4
  10. data/gemfiles/rails_23.gemfile +1 -1
  11. data/gemfiles/rails_23.gemfile.lock +19 -11
  12. data/gemfiles/rails_30.gemfile +1 -1
  13. data/gemfiles/rails_30.gemfile.lock +19 -11
  14. data/gemfiles/rails_31.gemfile +1 -1
  15. data/gemfiles/rails_31.gemfile.lock +19 -11
  16. data/gemfiles/rails_32.gemfile +1 -1
  17. data/gemfiles/rails_32.gemfile.lock +19 -11
  18. data/gemfiles/rails_4.gemfile +1 -1
  19. data/gemfiles/rails_4.gemfile.lock +19 -11
  20. data/gemfiles/rails_41.gemfile +1 -1
  21. data/gemfiles/rails_41.gemfile.lock +19 -11
  22. data/gemfiles/rails_42.gemfile +9 -0
  23. data/gemfiles/rails_42.gemfile.lock +115 -0
  24. data/lib/api_auth/base.rb +37 -23
  25. data/lib/api_auth/headers.rb +23 -3
  26. data/lib/api_auth/request_drivers/action_controller.rb +4 -0
  27. data/lib/api_auth/request_drivers/curb.rb +4 -0
  28. data/lib/api_auth/request_drivers/faraday.rb +4 -0
  29. data/lib/api_auth/request_drivers/httpi.rb +5 -1
  30. data/lib/api_auth/request_drivers/net_http.rb +4 -0
  31. data/lib/api_auth/request_drivers/rack.rb +5 -1
  32. data/lib/api_auth/request_drivers/rest_client.rb +4 -0
  33. data/spec/api_auth_spec.rb +112 -628
  34. data/spec/headers_spec.rb +132 -289
  35. data/spec/helpers_spec.rb +2 -2
  36. data/spec/railtie_spec.rb +13 -8
  37. data/spec/request_drivers/action_controller_spec.rb +218 -0
  38. data/spec/request_drivers/action_dispatch_spec.rb +219 -0
  39. data/spec/request_drivers/curb_spec.rb +89 -0
  40. data/spec/request_drivers/faraday_spec.rb +243 -0
  41. data/spec/request_drivers/httpi_spec.rb +147 -0
  42. data/spec/request_drivers/net_http_spec.rb +185 -0
  43. data/spec/request_drivers/rack_spec.rb +288 -0
  44. data/spec/request_drivers/rest_client_spec.rb +311 -0
  45. metadata +44 -19
  46. data/spec/application_helper.rb +0 -2
  47. data/spec/test_helper.rb +0 -2
@@ -52,10 +52,28 @@ module ApiAuth
52
52
  end
53
53
 
54
54
  # Returns the canonical string computed from the request's headers
55
- def canonical_string
55
+ def canonical_string_without_http_method
56
56
  [ @request.content_type,
57
57
  @request.content_md5,
58
- parse_uri(@request.request_uri),
58
+ parse_uri(@request.request_uri),
59
+ @request.timestamp
60
+ ].join(",")
61
+ end
62
+
63
+ # temp backwards compatibility
64
+ alias_method :canonical_string, :canonical_string_without_http_method
65
+
66
+ def canonical_string_with_http_method(override_method = nil)
67
+ request_method = override_method || @request.http_method
68
+
69
+ if request_method.nil?
70
+ raise ArgumentError, "unable to determine the http method from the request, please supply an override"
71
+ end
72
+
73
+ [ request_method.upcase,
74
+ @request.content_type,
75
+ @request.content_md5,
76
+ parse_uri(@request.request_uri),
59
77
  @request.timestamp
60
78
  ].join(",")
61
79
  end
@@ -92,8 +110,10 @@ module ApiAuth
92
110
 
93
111
  private
94
112
 
113
+ URI_WITHOUT_HOST_REGEXP = %r{https?://[^,?/]*}
114
+
95
115
  def parse_uri(uri)
96
- uri_without_host = uri.gsub(/https?:\/\/[^(,|\?|\/)]*/, '')
116
+ uri_without_host = uri.gsub(URI_WITHOUT_HOST_REGEXP, '')
97
117
  return '/' if uri_without_host.empty?
98
118
  uri_without_host
99
119
  end
@@ -41,6 +41,10 @@ module ApiAuth
41
41
  capitalize_keys @request.env
42
42
  end
43
43
 
44
+ def http_method
45
+ @request.request_method.to_s.upcase
46
+ end
47
+
44
48
  def content_type
45
49
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
46
50
  value.nil? ? "" : value
@@ -30,6 +30,10 @@ module ApiAuth
30
30
  capitalize_keys @request.headers
31
31
  end
32
32
 
33
+ def http_method
34
+ nil # not possible to get the method at this layer
35
+ end
36
+
33
37
  def content_type
34
38
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
35
39
  value.nil? ? "" : value
@@ -45,6 +45,10 @@ module ApiAuth
45
45
  capitalize_keys @request.headers
46
46
  end
47
47
 
48
+ def http_method
49
+ @request.method.to_s.upcase
50
+ end
51
+
48
52
  def content_type
49
53
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
50
54
  value.nil? ? "" : value
@@ -40,6 +40,10 @@ module ApiAuth
40
40
  capitalize_keys @request.headers
41
41
  end
42
42
 
43
+ def http_method
44
+ nil # not possible to get the method at this layer
45
+ end
46
+
43
47
  def content_type
44
48
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
45
49
  value.nil? ? "" : value
@@ -77,4 +81,4 @@ module ApiAuth
77
81
 
78
82
  end
79
83
 
80
- end
84
+ end
@@ -47,6 +47,10 @@ module ApiAuth
47
47
  @request
48
48
  end
49
49
 
50
+ def http_method
51
+ @request.method.upcase
52
+ end
53
+
50
54
  def content_type
51
55
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
52
56
  value.nil? ? "" : value
@@ -46,6 +46,10 @@ module ApiAuth
46
46
  capitalize_keys @request.env
47
47
  end
48
48
 
49
+ def http_method
50
+ @request.request_method.upcase
51
+ end
52
+
49
53
  def content_type
50
54
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
51
55
  value.nil? ? "" : value
@@ -57,7 +61,7 @@ module ApiAuth
57
61
  end
58
62
 
59
63
  def request_uri
60
- @request.url
64
+ @request.fullpath
61
65
  end
62
66
 
63
67
  def set_date
@@ -50,6 +50,10 @@ module ApiAuth
50
50
  capitalize_keys @request.processed_headers
51
51
  end
52
52
 
53
+ def http_method
54
+ @request.method.to_s.upcase
55
+ end
56
+
53
57
  def content_type
54
58
  value = find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE))
55
59
  value.nil? ? "": value
@@ -10,678 +10,162 @@ describe "ApiAuth" do
10
10
  end
11
11
 
12
12
  it "should generate secret keys that are 88 characters" do
13
- ApiAuth.generate_secret_key.size.should be(88)
13
+ expect(ApiAuth.generate_secret_key.size).to be(88)
14
14
  end
15
15
 
16
16
  it "should generate keys that have a Hamming Distance of at least 65" do
17
17
  key1 = ApiAuth.generate_secret_key
18
18
  key2 = ApiAuth.generate_secret_key
19
- Amatch::Hamming.new(key1).match(key2).should be > 65
19
+ expect(Amatch::Hamming.new(key1).match(key2)).to be > 65
20
20
  end
21
21
 
22
22
  end
23
23
 
24
- describe "signing requests" do
24
+ def hmac(secret_key, request, canonical_string = nil)
25
+ canonical_string ||= ApiAuth::Headers.new(request).canonical_string
26
+ digest = OpenSSL::Digest.new('sha1')
27
+ ApiAuth.b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
28
+ end
25
29
 
26
- def hmac(secret_key, request)
27
- canonical_string = ApiAuth::Headers.new(request).canonical_string
28
- digest = OpenSSL::Digest.new('sha1')
29
- ApiAuth.b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
30
- end
30
+ describe ".sign!" do
31
+ let(:request){ RestClient::Request.new(:url => "http://google.com", :method => :get) }
32
+ let(:headers){ ApiAuth::Headers.new(request) }
31
33
 
32
- before(:all) do
33
- @access_id = "1044"
34
- @secret_key = ApiAuth.generate_secret_key
35
- end
34
+ it "generates date header before signing" do
35
+ expect(ApiAuth::Headers).to receive(:new).and_return(headers)
36
36
 
37
- describe "with Net::HTTP" do
38
-
39
- before(:each) do
40
- @request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
41
- 'content-type' => 'text/plain',
42
- 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
43
- 'date' => Time.now.utc.httpdate)
44
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
45
- end
37
+ expect(headers).to receive(:set_date).ordered
38
+ expect(headers).to receive(:sign_header).ordered
46
39
 
47
- it "should return a Net::HTTP object after signing it" do
48
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Net::HTTP")
49
- end
40
+ ApiAuth.sign!(request, "abc", "123")
41
+ end
50
42
 
51
- describe "md5 header" do
52
- context "not already provided" do
53
- it "should calculate for empty string" do
54
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
55
- 'content-type' => 'text/plain',
56
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
57
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
58
- signed_request['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
59
- end
60
-
61
- it "should calculate for real content" do
62
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
63
- 'content-type' => 'text/plain',
64
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
65
- request.body = "hello\nworld"
66
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
67
- signed_request['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
68
- end
69
-
70
- it "should calculate for real multipart content" do
71
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
72
- 'content-type' => 'text/plain',
73
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
74
- request.body_stream = File.new('spec/fixtures/upload.png')
75
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
76
- signed_request['Content-MD5'].should == "k4U8MTA3RHDcewBzymVNEQ=="
77
- end
78
- end
79
-
80
- it "should leave the content-md5 alone if provided" do
81
- @signed_request['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
82
- end
83
- end
43
+ it "generates content-md5 header before signing" do
44
+ expect(ApiAuth::Headers).to receive(:new).and_return(headers)
45
+ expect(headers).to receive(:calculate_md5).ordered
46
+ expect(headers).to receive(:sign_header).ordered
84
47
 
85
- it "should sign the request" do
86
- @signed_request['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
87
- end
48
+ ApiAuth.sign!(request, "abc", "123")
49
+ end
88
50
 
89
- it "should authenticate a valid request" do
90
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
91
- end
51
+ it "returns the same request object back" do
52
+ expect(ApiAuth.sign!(request, "abc", "123")).to be request
53
+ end
92
54
 
93
- it "should NOT authenticate a non-valid request" do
94
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
95
- end
55
+ it "calculates the hmac_signature as expected" do
56
+ ApiAuth.sign!(request, "1044", "123")
57
+ signature = hmac("123", request)
58
+ expect(request.headers['Authorization']).to eq("APIAuth 1044:#{signature}")
59
+ end
96
60
 
97
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
98
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
61
+ context "when passed the with_http_method option" do
62
+ let(:request){
63
+ Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
99
64
  'content-type' => 'text/plain',
100
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
101
- request.body = "hello\nworld"
102
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
103
- signed_request.body = "goodbye"
104
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
105
- end
106
-
107
- it "should NOT authenticate an expired request" do
108
- @request['Date'] = 16.minutes.ago.utc.httpdate
109
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
110
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
111
- end
65
+ 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
66
+ 'date' => Time.now.utc.httpdate
67
+ )
68
+ }
112
69
 
113
- it "should NOT authenticate a request with an invalid date" do
114
- @request['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
115
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
116
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
117
- end
70
+ let(:canonical_string){ ApiAuth::Headers.new(request).canonical_string_with_http_method }
118
71
 
119
- it "should retrieve the access_id" do
120
- ApiAuth.access_id(@signed_request).should == "1044"
72
+ it "calculates the hmac_signature with http method" do
73
+ ApiAuth.sign!(request, "1044", "123", { :with_http_method => true })
74
+ signature = hmac("123", request, canonical_string)
75
+ expect(request['Authorization']).to eq("APIAuth 1044:#{signature}")
121
76
  end
122
-
123
77
  end
78
+ end
124
79
 
125
- describe "with RestClient" do
126
-
127
- before(:each) do
128
- headers = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
129
- 'Content-Type' => "text/plain",
130
- 'Date' => Time.now.utc.httpdate }
131
- @request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
132
- :headers => headers,
133
- :method => :put)
134
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
135
- end
136
-
137
- it "should return a RestClient object after signing it" do
138
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("RestClient")
139
- end
140
-
141
- describe "md5 header" do
142
- context "not already provided" do
143
- it "should calculate for empty string" do
144
- headers = { 'Content-Type' => "text/plain",
145
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
146
- request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
147
- :headers => headers,
148
- :method => :put)
149
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
150
- signed_request.headers['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
151
- end
152
-
153
- it "should calculate for real content" do
154
- headers = { 'Content-Type' => "text/plain",
155
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
156
- request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
157
- :headers => headers,
158
- :method => :put,
159
- :payload => "hellow\nworld")
160
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
161
- signed_request.headers['Content-MD5'].should == "G0grublI06013h58g9j8Vw=="
162
- end
163
- end
164
-
165
- it "should leave the content-md5 alone if provided" do
166
- @signed_request.headers['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
167
- end
168
- end
169
-
170
- it "should sign the request" do
171
- @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
172
- end
173
-
174
- it "should sign the request using the generated md5 header" do
175
- date = Time.now.utc.httpdate
176
- headers1 = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
177
- 'Content-Type' => "text/plain",
178
- 'Date' => date }
179
- request1 = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
180
- :headers => headers1,
181
- :method => :put)
182
- headers2 = { 'Content-Type' => "text/plain",
183
- 'Date' => date }
184
- request2 = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
185
- :headers => headers2,
186
- :method => :put)
187
-
188
- ApiAuth.sign!(request1, @access_id, @secret_key)
189
- ApiAuth.sign!(request2, @access_id, @secret_key)
190
-
191
- request2.headers['Authorization'].should == request1.headers['Authorization']
192
- end
193
-
194
- it "should sign the request using the generated Date header" do
195
- headers1 = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
196
- 'Content-Type' => "text/plain"}
197
- request1 = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
198
- :headers => headers1,
199
- :method => :put)
200
- ApiAuth.sign!(request1, @access_id, @secret_key)
201
- headers2 = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
202
- 'Content-Type' => "text/plain",
203
- 'Date' => request1.headers['DATE'] }
204
- request2 = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
205
- :headers => headers2,
206
- :method => :put)
207
-
208
- ApiAuth.sign!(request2, @access_id, @secret_key)
209
-
210
- request2.headers['Authorization'].should == request1.headers['Authorization']
211
- end
212
-
213
- it "should authenticate a valid request" do
214
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
215
- end
216
-
217
- it "should NOT authenticate a non-valid request" do
218
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
219
- end
220
-
221
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
222
- headers = { 'Content-Type' => "text/plain",
223
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
224
- request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
225
- :headers => headers,
226
- :method => :put,
227
- :payload => "hello\nworld")
228
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
229
- signed_request.instance_variable_set("@payload", RestClient::Payload.generate('goodbye'))
230
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
231
- end
232
-
233
- it "should NOT authenticate an expired request" do
234
- @request.headers['Date'] = 16.minutes.ago.utc.httpdate
235
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
236
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
237
- end
238
-
239
- it "should NOT authenticate a request with an invalid date" do
240
- @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
241
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
242
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
243
- end
244
-
245
- it "should retrieve the access_id" do
246
- ApiAuth.access_id(@signed_request).should == "1044"
247
- end
248
-
80
+ describe ".authentic?" do
81
+ let(:request){
82
+ new_request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
83
+ 'content-type' => 'text/plain',
84
+ 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
85
+ 'date' => Time.now.utc.httpdate
86
+ )
87
+
88
+ signature = hmac("123", new_request)
89
+ new_request["Authorization"] = "APIAuth 1044:#{signature}"
90
+ new_request
91
+ }
92
+
93
+ it "validates that the signature in the request header matches the way we sign it" do
94
+ expect(ApiAuth.authentic?(request, "123")).to eq true
249
95
  end
250
96
 
251
- describe "with Curb" do
252
-
253
- before(:each) do
254
- headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
255
- 'Content-Type' => "text/plain",
256
- 'Date' => Time.now.utc.httpdate }
257
- @request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
258
- curl.headers = headers
259
- end
260
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
261
- end
262
-
263
- it "should return a Curl::Easy object after signing it" do
264
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Curl::Easy")
265
- end
266
-
267
- describe "md5 header" do
268
- it "should not calculate and add the content-md5 header if not provided" do
269
- headers = { 'Content-Type' => "text/plain",
270
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
271
- request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
272
- curl.headers = headers
273
- end
274
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
275
- signed_request.headers['Content-MD5'].should == nil
276
- end
277
-
278
- it "should leave the content-md5 alone if provided" do
279
- @signed_request.headers['Content-MD5'].should == "e59ff97941044f85df5297e1c302d260"
280
- end
281
- end
282
-
283
- it "should sign the request" do
284
- @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
285
- end
286
-
287
- it "should authenticate a valid request" do
288
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
289
- end
290
-
291
- it "should NOT authenticate a non-valid request" do
292
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
293
- end
294
-
295
- it "should NOT authenticate an expired request" do
296
- @request.headers['Date'] = 16.minutes.ago.utc.httpdate
297
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
298
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
299
- end
300
-
301
- it "should NOT authenticate a request with an invalid date" do
302
- @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
303
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
304
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
305
- end
306
-
307
- it "should retrieve the access_id" do
308
- ApiAuth.access_id(@signed_request).should == "1044"
309
- end
310
-
97
+ it "fails to validate a non matching signature" do
98
+ expect(ApiAuth.authentic?(request, "456")).to eq false
311
99
  end
312
100
 
313
- describe "with ActionController/ActionDispatch" do
314
-
315
- let(:request_klass){ ActionDispatch::Request rescue ActionController::Request }
316
-
317
- before(:each) do
318
- @request = request_klass.new(
319
- 'PATH_INFO' => '/resource.xml',
320
- 'QUERY_STRING' => 'foo=bar&bar=foo',
321
- 'REQUEST_METHOD' => 'PUT',
322
- 'CONTENT_MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
323
- 'CONTENT_TYPE' => 'text/plain',
324
- 'HTTP_DATE' => Time.now.utc.httpdate,
325
- 'rack.input' => StringIO.new)
326
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
327
- end
328
-
329
- it "should return a ActionDispatch::Request object after signing it" do
330
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match(request_klass.to_s)
331
- end
332
-
333
- describe "md5 header" do
334
- context "not already provided" do
335
- it "should calculate for empty string" do
336
- request = request_klass.new(
337
- 'PATH_INFO' => '/resource.xml',
338
- 'QUERY_STRING' => 'foo=bar&bar=foo',
339
- 'REQUEST_METHOD' => 'PUT',
340
- 'CONTENT_TYPE' => 'text/plain',
341
- 'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
342
- 'rack.input' => StringIO.new)
343
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
344
- signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
345
- end
346
-
347
- it "should calculate for real content" do
348
- request = request_klass.new(
349
- 'PATH_INFO' => '/resource.xml',
350
- 'QUERY_STRING' => 'foo=bar&bar=foo',
351
- 'REQUEST_METHOD' => 'PUT',
352
- 'CONTENT_TYPE' => 'text/plain',
353
- 'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
354
- 'rack.input' => StringIO.new("hello\nworld"),
355
- 'CONTENT_LENGTH' => '11')
356
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
357
- signed_request.env['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
358
- end
359
-
360
- end
361
-
362
- it "should leave the content-md5 alone if provided" do
363
- @signed_request.env['CONTENT_MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
364
- end
365
- end
366
-
367
- it "should sign the request" do
368
- @signed_request.env['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
369
- end
370
-
371
- it "should authenticate a valid request" do
372
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
373
- end
374
-
375
- it "should NOT authenticate a non-valid request" do
376
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
377
- end
378
-
379
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
380
- request = request_klass.new(
381
- 'PATH_INFO' => '/resource.xml',
382
- 'QUERY_STRING' => 'foo=bar&bar=foo',
383
- 'REQUEST_METHOD' => 'PUT',
384
- 'CONTENT_TYPE' => 'text/plain',
385
- 'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
386
- 'rack.input' => StringIO.new("hello\nworld"))
387
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
388
- signed_request.instance_variable_get("@env")["rack.input"] = StringIO.new("goodbye")
389
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
390
- end
391
-
392
- it "should NOT authenticate an expired request" do
393
- @request.env['HTTP_DATE'] = 16.minutes.ago.utc.httpdate
394
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
395
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
396
- end
397
-
398
- it "should NOT authenticate a request with an invalid date" do
399
- @request.env['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
400
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
401
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
402
- end
403
-
404
- it "should retrieve the access_id" do
405
- ApiAuth.access_id(@signed_request).should == "1044"
406
- end
407
-
101
+ it "fails to validate non matching md5" do
102
+ request['content-md5'] = '12345'
103
+ expect(ApiAuth.authentic?(request, "123")).to eq false
408
104
  end
409
105
 
410
- describe "with Rack::Request" do
411
-
412
- before(:each) do
413
- headers = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
414
- 'Content-Type' => "text/plain",
415
- 'Date' => Time.now.utc.httpdate }
416
- @request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put).merge!(headers))
417
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
418
- end
419
-
420
- it "should return a Rack::Request object after signing it" do
421
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Rack::Request")
422
- end
423
-
424
- describe "md5 header" do
425
- context "not already provided" do
426
- it "should calculate for empty string" do
427
- headers = { 'Content-Type' => "text/plain",
428
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
429
- request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put).merge!(headers))
430
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
431
- signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
432
- end
433
-
434
- it "should calculate for real content" do
435
- headers = { 'Content-Type' => "text/plain",
436
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
437
- request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "hellow\nworld").merge!(headers))
438
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
439
- signed_request.env['Content-MD5'].should == "G0grublI06013h58g9j8Vw=="
440
- end
441
- end
442
-
443
- it "should leave the content-md5 alone if provided" do
444
- @signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
445
- end
446
- end
447
-
448
- it "should sign the request" do
449
- @signed_request.env['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
450
- end
451
-
452
- it "should authenticate a valid request" do
453
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
454
- end
455
-
456
- it "should NOT authenticate a non-valid request" do
457
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
458
- end
459
-
460
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
461
- headers = { 'Content-Type' => "text/plain",
462
- 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
463
- request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "hellow\nworld").merge!(headers))
464
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
465
- changed_request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "goodbye").merge!(headers))
466
- signed_request.env['rack.input'] = changed_request.env['rack.input']
467
- signed_request.env['CONTENT_LENGTH'] = changed_request.env['CONTENT_LENGTH']
468
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
469
- end
470
-
471
- it "should NOT authenticate an expired request" do
472
- @request.env['Date'] = 16.minutes.ago.utc.httpdate
473
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
474
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
475
- end
476
-
477
- it "should NOT authenticate a request with an invalid date" do
478
- @request.env['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
479
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
480
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
481
- end
482
-
483
- it "should retrieve the access_id" do
484
- ApiAuth.access_id(@signed_request).should == "1044"
485
- end
486
-
106
+ it "fails to validate expired requests" do
107
+ request['date'] = 16.minutes.ago.utc.httpdate
108
+ expect(ApiAuth.authentic?(request, "123")).to eq false
487
109
  end
488
110
 
489
- describe "with HTTPI" do
490
- before(:each) do
491
- @request = HTTPI::Request.new("http://localhost/resource.xml?foo=bar&bar=foo")
492
- @request.headers.merge!({
493
- 'content-type' => 'text/plain',
494
- 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
495
- 'date' => Time.now.utc.httpdate
496
- })
497
- @headers = ApiAuth::Headers.new(@request)
498
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
499
- end
500
-
501
- it "should return a HTTPI object after signing it" do
502
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("HTTPI::Request")
503
- end
504
-
505
- describe "md5 header" do
506
- context "not already provided" do
507
- it "should calculate for empty string" do
508
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
509
- 'content-type' => 'text/plain',
510
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
511
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
512
- signed_request['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
513
- end
514
-
515
- it "should calculate for real content" do
516
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
517
- 'content-type' => 'text/plain',
518
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
519
- request.body = "hello\nworld"
520
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
521
- signed_request['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
522
- end
523
- end
524
-
525
- it "should leave the content-md5 alone if provided" do
526
- @signed_request.headers['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
527
- end
528
- end
529
-
530
- it "should sign the request" do
531
- @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
532
- end
533
-
534
- it "should authenticate a valid request" do
535
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
536
- end
537
-
538
- it "should NOT authenticate a non-valid request" do
539
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
540
- end
541
-
542
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
543
- request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
544
- 'content-type' => 'text/plain',
545
- 'date' => "Mon, 23 Jan 1984 03:29:56 GMT")
546
- request.body = "hello\nworld"
547
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
548
- signed_request.body = "goodbye"
549
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
550
- end
551
-
552
- it "should NOT authenticate an expired request" do
553
- @request.headers['Date'] = 16.minutes.ago.utc.httpdate
554
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
555
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
556
- end
557
-
558
- it "should NOT authenticate a request with an invalid date" do
559
- @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
560
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
561
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
562
- end
563
-
564
- it "should retrieve the access_id" do
565
- ApiAuth.access_id(@signed_request).should == "1044"
566
- end
111
+ it "fails to validate if the date is invalid" do
112
+ request['date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
113
+ expect(ApiAuth.authentic?(request, "123")).to eq false
567
114
  end
568
115
 
569
- describe "with Faraday::Request" do
570
- before(:each) do
571
- stubs = Faraday::Adapter::Test::Stubs.new do |stub|
572
- stub.put('/resource.xml?foo=bar&bar=foo') { [200, {}, ''] }
573
- stub.put('/resource.xml') { [200, {}, ''] }
574
- end
575
-
576
- @faraday_conn = Faraday.new do |builder|
577
- builder.adapter :test, stubs do |stub|
578
- end
579
- end
580
-
581
- @faraday_conn.put '/resource.xml?foo=bar&bar=foo' do |request|
582
- @request = request
583
- @request.headers.merge!({'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
584
- 'content-type' => 'text/plain',
585
- 'DATE' => Time.now.utc.httpdate})
586
- end
587
-
588
- @headers = ApiAuth::Headers.new(@request)
589
- @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
590
- end
591
-
592
- it "should return a Faraday::Request object after signing it" do
593
- ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Faraday::Request")
594
- end
595
-
596
- describe "md5 header" do
597
- context "not already provided" do
598
- it "should calculate for empty string" do
599
- @faraday_conn.put '/resource.xml?foo=bar&bar=foo' do |request|
600
- request.headers.merge!({'content-type' => 'text/plain',
601
- 'DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT'})
602
-
603
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
604
- signed_request['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
605
- end
606
- end
607
-
608
- it "should calculate for real content" do
609
- @faraday_conn.put '/resource.xml?foo=bar&bar=foo' do |request|
610
- request.headers.merge!({'content-type' => 'text/plain',
611
- 'DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT'})
612
- request.body = "hello\nworld"
613
-
614
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
615
- signed_request['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
616
- end
617
- end
618
- end
619
-
620
- it "should leave the content-md5 alone if provided" do
621
- @signed_request.headers['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
622
- end
623
- end
624
-
625
- it "should sign the request" do
626
- @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
627
- end
628
-
629
- it "should authenticate a valid request with parameters" do
630
- ApiAuth.authentic?(@signed_request, @secret_key).should be_true
631
- end
116
+ context "canonical string contains the http_method" do
117
+ let(:request){
118
+ new_request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
119
+ 'content-type' => 'text/plain',
120
+ 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
121
+ 'date' => Time.now.utc.httpdate
122
+ )
123
+ canonical_string = ApiAuth::Headers.new(new_request).canonical_string_with_http_method
124
+ signature = hmac("123", new_request, canonical_string)
125
+ new_request["Authorization"] = "APIAuth 1044:#{signature}"
126
+ new_request
127
+ }
632
128
 
633
- it "should NOT authenticate a non-valid request" do
634
- ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
129
+ it "validates for canonical_strings containing the http_method" do
130
+ expect(ApiAuth.authentic?(request, "123")).to eq true
635
131
  end
636
132
 
637
- it "should NOT authenticate a mismatched content-md5 when body has changed" do
638
- @faraday_conn.put '/resource.xml?foo=bar&bar=foo' do |request|
639
- request.headers.merge!({'content-type' => 'text/plain',
640
- 'DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT'})
641
- request.body = "hello\nworld"
642
-
643
- signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
644
- signed_request.body = 'goodbye'
645
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
646
- end
133
+ it "fails to validate if the request method differs" do
134
+ canonical_string = ApiAuth::Headers.new(request).canonical_string_with_http_method('POST')
135
+ signature = hmac("123", request, canonical_string)
136
+ request["Authorization"] = "APIAuth 1044:#{signature}"
137
+ expect(ApiAuth.authentic?(request, "123")).to eq false
647
138
  end
139
+ end
140
+ end
648
141
 
649
- it "should NOT authenticate an expired request" do
650
- @request.headers['DATE'] = 16.minutes.ago.utc.httpdate
651
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
652
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
653
- end
142
+ describe ".access_id" do
143
+ context "normal APIAuth Auth header" do
144
+ let(:request){
145
+ RestClient::Request.new(
146
+ :url => "http://google.com",
147
+ :method => :get,
148
+ :headers => {:authorization => "APIAuth 1044:aGVsbG8gd29ybGQ="}
149
+ )
150
+ }
654
151
 
655
- it "should NOT authenticate a request with an invalid date" do
656
- @request.headers['DATE'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
657
- signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
658
- ApiAuth.authentic?(signed_request, @secret_key).should be_false
152
+ it "parses it from the Auth Header" do
153
+ expect(ApiAuth.access_id(request)).to eq("1044")
659
154
  end
155
+ end
660
156
 
661
- it "should retrieve the access_id" do
662
- ApiAuth.access_id(@signed_request).should == "1044"
663
- end
157
+ context "Corporate prefixed APIAuth header" do
158
+ let(:request){
159
+ RestClient::Request.new(
160
+ :url => "http://google.com",
161
+ :method => :get,
162
+ :headers => {:authorization => "Corporate APIAuth 1044:aGVsbG8gd29ybGQ="}
163
+ )
164
+ }
664
165
 
665
- describe 'request_uri' do
666
- context 'with parameters' do
667
- it "should return urls with a query string" do
668
- req = ::ApiAuth::RequestDrivers::FaradayRequest.new(@request)
669
- req.request_uri.should == '/resource.xml?bar=foo&foo=bar'
670
- end
671
- end
672
-
673
- context 'without parameters' do
674
- it "should return urls with no query string" do
675
- @faraday_conn.put '/resource.xml' do |request|
676
- request.headers.merge!({'content-type' => 'text/plain',
677
- 'DATE' => Time.now.utc.httpdate})
678
- req = ::ApiAuth::RequestDrivers::FaradayRequest.new(request)
679
- req.request_uri.should == '/resource.xml'
680
- end
681
- end
682
- end
166
+ it "parses it from the Auth Header" do
167
+ expect(ApiAuth.access_id(request)).to eq("1044")
683
168
  end
684
169
  end
685
170
  end
686
-
687
171
  end