rack-authenticate 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/gemfiles/rack-1.1.gemfile.lock +1 -1
- data/gemfiles/rack-1.2.gemfile.lock +1 -1
- data/gemfiles/rack-1.3.gemfile.lock +1 -1
- data/lib/rack/authenticate/client.rb +10 -2
- data/lib/rack/authenticate/middleware.rb +57 -5
- data/lib/rack/authenticate/version.rb +1 -1
- data/spec/rack/authenticate/client_spec.rb +9 -1
- data/spec/rack/authenticate/middleware_spec.rb +105 -0
- data/spec/rack/authenticate_spec.rb +1 -1
- metadata +12 -18
@@ -6,13 +6,14 @@ module Rack
|
|
6
6
|
module Authenticate
|
7
7
|
class Client
|
8
8
|
attr_reader :access_id, :secret_key
|
9
|
-
def initialize(access_id, secret_key)
|
9
|
+
def initialize(access_id, secret_key, options = {})
|
10
10
|
@access_id, @secret_key = access_id, secret_key
|
11
|
+
@ajax = options[:ajax]
|
11
12
|
end
|
12
13
|
|
13
14
|
def request_signature_headers(method, url, content_type = nil, content = nil)
|
14
15
|
{}.tap do |headers|
|
15
|
-
headers[
|
16
|
+
headers[date_header_field] = date = Time.now.httpdate
|
16
17
|
request = [method.to_s.upcase, url, date]
|
17
18
|
|
18
19
|
if content_md5 = content_md5_for(content_type, content)
|
@@ -27,6 +28,13 @@ module Rack
|
|
27
28
|
|
28
29
|
private
|
29
30
|
|
31
|
+
def date_header_field
|
32
|
+
# Browsers do not allow javascript to set the Date header when making an AJAX request:
|
33
|
+
# http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method
|
34
|
+
# Thus, we allow the custom X-Authorization-Date header to be used instead of Date.
|
35
|
+
@ajax ? 'X-Authorization-Date' : 'Date'
|
36
|
+
end
|
37
|
+
|
30
38
|
def content_md5_for(content_type, content)
|
31
39
|
if content_type.nil? && content.nil?
|
32
40
|
# no-op
|
@@ -5,14 +5,18 @@ require 'time'
|
|
5
5
|
module Rack
|
6
6
|
module Authenticate
|
7
7
|
class Middleware < ::Rack::Auth::Basic
|
8
|
+
ACCESS_CONTROL_MAX_AGE = 60 * 60 * 24 # 24 hours
|
9
|
+
|
8
10
|
class Configuration
|
9
11
|
def initialize(*args)
|
10
12
|
self.timestamp_minute_tolerance ||= 30
|
11
13
|
self.hmac_secret_key { |access_id| }
|
12
14
|
self.basic_auth_validation { |u, p| false }
|
15
|
+
self.support_cross_origin_resource_sharing = false
|
13
16
|
end
|
14
17
|
|
15
18
|
attr_accessor :timestamp_minute_tolerance
|
19
|
+
attr_writer :support_cross_origin_resource_sharing
|
16
20
|
attr_reader :basic_auth_validation_block
|
17
21
|
|
18
22
|
def hmac_secret_key(&block)
|
@@ -26,6 +30,10 @@ module Rack
|
|
26
30
|
def basic_auth_validation(&block)
|
27
31
|
@basic_auth_validation_block = block
|
28
32
|
end
|
33
|
+
|
34
|
+
def support_cross_origin_resource_sharing?
|
35
|
+
@support_cross_origin_resource_sharing
|
36
|
+
end
|
29
37
|
end
|
30
38
|
|
31
39
|
class Auth < ::Rack::Auth::AbstractRequest
|
@@ -56,10 +64,6 @@ module Rack
|
|
56
64
|
@request ||= ::Rack::Request.new(@env)
|
57
65
|
end unless method_defined?(:request)
|
58
66
|
|
59
|
-
def date
|
60
|
-
request.env['HTTP_DATE']
|
61
|
-
end
|
62
|
-
|
63
67
|
def valid_current_date?
|
64
68
|
timestamp = Time.httpdate(date)
|
65
69
|
rescue ArgumentError
|
@@ -107,6 +111,25 @@ module Rack
|
|
107
111
|
valid_current_date? &&
|
108
112
|
calculated_digest == given_digest
|
109
113
|
end
|
114
|
+
|
115
|
+
def supported_cors_preflight_request?
|
116
|
+
@configuration.support_cross_origin_resource_sharing? &&
|
117
|
+
request.request_method == 'OPTIONS' &&
|
118
|
+
%w[ HTTP_ACCESS_CONTROL_REQUEST_METHOD HTTP_ORIGIN ].all? { |k| request.env.has_key?(k) }
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def date
|
124
|
+
@date ||= request.env[date_header_field]
|
125
|
+
end
|
126
|
+
|
127
|
+
def date_header_field
|
128
|
+
# Browsers do not allow javascript to set the Date header when making an AJAX request:
|
129
|
+
# http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method
|
130
|
+
# Thus, we allow the custom X-Authorization-Date header to be used instead of Date.
|
131
|
+
@date_header_field ||= ['HTTP_X_AUTHORIZATION_DATE', 'HTTP_DATE'].find { |k| request.env.has_key?(k) } || 'HTTP_DATE'
|
132
|
+
end
|
110
133
|
end
|
111
134
|
|
112
135
|
def initialize(app)
|
@@ -117,13 +140,42 @@ module Rack
|
|
117
140
|
|
118
141
|
def call(env)
|
119
142
|
auth = Auth.new(env, @configuration)
|
143
|
+
return cors_allowances(env) if auth.supported_cors_preflight_request?
|
144
|
+
|
145
|
+
_call(env, auth) { super }.tap do |(status, headers, body)|
|
146
|
+
if @configuration.support_cross_origin_resource_sharing? && env.has_key?('HTTP_ORIGIN')
|
147
|
+
headers['Access-Control-Allow-Origin'] = env['HTTP_ORIGIN']
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def _call(env, auth)
|
120
155
|
return unauthorized unless auth.provided?
|
121
|
-
return
|
156
|
+
return yield if auth.basic?
|
122
157
|
return bad_request unless auth.hmac?
|
123
158
|
return bad_request unless auth.has_all_required_parts?
|
124
159
|
return unauthorized unless auth.valid?
|
160
|
+
|
125
161
|
@app.call(env)
|
126
162
|
end
|
163
|
+
|
164
|
+
def cors_allowances(env)
|
165
|
+
headers = {
|
166
|
+
'Access-Control-Allow-Origin' => env['HTTP_ORIGIN'],
|
167
|
+
'Access-Control-Allow-Methods' => env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'],
|
168
|
+
'Access-Control-Allow-Credentials' => 'true',
|
169
|
+
'Access-Control-Max-Age' => ACCESS_CONTROL_MAX_AGE.to_s,
|
170
|
+
'Content-Type' => 'text/plain'
|
171
|
+
}
|
172
|
+
|
173
|
+
if env.has_key?('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
|
174
|
+
headers['Access-Control-Allow-Headers'] = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
|
175
|
+
end
|
176
|
+
|
177
|
+
[200, headers, []]
|
178
|
+
end
|
127
179
|
end
|
128
180
|
end
|
129
181
|
end
|
@@ -16,7 +16,8 @@ module Rack
|
|
16
16
|
|
17
17
|
let(:access_id) { 'my-access-id' }
|
18
18
|
let(:secret_key) { 'the-s3cr3t' }
|
19
|
-
|
19
|
+
let(:options) { {} }
|
20
|
+
subject { Client.new(access_id, secret_key, options) }
|
20
21
|
|
21
22
|
describe "#request_signature_headers" do
|
22
23
|
it 'raises an Argument error if given a content type but not content' do
|
@@ -78,6 +79,13 @@ module Rack
|
|
78
79
|
headers.should include('Date' => http_date)
|
79
80
|
end
|
80
81
|
|
82
|
+
it 'returns the http date as the X-Authorization-Date in the headers hash for an ajax client' do
|
83
|
+
options[:ajax] = true
|
84
|
+
headers = subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
85
|
+
headers.keys.should_not include('Date')
|
86
|
+
headers.should include('X-Authorization-Date' => http_date)
|
87
|
+
end
|
88
|
+
|
81
89
|
context 'when there is no content' do
|
82
90
|
it 'does not use anything beyond the method, url and date for the digest' do
|
83
91
|
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
@@ -119,6 +119,21 @@ module Rack
|
|
119
119
|
auth.should_not be_valid_current_date
|
120
120
|
end
|
121
121
|
end
|
122
|
+
|
123
|
+
it 'uses the X-Authorization-Date header if given in order to support browser AJAX requests' do
|
124
|
+
basic_env['HTTP_DATE'] = (Time.now - 40.minutes).httpdate
|
125
|
+
basic_env['HTTP_X_AUTHORIZATION_DATE'] = Time.now.httpdate
|
126
|
+
auth = Auth.new(basic_env, stub(:timestamp_minute_tolerance => 10))
|
127
|
+
auth.should be_valid_current_date
|
128
|
+
|
129
|
+
Timecop.freeze(base_time - 11.minutes) do
|
130
|
+
auth.should_not be_valid_current_date
|
131
|
+
end
|
132
|
+
|
133
|
+
Timecop.freeze(base_time + 11.minutes) do
|
134
|
+
auth.should_not be_valid_current_date
|
135
|
+
end
|
136
|
+
end
|
122
137
|
end
|
123
138
|
|
124
139
|
describe "#has_all_required_parts?" do
|
@@ -252,9 +267,14 @@ module Rack
|
|
252
267
|
["#{username}:#{password}"].pack("m*")
|
253
268
|
end
|
254
269
|
|
270
|
+
def configure(&block)
|
271
|
+
@configuration_block = block
|
272
|
+
end
|
273
|
+
|
255
274
|
let(:app) do
|
256
275
|
hmac_creds = hmac_auth_creds
|
257
276
|
basic_creds = basic_auth_creds
|
277
|
+
config_block = @configuration_block || Proc.new { }
|
258
278
|
|
259
279
|
Rack::Builder.new do
|
260
280
|
use Rack::ContentLength
|
@@ -262,6 +282,7 @@ module Rack
|
|
262
282
|
config.hmac_secret_key { |access_id| hmac_creds[access_id] }
|
263
283
|
config.basic_auth_validation { |u, p| basic_creds[u] == p }
|
264
284
|
config.timestamp_minute_tolerance = 30
|
285
|
+
config_block.call(config)
|
265
286
|
end
|
266
287
|
|
267
288
|
run lambda { |env| [200, {}, ['OK']] }
|
@@ -307,6 +328,13 @@ module Rack
|
|
307
328
|
last_response.status.should eq(200)
|
308
329
|
end
|
309
330
|
|
331
|
+
it 'allows an HMAC-authorized request to use the custom X-Authorization-Date header to handle browers that cannot override a Date header on an AJAX request' do
|
332
|
+
header 'Authorization', 'HMAC abc:34a70d9901bd447a02157f9fc598e43d6bf5b484'
|
333
|
+
header 'X-Authorization-Date', http_date
|
334
|
+
get '/'
|
335
|
+
last_response.status.should eq(200)
|
336
|
+
end
|
337
|
+
|
310
338
|
it 'lets the request through when there is a valid Basic authorization header' do
|
311
339
|
header 'Authorization', "BASIC #{basis_auth_value('abc', 'foo')}"
|
312
340
|
get '/'
|
@@ -329,6 +357,83 @@ module Rack
|
|
329
357
|
post '/foo', "some content"
|
330
358
|
last_response.status.should eq(200)
|
331
359
|
end
|
360
|
+
|
361
|
+
it 'generates the same signature as an AJAX client', :no_timecop do
|
362
|
+
client = Client.new('abc', hmac_auth_creds['abc'], :ajax => true)
|
363
|
+
client.request_signature_headers('post', 'http://example.org/foo', 'text/plain', "some content").each do |key, value|
|
364
|
+
header key, value
|
365
|
+
end
|
366
|
+
|
367
|
+
header 'Content-Type', 'text/plain'
|
368
|
+
post '/foo', "some content"
|
369
|
+
last_response.status.should eq(200)
|
370
|
+
end
|
371
|
+
|
372
|
+
context 'when cross origin resource sharing is supported' do
|
373
|
+
before { configure { |c| c.support_cross_origin_resource_sharing = true } }
|
374
|
+
let(:headers) { 'X-Authorization-Date, Content-MD5, Authorization, Content-Type' }
|
375
|
+
let(:origin) { 'http://foo.example.com' }
|
376
|
+
|
377
|
+
let(:expected_response_headers) do {
|
378
|
+
'Content-Type' => 'text/plain',
|
379
|
+
'Access-Control-Allow-Origin' => origin,
|
380
|
+
'Access-Control-Allow-Methods' => 'PUT',
|
381
|
+
'Access-Control-Allow-Credentials' => 'true',
|
382
|
+
'Access-Control-Max-Age' => ACCESS_CONTROL_MAX_AGE.to_s
|
383
|
+
} end
|
384
|
+
|
385
|
+
it 'responds to a CORS OPTIONS request with all of the correct headers' do
|
386
|
+
header 'Origin', origin
|
387
|
+
header 'Access-Control-Request-Method', 'PUT'
|
388
|
+
options '/'
|
389
|
+
|
390
|
+
last_response.status.should eq(200)
|
391
|
+
last_response.headers.should include(expected_response_headers)
|
392
|
+
last_response.headers.should_not have_key('Access-Control-Allow-Headers')
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'includes Access-Control-Allow-Headers when they the request asks about them' do
|
396
|
+
header 'Origin', origin
|
397
|
+
header 'Access-Control-Request-Method', 'PUT'
|
398
|
+
header 'Access-Control-Request-Headers', headers
|
399
|
+
options '/'
|
400
|
+
|
401
|
+
last_response.status.should eq(200)
|
402
|
+
last_response.headers.should include(expected_response_headers.merge(
|
403
|
+
'Access-Control-Allow-Headers' => headers
|
404
|
+
))
|
405
|
+
end
|
406
|
+
|
407
|
+
it 'appends the Access-Control-Allow-Origin header to every response to a request with an Origin header' do
|
408
|
+
header 'Origin', origin
|
409
|
+
get '/'
|
410
|
+
last_response.headers.should include('Access-Control-Allow-Origin' => origin)
|
411
|
+
end
|
412
|
+
|
413
|
+
it 'does not append a Access-Control-Allow-Origin header to a request without an Origin header' do
|
414
|
+
get '/'
|
415
|
+
last_response.headers.keys.should_not include('Access-Control-Allow-Origin')
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context 'when cross origin resource sharing is not supported' do
|
420
|
+
before { configure { |c| c.support_cross_origin_resource_sharing = false } }
|
421
|
+
|
422
|
+
it 'does not respond to a CORS OPTIONS request' do
|
423
|
+
header 'Origin', 'http://foo.example.com'
|
424
|
+
header 'Access-Control-Request-Method', 'PUT'
|
425
|
+
options '/'
|
426
|
+
|
427
|
+
last_response.status.should eq(401)
|
428
|
+
last_response.headers.keys.select { |k| k.include?('Access-Control') }.should eq([])
|
429
|
+
end
|
430
|
+
|
431
|
+
it 'does not append the Access-Control-Allow-Origin header to every response' do
|
432
|
+
header 'Origin', 'http://foo.example.com'
|
433
|
+
get '/'
|
434
|
+
last_response.headers.keys.should_not include('Access-Control-Allow-Origin')
|
435
|
+
end
|
436
|
+
end
|
332
437
|
end
|
333
438
|
end
|
334
439
|
end
|
@@ -10,7 +10,7 @@ module Rack
|
|
10
10
|
describe Authenticate do
|
11
11
|
describe "#new_secret_key" do
|
12
12
|
it "generates a long random string" do
|
13
|
-
Rack::Authenticate.new_secret_key.should match(/[A-Za-z0-9
|
13
|
+
Rack::Authenticate.new_secret_key.should match(/[A-Za-z0-9\\\/\+]{60,}/)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-authenticate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-12-
|
12
|
+
date: 2011-12-16 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ruby-hmac
|
16
|
-
requirement: &
|
16
|
+
requirement: &2164691280 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.4.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2164691280
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &2164690620 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 2.8.0.rc1
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2164690620
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rack-test
|
38
|
-
requirement: &
|
38
|
+
requirement: &2164689740 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 0.6.1
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2164689740
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: timecop
|
49
|
-
requirement: &
|
49
|
+
requirement: &2164689140 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 0.3.5
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2164689140
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rake
|
60
|
-
requirement: &
|
60
|
+
requirement: &2164688520 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,7 +65,7 @@ dependencies:
|
|
65
65
|
version: 0.9.2.2
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *2164688520
|
69
69
|
description: A rack middleware that authenticates requests either using basic auth
|
70
70
|
or via signed HMAC.
|
71
71
|
email:
|
@@ -108,18 +108,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
108
|
- - ! '>='
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
-
segments:
|
112
|
-
- 0
|
113
|
-
hash: 3915972377908680387
|
114
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
112
|
none: false
|
116
113
|
requirements:
|
117
114
|
- - ! '>='
|
118
115
|
- !ruby/object:Gem::Version
|
119
116
|
version: '0'
|
120
|
-
segments:
|
121
|
-
- 0
|
122
|
-
hash: 3915972377908680387
|
123
117
|
requirements: []
|
124
118
|
rubyforge_project: rack-authenticate
|
125
119
|
rubygems_version: 1.8.6
|