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