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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/myron/code/rack-authenticate
3
3
  specs:
4
- rack-authenticate (0.1.0)
4
+ rack-authenticate (0.2.0)
5
5
  ruby-hmac (~> 0.4.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/myron/code/rack-authenticate
3
3
  specs:
4
- rack-authenticate (0.1.0)
4
+ rack-authenticate (0.2.0)
5
5
  ruby-hmac (~> 0.4.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/myron/code/rack-authenticate
3
3
  specs:
4
- rack-authenticate (0.1.0)
4
+ rack-authenticate (0.2.0)
5
5
  ruby-hmac (~> 0.4.0)
6
6
 
7
7
  GEM
@@ -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['Date'] = date = Time.now.httpdate
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 super if auth.basic?
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
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Authenticate
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  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
- subject { Client.new(access_id, secret_key) }
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\\\/]{60,}/)
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.1.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-14 00:00:00.000000000Z
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: &2165123900 !ruby/object:Gem::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: *2165123900
24
+ version_requirements: *2164691280
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &2165122980 !ruby/object:Gem::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: *2165122980
35
+ version_requirements: *2164690620
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rack-test
38
- requirement: &2165121800 !ruby/object:Gem::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: *2165121800
46
+ version_requirements: *2164689740
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: timecop
49
- requirement: &2165119880 !ruby/object:Gem::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: *2165119880
57
+ version_requirements: *2164689140
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &2165118740 !ruby/object:Gem::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: *2165118740
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