bixby-auth 0.1.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.
@@ -0,0 +1,660 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe "ApiAuth" do
5
+
6
+ describe "generating secret keys" do
7
+
8
+ it "should generate secret keys" do
9
+ ApiAuth.generate_secret_key
10
+ end
11
+
12
+ it "should generate secret keys that are 88 characters" do
13
+ ApiAuth.generate_secret_key.size.should be(88)
14
+ end
15
+
16
+ it "should generate keys that have a Hamming Distance of at least 65" do
17
+ key1 = ApiAuth.generate_secret_key
18
+ key2 = ApiAuth.generate_secret_key
19
+ Amatch::Hamming.new(key1).match(key2).should be > 65
20
+ end
21
+
22
+ end
23
+
24
+ describe "signing requests" do
25
+
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
31
+
32
+ before(:all) do
33
+ @access_id = "1044"
34
+ @secret_key = ApiAuth.generate_secret_key
35
+ end
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
46
+
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
50
+
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' => Time.now.utc.httpdate)
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' => Time.now.utc.httpdate)
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' => Time.now.utc.httpdate)
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
84
+
85
+ it "should sign the request" do
86
+ @signed_request['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
87
+ end
88
+
89
+ it "should authenticate a valid request" do
90
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
91
+ end
92
+
93
+ it "should NOT authenticate a non-valid request" do
94
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
95
+ end
96
+
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",
99
+ 'content-type' => 'text/plain',
100
+ 'date' => Time.now.utc.httpdate)
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
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
111
+ end
112
+
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
118
+
119
+ it "should retrieve the access_id" do
120
+ ApiAuth.access_id(@signed_request).should == "1044"
121
+ end
122
+
123
+ end
124
+
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' => Time.now.utc.httpdate }
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' => Time.now.utc.httpdate }
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' => Time.now.utc.httpdate }
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
+ headers = @request.headers.dup.merge({"Date" => 16.minutes.ago.utc.httpdate})
235
+ headers.delete("Authorization")
236
+ request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
237
+ :headers => headers,
238
+ :method => :put)
239
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
240
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
241
+ end
242
+
243
+ it "should NOT authenticate a request with an invalid date" do
244
+ @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
245
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
246
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
247
+ end
248
+
249
+ it "should retrieve the access_id" do
250
+ ApiAuth.access_id(@signed_request).should == "1044"
251
+ end
252
+
253
+ end
254
+
255
+ describe "with Curb" do
256
+
257
+ before(:each) do
258
+ headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
259
+ 'Content-Type' => "text/plain",
260
+ 'Date' => Time.now.utc.httpdate }
261
+ @request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
262
+ curl.headers = headers
263
+ end
264
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
265
+ end
266
+
267
+ it "should return a Curl::Easy object after signing it" do
268
+ ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Curl::Easy")
269
+ end
270
+
271
+ describe "md5 header" do
272
+ it "should not calculate and add the content-md5 header if not provided" do
273
+ headers = { 'Content-Type' => "text/plain",
274
+ 'Date' => Time.now.utc.httpdate }
275
+ request = Curl::Easy.new("/resource.xml?foo=bar&bar=foo") do |curl|
276
+ curl.headers = headers
277
+ end
278
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
279
+ signed_request.headers['Content-MD5'].should == nil
280
+ end
281
+
282
+ it "should leave the content-md5 alone if provided" do
283
+ @signed_request.headers['Content-MD5'].should == "e59ff97941044f85df5297e1c302d260"
284
+ end
285
+ end
286
+
287
+ it "should sign the request" do
288
+ @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
289
+ end
290
+
291
+ it "should authenticate a valid request" do
292
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
293
+ end
294
+
295
+ it "should NOT authenticate a non-valid request" do
296
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
297
+ end
298
+
299
+ it "should NOT authenticate an expired request" do
300
+ @request.headers['Date'] = 16.minutes.ago.utc.httpdate
301
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
302
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
303
+ end
304
+
305
+ it "should NOT authenticate a request with an invalid date" do
306
+ @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
307
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
308
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
309
+ end
310
+
311
+ it "should retrieve the access_id" do
312
+ ApiAuth.access_id(@signed_request).should == "1044"
313
+ end
314
+
315
+ end
316
+
317
+ describe "with ActionController/ActionDispatch" do
318
+
319
+ let(:request_klass){ ActionDispatch::Request rescue ActionController::Request }
320
+
321
+ before(:each) do
322
+ @request = request_klass.new(
323
+ 'PATH_INFO' => '/resource.xml',
324
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
325
+ 'REQUEST_METHOD' => 'PUT',
326
+ 'CONTENT_MD5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
327
+ 'CONTENT_TYPE' => 'text/plain',
328
+ 'HTTP_DATE' => Time.now.utc.httpdate,
329
+ 'rack.input' => StringIO.new)
330
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
331
+ end
332
+
333
+ it "should return a ActionDispatch::Request object after signing it" do
334
+ ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match(request_klass.to_s)
335
+ end
336
+
337
+ describe "md5 header" do
338
+ context "not already provided" do
339
+ it "should calculate for empty string" do
340
+ request = request_klass.new(
341
+ 'PATH_INFO' => '/resource.xml',
342
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
343
+ 'REQUEST_METHOD' => 'PUT',
344
+ 'CONTENT_TYPE' => 'text/plain',
345
+ 'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
346
+ 'rack.input' => StringIO.new)
347
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
348
+ signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
349
+ end
350
+
351
+ it "should calculate for real content" do
352
+ request = request_klass.new(
353
+ 'PATH_INFO' => '/resource.xml',
354
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
355
+ 'REQUEST_METHOD' => 'PUT',
356
+ 'CONTENT_TYPE' => 'text/plain',
357
+ 'HTTP_DATE' => 'Mon, 23 Jan 1984 03:29:56 GMT',
358
+ 'rack.input' => StringIO.new("hello\nworld"),
359
+ 'CONTENT_LENGTH' => '11')
360
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
361
+ signed_request.env['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
362
+ end
363
+
364
+ end
365
+
366
+ it "should leave the content-md5 alone if provided" do
367
+ @signed_request.env['CONTENT_MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
368
+ end
369
+ end
370
+
371
+ it "should sign the request" do
372
+ @signed_request.env['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
373
+ end
374
+
375
+ it "should authenticate a valid request" do
376
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
377
+ end
378
+
379
+ it "should NOT authenticate a non-valid request" do
380
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
381
+ end
382
+
383
+ it "should NOT authenticate a mismatched content-md5 when body has changed" do
384
+ request = request_klass.new(
385
+ 'PATH_INFO' => '/resource.xml',
386
+ 'QUERY_STRING' => 'foo=bar&bar=foo',
387
+ 'REQUEST_METHOD' => 'PUT',
388
+ 'CONTENT_TYPE' => 'text/plain',
389
+ 'HTTP_DATE' => Time.now.utc.httpdate,
390
+ 'rack.input' => StringIO.new("hello\nworld"))
391
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
392
+ signed_request.instance_variable_get("@env")["rack.input"] = StringIO.new("goodbye")
393
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
394
+ end
395
+
396
+ it "should NOT authenticate an expired request" do
397
+ @request.env['HTTP_DATE'] = 16.minutes.ago.utc.httpdate
398
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
399
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
400
+ end
401
+
402
+ it "should NOT authenticate a request with an invalid date" do
403
+ @request.env['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
404
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
405
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
406
+ end
407
+
408
+ it "should retrieve the access_id" do
409
+ ApiAuth.access_id(@signed_request).should == "1044"
410
+ end
411
+
412
+ end
413
+
414
+ describe "with Rack::Request" do
415
+
416
+ before(:each) do
417
+ headers = { 'Content-MD5' => "1B2M2Y8AsgTpgAmY7PhCfg==",
418
+ 'Content-Type' => "text/plain",
419
+ 'Date' => Time.now.utc.httpdate }
420
+ @request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put).merge!(headers))
421
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
422
+ end
423
+
424
+ it "should return a Rack::Request object after signing it" do
425
+ ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Rack::Request")
426
+ end
427
+
428
+ describe "md5 header" do
429
+ context "not already provided" do
430
+ it "should calculate for empty string" do
431
+ headers = { 'Content-Type' => "text/plain",
432
+ 'Date' => Time.now.utc.httpdate }
433
+ request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put).merge!(headers))
434
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
435
+ signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
436
+ end
437
+
438
+ it "should calculate for real content" do
439
+ headers = { 'Content-Type' => "text/plain",
440
+ 'Date' => Time.now.utc.httpdate }
441
+ request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "hellow\nworld").merge!(headers))
442
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
443
+ signed_request.env['Content-MD5'].should == "G0grublI06013h58g9j8Vw=="
444
+ end
445
+ end
446
+
447
+ it "should leave the content-md5 alone if provided" do
448
+ @signed_request.env['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
449
+ end
450
+ end
451
+
452
+ it "should sign the request" do
453
+ @signed_request.env['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
454
+ end
455
+
456
+ it "should authenticate a valid request" do
457
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
458
+ end
459
+
460
+ it "should NOT authenticate a non-valid request" do
461
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
462
+ end
463
+
464
+ it "should NOT authenticate a mismatched content-md5 when body has changed" do
465
+ headers = { 'Content-Type' => "text/plain",
466
+ 'Date' => Time.now.utc.httpdate }
467
+ request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "hellow\nworld").merge!(headers))
468
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
469
+ changed_request = Rack::Request.new(Rack::MockRequest.env_for("/resource.xml?foo=bar&bar=foo", :method => :put, :input => "goodbye").merge!(headers))
470
+ signed_request.env['rack.input'] = changed_request.env['rack.input']
471
+ signed_request.env['CONTENT_LENGTH'] = changed_request.env['CONTENT_LENGTH']
472
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
473
+ end
474
+
475
+ it "should NOT authenticate an expired request" do
476
+ @request.env['Date'] = 16.minutes.ago.utc.httpdate
477
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
478
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
479
+ end
480
+
481
+ it "should NOT authenticate a request with an invalid date" do
482
+ @request.env['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
483
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
484
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
485
+ end
486
+
487
+ it "should retrieve the access_id" do
488
+ ApiAuth.access_id(@signed_request).should == "1044"
489
+ end
490
+
491
+ end
492
+
493
+ describe "with HTTPI" do
494
+ before(:each) do
495
+ @request = HTTPI::Request.new("http://localhost/resource.xml?foo=bar&bar=foo")
496
+ @request.headers.merge!({
497
+ 'content-type' => 'text/plain',
498
+ 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
499
+ 'date' => Time.now.utc.httpdate
500
+ })
501
+ @headers = ApiAuth::Headers.new(@request)
502
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
503
+ end
504
+
505
+ it "should return a HTTPI object after signing it" do
506
+ ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("HTTPI::Request")
507
+ end
508
+
509
+ describe "md5 header" do
510
+ context "not already provided" do
511
+ it "should calculate for empty string" do
512
+ request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
513
+ 'content-type' => 'text/plain',
514
+ 'date' => Time.now.utc.httpdate)
515
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
516
+ signed_request['Content-MD5'].should == "1B2M2Y8AsgTpgAmY7PhCfg=="
517
+ end
518
+
519
+ it "should calculate for real content" do
520
+ request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
521
+ 'content-type' => 'text/plain',
522
+ 'date' => Time.now.utc.httpdate)
523
+ request.body = "hello\nworld"
524
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
525
+ signed_request['Content-MD5'].should == "kZXQvrKoieG+Be1rsZVINw=="
526
+ end
527
+ end
528
+
529
+ it "should leave the content-md5 alone if provided" do
530
+ @signed_request.headers['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
531
+ end
532
+ end
533
+
534
+ it "should sign the request" do
535
+ @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
536
+ end
537
+
538
+ it "should authenticate a valid request" do
539
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
540
+ end
541
+
542
+ it "should NOT authenticate a non-valid request" do
543
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
544
+ end
545
+
546
+ it "should NOT authenticate a mismatched content-md5 when body has changed" do
547
+ request = Net::HTTP::Put.new("/resource.xml?foo=bar&bar=foo",
548
+ 'content-type' => 'text/plain',
549
+ 'date' => Time.now.utc.httpdate)
550
+ request.body = "hello\nworld"
551
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
552
+ signed_request.body = "goodbye"
553
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
554
+ end
555
+
556
+ it "should NOT authenticate an expired request" do
557
+ @request.headers['Date'] = 16.minutes.ago.utc.httpdate
558
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
559
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
560
+ end
561
+
562
+ it "should NOT authenticate a request with an invalid date" do
563
+ @request.headers['Date'] = "٢٠١٤-٠٩-٠٨ ١٦:٣١:١٤ +٠٣٠٠"
564
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
565
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
566
+ end
567
+
568
+ it "should retrieve the access_id" do
569
+ ApiAuth.access_id(@signed_request).should == "1044"
570
+ end
571
+ end
572
+
573
+ describe "with Bixby::SignedJsonRequest" do
574
+
575
+ before(:each) do
576
+ @json_req = Bixby::JsonRequest.new("foo", "bar")
577
+ @request = Bixby::SignedJsonRequest.new(@json_req)
578
+ @request.headers.merge!({
579
+ 'Content-Type' => 'text/plain',
580
+ 'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
581
+ 'date' => ApiAuth::Helpers.time_as_httpdate
582
+ })
583
+ @request.body = ""
584
+ @headers = ApiAuth::Headers.new(@request)
585
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
586
+ end
587
+
588
+ it "should return a Bixby::SignedJsonRequest object after signing it" do
589
+ ApiAuth.sign!(@request, @access_id, @secret_key).class.to_s.should match("Bixby::SignedJsonRequest")
590
+ end
591
+
592
+ describe "md5 header" do
593
+ context "not already provided" do
594
+ it "should calculate for empty string" do
595
+ request = Bixby::SignedJsonRequest.new(@json_req)
596
+ request.headers.merge!({
597
+ 'Content-Type' => 'text/plain',
598
+ 'Date' => Time.now.utc.httpdate
599
+ })
600
+ request.body = ""
601
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
602
+ signed_request.headers['Content-MD5'].should == Digest::MD5.base64digest('')
603
+ end
604
+
605
+ it "should calculate for real content" do
606
+ request = Bixby::SignedJsonRequest.new(@json_req)
607
+ request.headers.merge!({
608
+ 'Content-Type' => 'text/plain',
609
+ 'Date' => Time.now.utc.httpdate
610
+ })
611
+ request.body = "hello\nworld"
612
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
613
+ signed_request.headers['Content-MD5'].should == Digest::MD5.base64digest("hello\nworld")
614
+ end
615
+ end
616
+
617
+ it "should leave the Content-MD5 alone if provided" do
618
+ @signed_request.headers['Content-MD5'].should == '1B2M2Y8AsgTpgAmY7PhCfg=='
619
+ end
620
+ end
621
+
622
+ it "should sign the request" do
623
+ @signed_request.headers['Authorization'].should == "APIAuth 1044:#{hmac(@secret_key, @request)}"
624
+ end
625
+
626
+ it "should authenticate a valid request" do
627
+ ApiAuth.authentic?(@signed_request, @secret_key).should be_true
628
+ end
629
+
630
+ it "should NOT authenticate a non-valid request" do
631
+ ApiAuth.authentic?(@signed_request, @secret_key+'j').should be_false
632
+ end
633
+
634
+ it "should NOT authenticate a mismatched content-md5 when body has changed" do
635
+ request = Bixby::SignedJsonRequest.new(@json_req)
636
+ request.headers.merge!({
637
+ 'Content-Type' => 'text/plain',
638
+ 'Date' => Time.now.utc.httpdate
639
+ })
640
+ request.body = "hello\nworld"
641
+ signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
642
+ signed_request.body = "goodbye"
643
+ ApiAuth.authentic?(signed_request, @secret_key).should be_false
644
+ end
645
+
646
+ it "should NOT authenticate an expired request" do
647
+ @request.headers['Date'] = 16.minutes.ago.utc.httpdate
648
+ signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
649
+ expect{ ApiAuth.authentic?(signed_request, @secret_key)}.to raise_error(ApiAuth::RequestTooOld)
650
+ end
651
+
652
+ it "should retrieve the access_id" do
653
+ ApiAuth.access_id(@signed_request).should == "1044"
654
+ end
655
+
656
+ end
657
+
658
+ end
659
+
660
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
Binary file