rack-authenticate 0.1.0 → 0.2.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.
- 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
|