rack-authenticate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --order random
@@ -0,0 +1,11 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ree
6
+ - rbx-18mode
7
+ - jruby
8
+ gemfile:
9
+ - gemfiles/rack-1.1.gemfile
10
+ - gemfiles/rack-1.2.gemfile
11
+ - gemfiles/rack-1.3.gemfile
@@ -0,0 +1,11 @@
1
+ appraise "rack-1.1" do
2
+ gem 'rack', '~> 1.1.2'
3
+ end
4
+
5
+ appraise "rack-1.2" do
6
+ gem 'rack', '~> 1.2.4'
7
+ end
8
+
9
+ appraise "rack-1.3" do
10
+ gem 'rack', '~> 1.3.5'
11
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rack-authenticate.gemspec
4
+ gemspec
5
+ gem 'rack'
6
+ gem 'appraisal'
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.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require "rspec/core/rake_task"
3
+ require 'appraisal'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rack", "~> 1.1.2"
7
+
8
+ gemspec :path=>"../"
@@ -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,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rack", "~> 1.2.4"
7
+
8
+ gemspec :path=>"../"
@@ -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,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rack", "~> 1.3.5"
7
+
8
+ gemspec :path=>"../"
@@ -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,11 @@
1
+ module Rack
2
+ module Authenticate
3
+ def self.new_secret_key
4
+ require 'base64'
5
+ require 'securerandom'
6
+ require 'digest/sha2'
7
+ Base64.encode64(Digest::SHA2.new(512).digest(SecureRandom.random_bytes(512))).chomp
8
+ end
9
+ end
10
+ end
11
+
@@ -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,5 @@
1
+ module Rack
2
+ module Authenticate
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -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