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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +28 -0
- data/Gemfile.lock +245 -0
- data/LICENSE.txt +20 -0
- data/README.md +19 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/bixby-auth.gemspec +129 -0
- data/lib/api-auth.rb +2 -0
- data/lib/api_auth/base.rb +105 -0
- data/lib/api_auth/errors.rb +12 -0
- data/lib/api_auth/headers.rb +82 -0
- data/lib/api_auth/helpers.rb +39 -0
- data/lib/api_auth/railtie.rb +129 -0
- data/lib/api_auth/request_drivers/action_controller.rb +84 -0
- data/lib/api_auth/request_drivers/action_dispatch.rb +17 -0
- data/lib/api_auth/request_drivers/bixby_request.rb +65 -0
- data/lib/api_auth/request_drivers/curb.rb +72 -0
- data/lib/api_auth/request_drivers/httpi.rb +82 -0
- data/lib/api_auth/request_drivers/net_http.rb +98 -0
- data/lib/api_auth/request_drivers/rack.rb +88 -0
- data/lib/api_auth/request_drivers/rest_client.rb +98 -0
- data/lib/api_auth/request_drivers.rb +21 -0
- data/lib/api_auth.rb +10 -0
- data/lib/bixby-auth.rb +3 -0
- data/spec/api_auth_spec.rb +660 -0
- data/spec/application_helper.rb +2 -0
- data/spec/fixtures/upload.png +0 -0
- data/spec/headers_spec.rb +356 -0
- data/spec/helpers_spec.rb +14 -0
- data/spec/railtie_spec.rb +134 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/test_helper.rb +2 -0
- data/test/helper.rb +35 -0
- data/test/test_bixby-auth.rb +7 -0
- metadata +346 -0
@@ -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
|
Binary file
|