rack-authenticate 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.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +11 -0
- data/Appraisals +11 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/Rakefile +6 -0
- data/gemfiles/rack-1.1.gemfile +8 -0
- data/gemfiles/rack-1.1.gemfile.lock +39 -0
- data/gemfiles/rack-1.2.gemfile +8 -0
- data/gemfiles/rack-1.2.gemfile.lock +39 -0
- data/gemfiles/rack-1.3.gemfile +8 -0
- data/gemfiles/rack-1.3.gemfile.lock +39 -0
- data/lib/rack-authenticate.rb +1 -0
- data/lib/rack/authenticate.rb +11 -0
- data/lib/rack/authenticate/client.rb +42 -0
- data/lib/rack/authenticate/middleware.rb +130 -0
- data/lib/rack/authenticate/version.rb +5 -0
- data/rack-authenticate.gemspec +26 -0
- data/spec/rack/authenticate/client_spec.rb +126 -0
- data/spec/rack/authenticate/middleware_spec.rb +336 -0
- data/spec/rack/authenticate_spec.rb +17 -0
- metadata +133 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Appraisals
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 SEOmoz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/myron/code/rack-authenticate
|
3
|
+
specs:
|
4
|
+
rack-authenticate (0.1.0)
|
5
|
+
ruby-hmac (~> 0.4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
appraisal (0.4.0)
|
11
|
+
bundler
|
12
|
+
rake
|
13
|
+
diff-lcs (1.1.3)
|
14
|
+
rack (1.1.2)
|
15
|
+
rack-test (0.6.1)
|
16
|
+
rack (>= 1.0)
|
17
|
+
rake (0.9.2.2)
|
18
|
+
rspec (2.8.0.rc1)
|
19
|
+
rspec-core (= 2.8.0.rc1)
|
20
|
+
rspec-expectations (= 2.8.0.rc1)
|
21
|
+
rspec-mocks (= 2.8.0.rc1)
|
22
|
+
rspec-core (2.8.0.rc1)
|
23
|
+
rspec-expectations (2.8.0.rc1)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.8.0.rc1)
|
26
|
+
ruby-hmac (0.4.0)
|
27
|
+
timecop (0.3.5)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
appraisal
|
34
|
+
rack (~> 1.1.2)
|
35
|
+
rack-authenticate!
|
36
|
+
rack-test (~> 0.6.1)
|
37
|
+
rake (~> 0.9.2.2)
|
38
|
+
rspec (~> 2.8.0.rc1)
|
39
|
+
timecop (~> 0.3.5)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/myron/code/rack-authenticate
|
3
|
+
specs:
|
4
|
+
rack-authenticate (0.1.0)
|
5
|
+
ruby-hmac (~> 0.4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
appraisal (0.4.0)
|
11
|
+
bundler
|
12
|
+
rake
|
13
|
+
diff-lcs (1.1.3)
|
14
|
+
rack (1.2.4)
|
15
|
+
rack-test (0.6.1)
|
16
|
+
rack (>= 1.0)
|
17
|
+
rake (0.9.2.2)
|
18
|
+
rspec (2.8.0.rc1)
|
19
|
+
rspec-core (= 2.8.0.rc1)
|
20
|
+
rspec-expectations (= 2.8.0.rc1)
|
21
|
+
rspec-mocks (= 2.8.0.rc1)
|
22
|
+
rspec-core (2.8.0.rc1)
|
23
|
+
rspec-expectations (2.8.0.rc1)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.8.0.rc1)
|
26
|
+
ruby-hmac (0.4.0)
|
27
|
+
timecop (0.3.5)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
appraisal
|
34
|
+
rack (~> 1.2.4)
|
35
|
+
rack-authenticate!
|
36
|
+
rack-test (~> 0.6.1)
|
37
|
+
rake (~> 0.9.2.2)
|
38
|
+
rspec (~> 2.8.0.rc1)
|
39
|
+
timecop (~> 0.3.5)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/myron/code/rack-authenticate
|
3
|
+
specs:
|
4
|
+
rack-authenticate (0.1.0)
|
5
|
+
ruby-hmac (~> 0.4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
appraisal (0.4.0)
|
11
|
+
bundler
|
12
|
+
rake
|
13
|
+
diff-lcs (1.1.3)
|
14
|
+
rack (1.3.5)
|
15
|
+
rack-test (0.6.1)
|
16
|
+
rack (>= 1.0)
|
17
|
+
rake (0.9.2.2)
|
18
|
+
rspec (2.8.0.rc1)
|
19
|
+
rspec-core (= 2.8.0.rc1)
|
20
|
+
rspec-expectations (= 2.8.0.rc1)
|
21
|
+
rspec-mocks (= 2.8.0.rc1)
|
22
|
+
rspec-core (2.8.0.rc1)
|
23
|
+
rspec-expectations (2.8.0.rc1)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.8.0.rc1)
|
26
|
+
ruby-hmac (0.4.0)
|
27
|
+
timecop (0.3.5)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
appraisal
|
34
|
+
rack (~> 1.3.5)
|
35
|
+
rack-authenticate!
|
36
|
+
rack-test (~> 0.6.1)
|
37
|
+
rake (~> 0.9.2.2)
|
38
|
+
rspec (~> 2.8.0.rc1)
|
39
|
+
timecop (~> 0.3.5)
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/authenticate'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'hmac-sha1'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Authenticate
|
7
|
+
class Client
|
8
|
+
attr_reader :access_id, :secret_key
|
9
|
+
def initialize(access_id, secret_key)
|
10
|
+
@access_id, @secret_key = access_id, secret_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_signature_headers(method, url, content_type = nil, content = nil)
|
14
|
+
{}.tap do |headers|
|
15
|
+
headers['Date'] = date = Time.now.httpdate
|
16
|
+
request = [method.to_s.upcase, url, date]
|
17
|
+
|
18
|
+
if content_md5 = content_md5_for(content_type, content)
|
19
|
+
headers['Content-MD5'] = content_md5
|
20
|
+
request += [content_type, content_md5]
|
21
|
+
end
|
22
|
+
|
23
|
+
digest = HMAC::SHA1.hexdigest(secret_key, request.join("\n"))
|
24
|
+
headers['Authorization'] = "HMAC #{access_id}:#{digest}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def content_md5_for(content_type, content)
|
31
|
+
if content_type.nil? && content.nil?
|
32
|
+
# no-op
|
33
|
+
elsif content_type && content
|
34
|
+
Digest::MD5.hexdigest(content)
|
35
|
+
else
|
36
|
+
raise ArgumentError.new("Both content_type and content must be given or neither.")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'hmac-sha1'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Authenticate
|
7
|
+
class Middleware < ::Rack::Auth::Basic
|
8
|
+
class Configuration
|
9
|
+
def initialize(*args)
|
10
|
+
self.timestamp_minute_tolerance ||= 30
|
11
|
+
self.hmac_secret_key { |access_id| }
|
12
|
+
self.basic_auth_validation { |u, p| false }
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :timestamp_minute_tolerance
|
16
|
+
attr_reader :basic_auth_validation_block
|
17
|
+
|
18
|
+
def hmac_secret_key(&block)
|
19
|
+
@hmac_secret_key_block = block
|
20
|
+
end
|
21
|
+
|
22
|
+
def hmac_secret_key_for(access_id)
|
23
|
+
@hmac_secret_key_block[access_id]
|
24
|
+
end
|
25
|
+
|
26
|
+
def basic_auth_validation(&block)
|
27
|
+
@basic_auth_validation_block = block
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Auth < ::Rack::Auth::AbstractRequest
|
32
|
+
def initialize(env, configuration = Configuration.new)
|
33
|
+
super(env)
|
34
|
+
@configuration = configuration
|
35
|
+
end
|
36
|
+
|
37
|
+
def basic?
|
38
|
+
:basic == scheme
|
39
|
+
end
|
40
|
+
|
41
|
+
def hmac?
|
42
|
+
:hmac == scheme
|
43
|
+
end
|
44
|
+
|
45
|
+
def has_all_required_parts?
|
46
|
+
return false unless date
|
47
|
+
|
48
|
+
if has_content?
|
49
|
+
content_md5.to_s != '' && request.content_type.to_s != ''
|
50
|
+
else
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def request
|
56
|
+
@request ||= ::Rack::Request.new(@env)
|
57
|
+
end unless method_defined?(:request)
|
58
|
+
|
59
|
+
def date
|
60
|
+
request.env['HTTP_DATE']
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_current_date?
|
64
|
+
timestamp = Time.httpdate(date)
|
65
|
+
rescue ArgumentError
|
66
|
+
return false
|
67
|
+
else
|
68
|
+
tolerance = @configuration.timestamp_minute_tolerance * 60
|
69
|
+
now = Time.now
|
70
|
+
(now - tolerance) <= timestamp && (now + tolerance) >= timestamp
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_content?
|
74
|
+
request.content_length.to_i > 0
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO: replace the request body with a proxy object that verifies this when it is read.
|
78
|
+
def content_md5
|
79
|
+
request.env['HTTP_CONTENT_MD5']
|
80
|
+
end
|
81
|
+
|
82
|
+
def canonicalized_request
|
83
|
+
parts = [ request.request_method, request.url, date ]
|
84
|
+
parts += [ request.content_type, content_md5 ] if has_content?
|
85
|
+
parts.join("\n")
|
86
|
+
end
|
87
|
+
|
88
|
+
def access_id
|
89
|
+
@access_id ||= params.split(':').first
|
90
|
+
end
|
91
|
+
|
92
|
+
def secret_key
|
93
|
+
@configuration.hmac_secret_key_for(access_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
def given_digest
|
97
|
+
@given_digest ||= params.split(':').last
|
98
|
+
end
|
99
|
+
|
100
|
+
def calculated_digest
|
101
|
+
@calculated_digest ||= HMAC::SHA1.hexdigest(secret_key, canonicalized_request)
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid?
|
105
|
+
provided? &&
|
106
|
+
secret_key &&
|
107
|
+
valid_current_date? &&
|
108
|
+
calculated_digest == given_digest
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def initialize(app)
|
113
|
+
@configuration = Configuration.new
|
114
|
+
yield @configuration
|
115
|
+
super(app, &@configuration.basic_auth_validation_block)
|
116
|
+
end
|
117
|
+
|
118
|
+
def call(env)
|
119
|
+
auth = Auth.new(env, @configuration)
|
120
|
+
return unauthorized unless auth.provided?
|
121
|
+
return super if auth.basic?
|
122
|
+
return bad_request unless auth.hmac?
|
123
|
+
return bad_request unless auth.has_all_required_parts?
|
124
|
+
return unauthorized unless auth.valid?
|
125
|
+
@app.call(env)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rack/authenticate/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rack-authenticate"
|
7
|
+
s.version = Rack::Authenticate::VERSION
|
8
|
+
s.authors = ["Myron Marston"]
|
9
|
+
s.email = ["myron.marston@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A rack middleware that authenticates requests either using basic auth or via signed HMAC.}
|
12
|
+
s.description = %q{A rack middleware that authenticates requests either using basic auth or via signed HMAC.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "rack-authenticate"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'ruby-hmac', '~> 0.4.0'
|
22
|
+
s.add_development_dependency 'rspec', '~> 2.8.0.rc1'
|
23
|
+
s.add_development_dependency 'rack-test', '~> 0.6.1'
|
24
|
+
s.add_development_dependency 'timecop', '~> 0.3.5'
|
25
|
+
s.add_development_dependency 'rake', '~> 0.9.2.2'
|
26
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'rack/authenticate/client'
|
2
|
+
require 'timecop'
|
3
|
+
|
4
|
+
RSpec.configure do |c|
|
5
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
6
|
+
c.filter_run :f
|
7
|
+
c.run_all_when_everything_filtered = true
|
8
|
+
end
|
9
|
+
|
10
|
+
module Rack
|
11
|
+
module Authenticate
|
12
|
+
describe Client do
|
13
|
+
let(:http_date) { "Tue, 15 Nov 1994 08:12:31 GMT" }
|
14
|
+
let(:base_time) { Time.httpdate(http_date) }
|
15
|
+
around(:each) { |e| Timecop.travel(base_time, &e) }
|
16
|
+
|
17
|
+
let(:access_id) { 'my-access-id' }
|
18
|
+
let(:secret_key) { 'the-s3cr3t' }
|
19
|
+
subject { Client.new(access_id, secret_key) }
|
20
|
+
|
21
|
+
describe "#request_signature_headers" do
|
22
|
+
it 'raises an Argument error if given a content type but not content' do
|
23
|
+
expect {
|
24
|
+
subject.request_signature_headers("get", "http://foo.com/", "text/plain", nil)
|
25
|
+
}.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'raises an Argument error if given a content but no content type' do
|
29
|
+
expect {
|
30
|
+
subject.request_signature_headers("get", "http://foo.com/", nil, "content")
|
31
|
+
}.to raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns the auth header using the HMAC digest' do
|
35
|
+
HMAC::SHA1.stub(:hexdigest => 'the-hex-digest')
|
36
|
+
headers = subject.request_signature_headers("get", "http://foo.com/")
|
37
|
+
headers.should include('Authorization' => "HMAC my-access-id:the-hex-digest")
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'uses the secret key to generate the digest' do
|
41
|
+
HMAC::SHA1.should_receive(:hexdigest).with(secret_key, anything)
|
42
|
+
subject.request_signature_headers("get", "http://foo.com/")
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'uses the uppercased request method in the digest' do
|
46
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
47
|
+
request.split("\n").first.should eq("GET")
|
48
|
+
end
|
49
|
+
|
50
|
+
subject.request_signature_headers("get", "http://foo.com/")
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'handles symbol methods' do
|
54
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
55
|
+
request.split("\n").first.should eq("DELETE")
|
56
|
+
end
|
57
|
+
|
58
|
+
subject.request_signature_headers(:delete, "http://foo.com/")
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'uses the request URL in the digest' do
|
62
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
63
|
+
request.split("\n")[1].should eq("http://foo.com/bar?q=buzz")
|
64
|
+
end
|
65
|
+
|
66
|
+
subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'uses the current http date in the digest' do
|
70
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
71
|
+
request.split("\n")[2].should eq(http_date)
|
72
|
+
end
|
73
|
+
subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'returns the http date in the headers hash' do
|
77
|
+
headers = subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
78
|
+
headers.should include('Date' => http_date)
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when there is no content' do
|
82
|
+
it 'does not use anything beyond the method, url and date for the digest' do
|
83
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
84
|
+
request.split("\n").should have(3).parts
|
85
|
+
end
|
86
|
+
|
87
|
+
subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'does not include a Content-MD5 header in the headers hash' do
|
91
|
+
headers = subject.request_signature_headers("get", "http://foo.com/bar?q=buzz")
|
92
|
+
headers.should_not have_key('Content-MD5')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when there is content' do
|
97
|
+
let(:content_md5) { 'the-content-md5' }
|
98
|
+
before(:each) do
|
99
|
+
Digest::MD5.stub(:hexdigest).and_return(content_md5)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns the Content-MD5 header in the headers hash' do
|
103
|
+
headers = subject.request_signature_headers("get", "http://foo.com/bar?q=buzz", "text/plain", "content")
|
104
|
+
headers.should include('Content-MD5' => content_md5)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'generates the Content-MD5 based on the content' do
|
108
|
+
Digest::MD5.should_receive(:hexdigest).with("content")
|
109
|
+
subject.request_signature_headers("get", "http://foo.com/bar?q=buzz", "text/plain", "content")
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'uses the content type and content md5 in the digest' do
|
113
|
+
HMAC::SHA1.should_receive(:hexdigest) do |key, request|
|
114
|
+
parts = request.split("\n")
|
115
|
+
parts.should have(5).parts
|
116
|
+
parts.last(2).should eq(['text/plain', 'the-content-md5'])
|
117
|
+
end
|
118
|
+
|
119
|
+
subject.request_signature_headers("get", "http://foo.com/bar?q=buzz", "text/plain", "content")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'timecop'
|
2
|
+
require 'rack/authenticate/middleware'
|
3
|
+
require 'rack/authenticate/client'
|
4
|
+
require 'rack/test'
|
5
|
+
|
6
|
+
RSpec.configure do |c|
|
7
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
8
|
+
c.filter_run :f
|
9
|
+
c.run_all_when_everything_filtered = true
|
10
|
+
end
|
11
|
+
|
12
|
+
class Integer
|
13
|
+
def minutes
|
14
|
+
self * 60
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Rack
|
19
|
+
module Authenticate
|
20
|
+
class Middleware
|
21
|
+
shared_context 'http_date' do
|
22
|
+
let(:http_date) { "Tue, 15 Nov 1994 08:12:31 GMT" }
|
23
|
+
let(:base_time) { Time.httpdate(http_date) }
|
24
|
+
around(:each) do |example|
|
25
|
+
if example.metadata[:no_timecop]
|
26
|
+
example.run
|
27
|
+
else
|
28
|
+
Timecop.travel(base_time, &example)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe Auth do
|
34
|
+
include_context 'http_date'
|
35
|
+
let(:content_md5) { 'some-long-md5' }
|
36
|
+
let(:basic_env) do {
|
37
|
+
"HTTP_HOST" => "example.org",
|
38
|
+
"SERVER_NAME" => "example.org",
|
39
|
+
"CONTENT_LENGTH" => "0",
|
40
|
+
"rack.url_scheme" => "http",
|
41
|
+
"HTTPS" => "off",
|
42
|
+
"PATH_INFO" => "/foo/bar",
|
43
|
+
"SERVER_PORT" => "80",
|
44
|
+
"REQUEST_METHOD" => "GET",
|
45
|
+
"QUERY_STRING" => "",
|
46
|
+
"HTTP_DATE" => http_date,
|
47
|
+
"rack.input" => StringIO.new("")
|
48
|
+
} end
|
49
|
+
|
50
|
+
describe "#basic?" do
|
51
|
+
it 'returns true if given a basic auth header' do
|
52
|
+
basic_env['HTTP_AUTHORIZATION'] = 'BASIC abc:asfkj23asdfkj'
|
53
|
+
Auth.new(basic_env).should be_basic
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns false if given an hmac auth header' do
|
57
|
+
basic_env['HTTP_AUTHORIZATION'] = 'HMAC abc:asfkj23asdfkj'
|
58
|
+
Auth.new(basic_env).should_not be_basic
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#hmac?" do
|
63
|
+
it 'returns true if given an hmac auth header' do
|
64
|
+
basic_env['HTTP_AUTHORIZATION'] = 'HMAC abc:asfkj23asdfkj'
|
65
|
+
Auth.new(basic_env).should be_hmac
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns false if given a basic auth header' do
|
69
|
+
basic_env['HTTP_AUTHORIZATION'] = 'BASIC abc:asfkj23asdfkj'
|
70
|
+
Auth.new(basic_env).should_not be_hmac
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#canonicalized_request" do
|
75
|
+
it 'combines the HTTP verb, the date and the Request URI' do
|
76
|
+
Auth.new(basic_env).canonicalized_request.split("\n").should eq([
|
77
|
+
'GET',
|
78
|
+
'http://example.org/foo/bar',
|
79
|
+
http_date
|
80
|
+
])
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'does not blow up if there is no date' do
|
84
|
+
basic_env.delete('HTTP_DATE')
|
85
|
+
Auth.new(basic_env).canonicalized_request
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'includes the content MD5 and Type when they are present' do
|
89
|
+
basic_env['CONTENT_LENGTH'] = '10'
|
90
|
+
basic_env['HTTP_CONTENT_MD5'] = content_md5
|
91
|
+
basic_env['CONTENT_TYPE'] = 'text/plain'
|
92
|
+
|
93
|
+
Auth.new(basic_env).canonicalized_request.split("\n").should eq([
|
94
|
+
'GET',
|
95
|
+
'http://example.org/foo/bar',
|
96
|
+
http_date,
|
97
|
+
'text/plain',
|
98
|
+
content_md5
|
99
|
+
])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#valid_current_date?" do
|
104
|
+
it 'returns false if the date is not in the correct format' do
|
105
|
+
basic_env['HTTP_DATE'] = 'some time yesterday'
|
106
|
+
auth = Auth.new(basic_env, stub(:timestamp_minute_tolerance => 10))
|
107
|
+
auth.should_not be_valid_current_date
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns false if the date is outside the configured tolerance' do
|
111
|
+
auth = Auth.new(basic_env, stub(:timestamp_minute_tolerance => 10))
|
112
|
+
auth.should be_valid_current_date
|
113
|
+
|
114
|
+
Timecop.freeze(base_time - 11.minutes) do
|
115
|
+
auth.should_not be_valid_current_date
|
116
|
+
end
|
117
|
+
|
118
|
+
Timecop.freeze(base_time + 11.minutes) do
|
119
|
+
auth.should_not be_valid_current_date
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#has_all_required_parts?" do
|
125
|
+
subject { Auth.new(env) }
|
126
|
+
|
127
|
+
context 'for a request with no body' do
|
128
|
+
let(:env) { basic_env }
|
129
|
+
|
130
|
+
it 'returns true if it has everything it needs' do
|
131
|
+
should have_all_required_parts
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns false if it lacks the Date header' do
|
135
|
+
basic_env.delete('HTTP_DATE')
|
136
|
+
should_not have_all_required_parts
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'for a request with a body' do
|
141
|
+
let(:env) { basic_env.merge('CONTENT_LENGTH' => '10') }
|
142
|
+
|
143
|
+
it 'returns true if it has a content type and content MD5' do
|
144
|
+
basic_env['HTTP_CONTENT_MD5'] = content_md5
|
145
|
+
basic_env['CONTENT_TYPE'] = 'text/plain'
|
146
|
+
should have_all_required_parts
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'returns false if it lacks the content md5 header' do
|
150
|
+
basic_env['CONTENT_TYPE'] = 'text/plain'
|
151
|
+
should_not have_all_required_parts
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'returns false if it lacks the content type header' do
|
155
|
+
basic_env['HTTP_CONTENT_MD5'] = content_md5
|
156
|
+
should_not have_all_required_parts
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#access_id" do
|
162
|
+
it 'extracts it from the Auth header' do
|
163
|
+
basic_env['HTTP_AUTHORIZATION'] = 'HMAC abc:asfkj23asdfkj'
|
164
|
+
Auth.new(basic_env).access_id.should eq('abc')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe "#secret_key" do
|
169
|
+
it 'finds the key matching the given access id from the configured creds' do
|
170
|
+
basic_env['HTTP_AUTHORIZATION'] = 'HMAC abc:asfkj23asdfkj'
|
171
|
+
configuration = Configuration.new
|
172
|
+
configuration.hmac_secret_key do |access_id|
|
173
|
+
{ 'def' => '123456', 'abc' => '654321' }[access_id]
|
174
|
+
end
|
175
|
+
auth = Auth.new(basic_env, configuration)
|
176
|
+
auth.secret_key.should eq('654321')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe "#given_digest" do
|
181
|
+
it 'extracts it from the Auth header' do
|
182
|
+
basic_env['HTTP_AUTHORIZATION'] = 'HMAC abc:asfkj23asdfkj'
|
183
|
+
Auth.new(basic_env).given_digest.should eq('asfkj23asdfkj')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#calculated_digest" do
|
188
|
+
it 'calculates the digest using the secret key, the canonicalized request and HMAC-SHA1' do
|
189
|
+
digest = 'e593edc35cc753591052923c39ce6981330a4f13'
|
190
|
+
HMAC::SHA1.hexdigest('the-key', 'canonicalized-request').should eq(digest)
|
191
|
+
auth = Auth.new(basic_env)
|
192
|
+
auth.stub(:secret_key => 'the-key', :canonicalized_request => 'canonicalized-request')
|
193
|
+
auth.calculated_digest.should eq(digest)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#valid?" do
|
198
|
+
let(:configuration) do
|
199
|
+
Configuration.new.tap do |c|
|
200
|
+
c.timestamp_minute_tolerance = 10
|
201
|
+
c.hmac_secret_key { |id| { 'abc' => '123' }[id] }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
let(:access_id) { 'abc' }
|
206
|
+
let(:digest) { '2baf72a8a52e1cfec37f588c5b4e0914cb4f63b5' }
|
207
|
+
let(:env) { basic_env.merge('HTTP_AUTHORIZATION' => "HMAC #{access_id}:#{digest}") }
|
208
|
+
let(:auth) { Auth.new(env, configuration) }
|
209
|
+
|
210
|
+
it 'returns true if the calculated digest matches the given digest' do
|
211
|
+
auth.should be_valid
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'returns false if the digests do not match' do
|
215
|
+
digest.gsub!('7', '6')
|
216
|
+
auth.should_not be_valid
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'returns false if no secret key can be found for the given access id' do
|
220
|
+
access_id.gsub!('a', '1')
|
221
|
+
auth.should_not be_valid
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'returns false if there is no given credential' do
|
225
|
+
env.delete('HTTP_AUTHORIZATION')
|
226
|
+
auth.should_not be_valid
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'returns false if the date is not in a valid range' do
|
230
|
+
Timecop.freeze(base_time + 12.minutes) do
|
231
|
+
auth.should_not be_valid
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe self do
|
238
|
+
include_context 'http_date'
|
239
|
+
include Rack::Test::Methods
|
240
|
+
|
241
|
+
let(:hmac_auth_creds) do {
|
242
|
+
'abc' => '123',
|
243
|
+
'def' => '456'
|
244
|
+
} end
|
245
|
+
|
246
|
+
let(:basic_auth_creds) do {
|
247
|
+
'abc' => 'foo',
|
248
|
+
'def' => 'bar'
|
249
|
+
} end
|
250
|
+
|
251
|
+
def basis_auth_value(username, password)
|
252
|
+
["#{username}:#{password}"].pack("m*")
|
253
|
+
end
|
254
|
+
|
255
|
+
let(:app) do
|
256
|
+
hmac_creds = hmac_auth_creds
|
257
|
+
basic_creds = basic_auth_creds
|
258
|
+
|
259
|
+
Rack::Builder.new do
|
260
|
+
use Rack::ContentLength
|
261
|
+
use Rack::Authenticate::Middleware do |config|
|
262
|
+
config.hmac_secret_key { |access_id| hmac_creds[access_id] }
|
263
|
+
config.basic_auth_validation { |u, p| basic_creds[u] == p }
|
264
|
+
config.timestamp_minute_tolerance = 30
|
265
|
+
end
|
266
|
+
|
267
|
+
run lambda { |env| [200, {}, ['OK']] }
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'responds with a 401 if there are no headers at all' do
|
272
|
+
get '/'
|
273
|
+
last_response.status.should eq(401)
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'responds with a 400 when the request is missing required information for HMAC authorization' do
|
277
|
+
# no date header set...
|
278
|
+
header 'Authorization', 'HMAC abc:adfafdsfdas'
|
279
|
+
get '/'
|
280
|
+
last_response.status.should eq(400)
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'responds with a 400 when given an unrecognized type of authorization' do
|
284
|
+
header 'Date', "Tue, 15 Nov 1994 08:12:31 GMT"
|
285
|
+
header 'Authorization', 'DIGEST abc:adfafdsfdas'
|
286
|
+
get '/'
|
287
|
+
last_response.status.should eq(400)
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'responds with a 401 when there is no authorization header' do
|
291
|
+
header 'Date', "Tue, 15 Nov 1994 08:12:31 GMT"
|
292
|
+
get '/'
|
293
|
+
last_response.status.should eq(401)
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'responds with a 401 when there is an HMAC authorization header but it is invalid' do
|
297
|
+
header 'Authorization', 'HMAC abc:asfkj23asdfkj'
|
298
|
+
header 'Date', "Tue, 15 Nov 1994 08:12:31 GMT"
|
299
|
+
get '/'
|
300
|
+
last_response.status.should eq(401)
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'lets the request through when there is a valid HMAC authorization header' do
|
304
|
+
header 'Authorization', 'HMAC abc:34a70d9901bd447a02157f9fc598e43d6bf5b484'
|
305
|
+
header 'Date', http_date
|
306
|
+
get '/'
|
307
|
+
last_response.status.should eq(200)
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'lets the request through when there is a valid Basic authorization header' do
|
311
|
+
header 'Authorization', "BASIC #{basis_auth_value('abc', 'foo')}"
|
312
|
+
get '/'
|
313
|
+
last_response.status.should eq(200)
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'responds with a 401 when there is a BASIC authorization header but it is invalid' do
|
317
|
+
header 'Authorization', "BASIC #{basis_auth_value('abc', 'foot')}"
|
318
|
+
get '/'
|
319
|
+
last_response.status.should eq(401)
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'generates the same signature as the client', :no_timecop do
|
323
|
+
client = Client.new('abc', hmac_auth_creds['abc'])
|
324
|
+
client.request_signature_headers('post', 'http://example.org/foo', 'text/plain', "some content").each do |key, value|
|
325
|
+
header key, value
|
326
|
+
end
|
327
|
+
|
328
|
+
header 'Content-Type', 'text/plain'
|
329
|
+
post '/foo', "some content"
|
330
|
+
last_response.status.should eq(200)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rack/authenticate'
|
2
|
+
|
3
|
+
RSpec.configure do |c|
|
4
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
5
|
+
c.filter_run :f
|
6
|
+
c.run_all_when_everything_filtered = true
|
7
|
+
end
|
8
|
+
|
9
|
+
module Rack
|
10
|
+
describe Authenticate do
|
11
|
+
describe "#new_secret_key" do
|
12
|
+
it "generates a long random string" do
|
13
|
+
Rack::Authenticate.new_secret_key.should match(/[A-Za-z0-9\\\/]{60,}/)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-authenticate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Myron Marston
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-14 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ruby-hmac
|
16
|
+
requirement: &2165123900 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.4.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2165123900
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &2165122980 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.8.0.rc1
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2165122980
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rack-test
|
38
|
+
requirement: &2165121800 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.6.1
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2165121800
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: timecop
|
49
|
+
requirement: &2165119880 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.3.5
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2165119880
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
requirement: &2165118740 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 0.9.2.2
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2165118740
|
69
|
+
description: A rack middleware that authenticates requests either using basic auth
|
70
|
+
or via signed HMAC.
|
71
|
+
email:
|
72
|
+
- myron.marston@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- .rspec
|
79
|
+
- .travis.yml
|
80
|
+
- Appraisals
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE
|
83
|
+
- Rakefile
|
84
|
+
- gemfiles/rack-1.1.gemfile
|
85
|
+
- gemfiles/rack-1.1.gemfile.lock
|
86
|
+
- gemfiles/rack-1.2.gemfile
|
87
|
+
- gemfiles/rack-1.2.gemfile.lock
|
88
|
+
- gemfiles/rack-1.3.gemfile
|
89
|
+
- gemfiles/rack-1.3.gemfile.lock
|
90
|
+
- lib/rack-authenticate.rb
|
91
|
+
- lib/rack/authenticate.rb
|
92
|
+
- lib/rack/authenticate/client.rb
|
93
|
+
- lib/rack/authenticate/middleware.rb
|
94
|
+
- lib/rack/authenticate/version.rb
|
95
|
+
- rack-authenticate.gemspec
|
96
|
+
- spec/rack/authenticate/client_spec.rb
|
97
|
+
- spec/rack/authenticate/middleware_spec.rb
|
98
|
+
- spec/rack/authenticate_spec.rb
|
99
|
+
homepage: ''
|
100
|
+
licenses: []
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
hash: 3915972377908680387
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
hash: 3915972377908680387
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project: rack-authenticate
|
125
|
+
rubygems_version: 1.8.6
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: A rack middleware that authenticates requests either using basic auth or
|
129
|
+
via signed HMAC.
|
130
|
+
test_files:
|
131
|
+
- spec/rack/authenticate/client_spec.rb
|
132
|
+
- spec/rack/authenticate/middleware_spec.rb
|
133
|
+
- spec/rack/authenticate_spec.rb
|