rack-authenticate 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|