api-auth 1.3.2 → 1.4.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.
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