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.
@@ -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