ascarter-auth-hmac 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,514 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+ require "net/http"
3
+ require 'time'
4
+ require 'yaml'
5
+ require 'rubygems'
6
+ gem 'actionpack'
7
+ gem 'activeresource'
8
+ require 'action_controller'
9
+ require 'action_controller/test_process'
10
+ require 'active_resource'
11
+ require 'active_resource/http_mock'
12
+
13
+ # Class for doing a custom signature
14
+ class CustomSignature < String
15
+ def initialize(request)
16
+ self << "Custom signature string: #{request.method}"
17
+ end
18
+ end
19
+
20
+ def signature(value, secret)
21
+ digest = OpenSSL::Digest::Digest.new('sha1')
22
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, value)).strip
23
+ end
24
+
25
+ describe AuthHMAC do
26
+ before(:each) do
27
+ @request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
28
+ 'content-type' => 'text/plain',
29
+ 'content-md5' => 'blahblah',
30
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
31
+ end
32
+
33
+ describe ".canonical_string" do
34
+ it "should generate a canonical string using default method" do
35
+ AuthHMAC.canonical_string(@request).should == "PUT\ntext/plain\nblahblah\nThu, 10 Jul 2008 03:29:56 GMT\n/path/to/put"
36
+ end
37
+ end
38
+
39
+ describe ".signature" do
40
+ it "should generate a valid signature string for a secret" do
41
+ AuthHMAC.signature(@request, 'secret').should == "71wAJM4IIu/3o6lcqx/tw7XnAJs="
42
+ end
43
+ end
44
+
45
+ describe ".sign!" do
46
+ before(:each) do
47
+ @request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
48
+ 'content-type' => 'text/plain',
49
+ 'content-md5' => 'blahblah',
50
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
51
+ end
52
+
53
+ it "should sign using the key passed in as a parameter" do
54
+ AuthHMAC.sign!(@request, "my-key-id", "secret")
55
+ @request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
56
+ end
57
+
58
+ it "should sign using custom service id" do
59
+ AuthHMAC.sign!(@request, "my-key-id", "secret", { :service_id => 'MyService' })
60
+ @request['Authorization'].should == "MyService my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
61
+ end
62
+
63
+ it "should sign using custom signature method" do
64
+ options = {
65
+ :service_id => 'MyService',
66
+ :signature => CustomSignature
67
+ }
68
+ AuthHMAC.sign!(@request, "my-key-id", "secret", options)
69
+ @request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
70
+ end
71
+ end
72
+
73
+ describe "#sign!" do
74
+ before(:each) do
75
+ @get_request = Net::HTTP::Get.new("/")
76
+ @put_request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
77
+ 'content-type' => 'text/plain',
78
+ 'content-md5' => 'blahblah',
79
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
80
+ @store = mock('store')
81
+ @store.stub!(:[]).and_return("")
82
+ @authhmac = AuthHMAC.new(@store)
83
+ end
84
+
85
+ describe "default AuthHMAC with CanonicalString signature" do
86
+ it "should add an Authorization header" do
87
+ @authhmac.sign!(@get_request, 'key-id')
88
+ @get_request.key?("Authorization").should be_true
89
+ end
90
+
91
+ it "should fetch the secret from the store" do
92
+ @store.should_receive(:[]).with('key-id').and_return('secret')
93
+ @authhmac.sign!(@get_request, 'key-id')
94
+ end
95
+
96
+ it "should prefix the Authorization Header with AuthHMAC" do
97
+ @authhmac.sign!(@get_request, 'key-id')
98
+ @get_request['Authorization'].should match(/^AuthHMAC /)
99
+ end
100
+
101
+ it "should include the key id as the first part of the Authorization header value" do
102
+ @authhmac.sign!(@get_request, 'key-id')
103
+ @get_request['Authorization'].should match(/^AuthHMAC key-id:/)
104
+ end
105
+
106
+ it "should include the base64 encoded HMAC signature as the last part of the header value" do
107
+ @authhmac.sign!(@get_request, 'key-id')
108
+ @get_request['Authorization'].should match(/:[A-Za-z0-9+\/]{26,28}[=]{0,2}$/)
109
+ end
110
+
111
+ it "should create a complete signature" do
112
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
113
+ @authhmac.sign!(@put_request, "my-key-id")
114
+ @put_request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
115
+ end
116
+ end
117
+
118
+ describe "custom signatures" do
119
+ before(:each) do
120
+ @options = {
121
+ :service_id => 'MyService',
122
+ :signature => CustomSignature
123
+ }
124
+ @authhmac = AuthHMAC.new(@store, @options)
125
+ end
126
+
127
+ it "should prefix the Authorization header with custom service id" do
128
+ @authhmac.sign!(@get_request, 'key-id')
129
+ @get_request['Authorization'].should match(/^MyService /)
130
+ end
131
+
132
+ it "should create a complete signature using options" do
133
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
134
+ @authhmac.sign!(@put_request, "my-key-id")
135
+ @put_request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "authenticated?" do
141
+ before(:each) do
142
+ @credentials = YAML.load(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'credentials.yml')))
143
+ @authhmac = AuthHMAC.new(@credentials)
144
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
145
+ end
146
+
147
+ it "should return false when there is no Authorization Header" do
148
+ @authhmac.authenticated?(@request).should be_false
149
+ end
150
+
151
+ it "should return false when the Authorization value isn't prefixed with HMAC" do
152
+ @request['Authorization'] = "id:secret"
153
+ @authhmac.authenticated?(@request).should be_false
154
+ end
155
+
156
+ it "should return false when the access key id can't be found" do
157
+ @request['Authorization'] = 'AuthHMAC missing-key:blah'
158
+ @authhmac.authenticated?(@request).should be_false
159
+ end
160
+
161
+ it "should return false when there is no hmac" do
162
+ @request['Authorization'] = 'AuthHMAC missing-key:'
163
+ @authhmac.authenticated?(@request).should be_false
164
+ end
165
+
166
+ it "should return false when the hmac doesn't match" do
167
+ @request['Authorization'] = 'AuthHMAC access key 1:blah'
168
+ @authhmac.authenticated?(@request).should be_false
169
+ end
170
+
171
+ it "should return false if the request was modified after signing" do
172
+ @authhmac.sign!(@request, 'access key 1')
173
+ @request.content_type = 'text/plain'
174
+ @authhmac.authenticated?(@request).should be_false
175
+ end
176
+
177
+ it "should return true when the hmac does match" do
178
+ @authhmac.sign!(@request, 'access key 1')
179
+ @authhmac.authenticated?(@request).should be_true
180
+ end
181
+
182
+ describe "custom signatures" do
183
+ before(:each) do
184
+ @options = {
185
+ :service_id => 'MyService',
186
+ :signature => CustomSignature
187
+ }
188
+ end
189
+
190
+ it "should return false for invalid service id" do
191
+ @authhmac.sign!(@request, 'access key 1')
192
+ AuthHMAC.new(@credentials, @options.except(:signature)).authenticated?(@request).should be_false
193
+ end
194
+
195
+ it "should return false for request using default CanonicalString signature" do
196
+ @authhmac.sign!(@request, 'access key 1')
197
+ AuthHMAC.new(@credentials, @options.except(:service_id)).authenticated?(@request).should be_false
198
+ end
199
+
200
+ it "should return true when valid" do
201
+ @authhmac = AuthHMAC.new(@credentials, @options)
202
+ @authhmac.sign!(@request, 'access key 1')
203
+ @authhmac.authenticated?(@request).should be_true
204
+ end
205
+ end
206
+ end
207
+
208
+ describe "#sign! with YAML credentials" do
209
+ before(:each) do
210
+ @authhmac = AuthHMAC.new(YAML.load(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'credentials.yml'))))
211
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
212
+ end
213
+
214
+ it "should raise an argument error if credentials are missing" do
215
+ lambda { @authhmac.sign!(@request, 'missing') }.should raise_error(ArgumentError)
216
+ end
217
+
218
+ it "should sign with the secret" do
219
+ @authhmac.sign!(@request, "access key 1")
220
+ @request['Authorization'].should == "AuthHMAC access key 1:ovwO0OBERuF3/uR3aowaUCkFMiE="
221
+ end
222
+
223
+ it "should sign with the other secret" do
224
+ @authhmac.sign!(@request, "access key 2")
225
+ @request['Authorization'].should == "AuthHMAC access key 2:vT010RQm4IZ6+UCVpK2/N0FLpLw="
226
+ end
227
+ end
228
+
229
+ describe AuthHMAC::CanonicalString do
230
+ it "should include the http verb when it is GET" do
231
+ request = Net::HTTP::Get.new("/")
232
+ AuthHMAC::CanonicalString.new(request).should match(/GET/)
233
+ end
234
+
235
+ it "should include the http verb when it is POST" do
236
+ request = Net::HTTP::Post.new("/")
237
+ AuthHMAC::CanonicalString.new(request).should match(/POST/)
238
+ end
239
+
240
+ it "should include the content-type" do
241
+ request = Net::HTTP::Put.new("/", {'Content-Type' => 'application/xml'})
242
+ AuthHMAC::CanonicalString.new(request).should match(/application\/xml/)
243
+ end
244
+
245
+ it "should include the content-type even if the case is messed up" do
246
+ request = Net::HTTP::Put.new("/", {'cOntent-type' => 'text/html'})
247
+ AuthHMAC::CanonicalString.new(request).should match(/text\/html/)
248
+ end
249
+
250
+ it "should include the content-md5" do
251
+ request = Net::HTTP::Put.new("/", {'Content-MD5' => 'skwkend'})
252
+ AuthHMAC::CanonicalString.new(request).should match(/skwkend/)
253
+ end
254
+
255
+ it "should include the content-md5 even if the case is messed up" do
256
+ request = Net::HTTP::Put.new("/", {'content-md5' => 'adsada'})
257
+ AuthHMAC::CanonicalString.new(request).should match(/adsada/)
258
+ end
259
+
260
+ it "should include the date" do
261
+ date = Time.now.httpdate
262
+ request = Net::HTTP::Put.new("/", {'Date' => date})
263
+ AuthHMAC::CanonicalString.new(request).should match(/#{date}/)
264
+ end
265
+
266
+ it "should include the request path" do
267
+ request = Net::HTTP::Get.new("/path/to/file")
268
+ AuthHMAC::CanonicalString.new(request).should match(/\/path\/to\/file[^?]?/)
269
+ end
270
+
271
+ it "should ignore the query string of the request path" do
272
+ request = Net::HTTP::Get.new("/other/path/to/file?query=foo")
273
+ AuthHMAC::CanonicalString.new(request).should match(/\/other\/path\/to\/file[^?]?/)
274
+ end
275
+
276
+ it "should build the correct string" do
277
+ date = Time.now.httpdate
278
+ request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
279
+ 'content-type' => 'text/plain',
280
+ 'content-md5' => 'blahblah',
281
+ 'date' => date)
282
+ AuthHMAC::CanonicalString.new(request).should == "PUT\ntext/plain\nblahblah\n#{date}\n/path/to/put"
283
+ end
284
+
285
+ it "should build the correct string when some elements are missing" do
286
+ date = Time.now.httpdate
287
+ request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo",
288
+ 'date' => date)
289
+ AuthHMAC::CanonicalString.new(request).should == "GET\n\n\n#{date}\n/path/to/get"
290
+ end
291
+ end
292
+
293
+ describe AuthHMAC::Rails::ControllerFilter do
294
+ class TestController < ActionController::Base
295
+ with_auth_hmac YAML.load(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'credentials.yml'))),
296
+ :only => [:index]
297
+
298
+ def index
299
+ render :nothing => true, :status => :ok
300
+ end
301
+
302
+ def public
303
+ render :nothing => true, :status => :ok
304
+ end
305
+
306
+ def rescue_action(e) raise(e) end
307
+ end
308
+
309
+ class MessageTestController < ActionController::Base
310
+ with_auth_hmac YAML.load(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'credentials.yml'))),
311
+ :failure_message => "Stay away!", :except => :public
312
+
313
+ def index
314
+ render :nothing => true, :status => :ok
315
+ end
316
+
317
+ def public
318
+ render :nothing => true, :status => :ok
319
+ end
320
+
321
+ def rescue_action(e) raise(e) end
322
+ end
323
+
324
+ class NilCredentialsController < ActionController::Base
325
+ with_auth_hmac nil
326
+ before_filter :force_auth
327
+
328
+ def index
329
+ render :nothing => true, :status => :ok
330
+ end
331
+
332
+ def public
333
+ render :nothing => true, :status => :ok
334
+ end
335
+
336
+ def rescue_action(e) raise(e) end
337
+
338
+ private
339
+ def force_auth
340
+ hmac_authenticated?
341
+ end
342
+ end
343
+
344
+ class CustomTestController < ActionController::Base
345
+ with_auth_hmac YAML.load(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'credentials.yml'))),
346
+ :failure_message => "Stay away!",
347
+ :except => :public,
348
+ :hmac => { :service_id => 'MyService', :signature => CustomSignature }
349
+
350
+ def index
351
+ render :nothing => true, :status => :ok
352
+ end
353
+
354
+ def public
355
+ render :nothing => true, :status => :ok
356
+ end
357
+
358
+ def rescue_action(e) raise(e) end
359
+ end
360
+
361
+ describe NilCredentialsController do
362
+ it "should not raise an error when credentials are nil" do
363
+ request = ActionController::TestRequest.new
364
+ request.action = 'index'
365
+ request.path = "/index"
366
+ lambda do
367
+ NilCredentialsController.new.process(request, ActionController::TestResponse.new).code.should == "200"
368
+ end.should_not raise_error
369
+ end
370
+ end
371
+
372
+ describe TestController do
373
+ it "should allow a request with the proper hmac" do
374
+ request = ActionController::TestRequest.new
375
+ request.env['Authorization'] = "AuthHMAC access key 1:6BVEVfAyIDoI3K+WallRMnDxROQ="
376
+ request.env['DATE'] = "Thu, 10 Jul 2008 03:29:56 GMT"
377
+ request.action = 'index'
378
+ request.path = "/index"
379
+ TestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
380
+ end
381
+
382
+ it "should reject a request with no hmac" do
383
+ request = ActionController::TestRequest.new
384
+ request.action = 'index'
385
+ TestController.new.process(request, ActionController::TestResponse.new).code.should == "401"
386
+ end
387
+
388
+ it "should reject a request with the wrong hmac" do
389
+ request = ActionController::TestRequest.new
390
+ request.action = 'index'
391
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
392
+ TestController.new.process(request, ActionController::TestResponse.new).code.should == "401"
393
+ end
394
+
395
+ it "should include a WWW-Authenticate header with the schema AuthHMAC" do
396
+ request = ActionController::TestRequest.new
397
+ request.action = 'index'
398
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
399
+ TestController.new.process(request, ActionController::TestResponse.new).headers['WWW-Authenticate'].should == "AuthHMAC"
400
+ end
401
+
402
+ it "should include a default error message" do
403
+ request = ActionController::TestRequest.new
404
+ request.action = 'index'
405
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
406
+ TestController.new.process(request, ActionController::TestResponse.new).body.should == "HMAC Authentication failed"
407
+ end
408
+
409
+ it "should allow anything to access the public action (using only)" do
410
+ request = ActionController::TestRequest.new
411
+ request.action = 'public'
412
+ TestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
413
+ end
414
+ end
415
+
416
+ describe MessageTestController do
417
+ it "should reject a request with a given message" do
418
+ request = ActionController::TestRequest.new
419
+ request.action = 'index'
420
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
421
+ MessageTestController.new.process(request, ActionController::TestResponse.new).body.should == "Stay away!"
422
+ end
423
+
424
+ it "should allow anything to access the public action (using except)" do
425
+ request = ActionController::TestRequest.new
426
+ request.action = 'public'
427
+ MessageTestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
428
+ end
429
+ end
430
+
431
+ describe CustomTestController do
432
+ it "should allow a request with the proper hmac" do
433
+ request = ActionController::TestRequest.new
434
+ request.env['Authorization'] = "MyService access key 1:J2W4dOrv/sGsL0C5adnZYiQ3d70="
435
+ request.env['DATE'] = "Thu, 10 Jul 2008 03:29:56 GMT"
436
+ request.action = 'index'
437
+ request.path = "/index"
438
+ CustomTestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
439
+ end
440
+
441
+ it "should reject a request with no hmac" do
442
+ request = ActionController::TestRequest.new
443
+ request.action = 'index'
444
+ CustomTestController.new.process(request, ActionController::TestResponse.new).code.should == "401"
445
+ end
446
+
447
+ it "should reject a request with the wrong hmac" do
448
+ request = ActionController::TestRequest.new
449
+ request.action = 'index'
450
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
451
+ CustomTestController.new.process(request, ActionController::TestResponse.new).code.should == "401"
452
+ end
453
+
454
+ it "should reject a request with a given message" do
455
+ request = ActionController::TestRequest.new
456
+ request.action = 'index'
457
+ request.env['Authorization'] = "AuthHMAC bogus:bogus"
458
+ CustomTestController.new.process(request, ActionController::TestResponse.new).body.should == "Stay away!"
459
+ end
460
+
461
+ it "should allow anything to access the public action (using except)" do
462
+ request = ActionController::TestRequest.new
463
+ request.action = 'public'
464
+ CustomTestController.new.process(request, ActionController::TestResponse.new).code.should == "200"
465
+ end
466
+ end
467
+ end
468
+
469
+ describe AuthHMAC::Rails::ActiveResourceExtension do
470
+ class TestResource < ActiveResource::Base
471
+ with_auth_hmac("access_id", "secret")
472
+ self.site = "http://localhost/"
473
+ end
474
+
475
+ class CustomTestResource < ActiveResource::Base
476
+ with_auth_hmac("access_id", "secret", { :service_id => 'MyService', :signature => CustomSignature })
477
+ self.site = "http://localhost/"
478
+ end
479
+
480
+ describe TestResource do
481
+ it "should send requests using HMAC authentication" do
482
+ now = Time.parse("Thu, 10 Jul 2008 03:29:56 GMT")
483
+ Time.should_receive(:now).at_least(1).and_return(now)
484
+ ActiveResource::HttpMock.respond_to do |mock|
485
+ mock.get "/test_resources/1.xml",
486
+ {
487
+ 'Authorization' => 'AuthHMAC access_id:44dvKATf4xanDtypqEA0EFYvOgI=',
488
+ 'Accept' => 'application/xml',
489
+ 'Date' => "Thu, 10 Jul 2008 03:29:56 GMT"
490
+ },
491
+ { :id => "1" }.to_xml(:root => 'test_resource')
492
+ end
493
+ TestResource.find(1)
494
+ end
495
+ end
496
+
497
+ describe CustomTestResource do
498
+ it "should send requests using HMAC authentication" do
499
+ now = Time.parse("Thu, 10 Jul 2008 03:29:56 GMT")
500
+ Time.should_receive(:now).at_least(1).and_return(now)
501
+ ActiveResource::HttpMock.respond_to do |mock|
502
+ mock.get "/custom_test_resources/1.xml",
503
+ {
504
+ 'Authorization' => 'MyService access_id:ZwCBL2rWLOMnwRrdF7wWEdJn7yA=',
505
+ 'Accept' => 'application/xml',
506
+ 'Date' => "Thu, 10 Jul 2008 03:29:56 GMT"
507
+ },
508
+ { :id => "1" }.to_xml(:root => 'custom_test_resource')
509
+ end
510
+ CustomTestResource.find(1)
511
+ end
512
+ end
513
+ end
514
+ end