ascarter-auth-hmac 1.0.1

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,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