lockbox_middleware 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2010, Democratic National Committee
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the Democratic National Committee nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ = LockBox
2
+
3
+ LockBox is a centralized API authentication service written by the DNC Innovation Lab. It lets your API
4
+ users share a single identity across multiple services.
5
+ It is licensed under the New BSD License (see the LICENSE file for details).
6
+
7
+ It is a Ruby on Rails application on the server side, and Rack middleware on the client side
8
+ (which means it integrates nicely with any modern Ruby web framework). As of v1.2.0, there is an
9
+ unfortunate Rails dependency in the middleware gem. Hopefully we'll get rid of that soon.
10
+
11
+ Lockbox handles things like rate limiting, API key signup and management, and supports HMAC
12
+ authentication as well as plain-text key exchange. We are working on replacing HMAC with OAuth 2.0.
13
+
14
+ == Configuration
15
+
16
+ LockBox needs a configuration file named "lockbox.yml" in order to work. In a Rack app (incl. Rails),
17
+ this file should be placed in app_root/config/lockbox.yml.
18
+
19
+ You should define (for each of your environments), the base_uri of your app and the relative paths you want
20
+ to protect with LockBox.
21
+
22
+ Here's an example lockbox.yml:
23
+
24
+ production:
25
+ base_uri: http://lockbox.foo.org
26
+ development:
27
+ base_uri: http://localhost:3001
28
+ cucumber:
29
+ base_uri: http://localhost:3001
30
+ test:
31
+ base_uri: http://localhost:3001
32
+ all:
33
+ protect_paths:
34
+ - ^/api/
35
+
36
+ == Download
37
+
38
+ Github: http://github.com/dnclabs/lockbox/tree/master
39
+
40
+ == Authors
41
+
42
+ - Nathan Woodhull
43
+ - Chris Gill
44
+ - Brian Cardarella
45
+ - Wes Morgan
46
+
47
+ Copyright 2010 Democratic National Committee,
48
+ All Rights Reserved.
@@ -0,0 +1,48 @@
1
+ require 'forwardable'
2
+
3
+ module LockBoxCache
4
+ class Cache
5
+ extend Forwardable
6
+ def_delegators :@cache, :write, :read, :delete
7
+
8
+ class RailsCache
9
+ def write(key, value)
10
+ Rails.cache.write(key, value)
11
+ end
12
+
13
+ def read(key)
14
+ Rails.cache.read(key)
15
+ end
16
+
17
+ def delete(key)
18
+ Rails.cache.delete(key)
19
+ end
20
+ end
21
+
22
+ class HashCache
23
+ def initialize
24
+ @store = Hash.new
25
+ end
26
+
27
+ def write(key, value)
28
+ @store[key] = value
29
+ end
30
+
31
+ def read(key)
32
+ @store[key]
33
+ end
34
+
35
+ def delete(key)
36
+ @store.delete(key)
37
+ end
38
+ end
39
+
40
+ def initialize
41
+ if defined?(Rails)
42
+ @cache = RailsCache.new
43
+ else
44
+ @cache = HashCache.new
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,156 @@
1
+ require 'httparty'
2
+ require 'lockbox_cache'
3
+ require 'auth-hmac'
4
+
5
+ class LockBox
6
+ include HTTParty
7
+ include LockBoxCache
8
+
9
+ def self.config
10
+ if defined?(Rails)
11
+ root_dir = Rails.root
12
+ else
13
+ root_dir = '.'
14
+ end
15
+ yaml_config = YAML.load_file(File.join(root_dir,'config','lockbox.yml'))
16
+ return_config = {}
17
+ environment = Rails.env if defined? Rails
18
+ environment ||= ENV['RACK_ENV']
19
+ environment ||= 'test'
20
+ if !environment.nil?
21
+ if !yaml_config['all'].nil?
22
+ return_config = yaml_config['all']
23
+ return_config.merge!(yaml_config[environment])
24
+ else
25
+ return_config = yaml_config[environment]
26
+ end
27
+ end
28
+ return_config
29
+ end
30
+
31
+ base_uri config['base_uri']
32
+
33
+ def initialize(app)
34
+ @app = app
35
+ @cache = LockBoxCache::Cache.new
36
+ end
37
+
38
+ def call(env)
39
+ dup.call!(env)
40
+ end
41
+
42
+ def protected_paths
43
+ self.class.config['protect_paths'].map do |path|
44
+ Regexp.new(path)
45
+ end
46
+ end
47
+
48
+ def call!(env)
49
+ #attempt to authenticate any requests to /api
50
+ request = Rack::Request.new(env)
51
+ path_protected = false
52
+ protected_paths.each do |path|
53
+ if env['PATH_INFO'] =~ path
54
+ path_protected = true
55
+ authorized = false
56
+ key = request['key']
57
+ if key.blank?
58
+ key = 'hmac'
59
+ end
60
+
61
+ auth = auth_response(key,env)
62
+ authorized = auth[:authorized]
63
+ auth_headers = auth[:headers]
64
+
65
+ if authorized
66
+ app_response = @app.call(env)
67
+ app_headers = app_response[1]
68
+ response_headers = app_headers.merge(auth_headers)
69
+ return [app_response[0], response_headers, app_response[2]]
70
+ else
71
+ message = "Access Denied"
72
+ return [401, {'Content-Type' => 'text/plain', 'Content-Length' => "#{message.length}"}, message]
73
+ end
74
+ end
75
+ end
76
+ unless path_protected
77
+ #pass everything else straight through to app
78
+ return @app.call(env)
79
+ end
80
+ end
81
+
82
+ def auth_response(api_key, env={})
83
+ if api_key != 'hmac'
84
+ cached_auth = auth_cache(api_key)
85
+ if !cached_auth.nil?
86
+ # currently we don't cache forward headers
87
+ return {:authorized => cached_auth, :headers => {}}
88
+ end
89
+ end
90
+ auth_response = self.class.get("/authentication/#{api_key}", {:headers => auth_headers(env)})
91
+ authorized = (auth_response.code == 200)
92
+ cache_response_if_allowed(api_key, auth_response) if authorized
93
+ {:authorized => authorized, :headers => response_headers(auth_response)}
94
+ end
95
+
96
+ private
97
+
98
+ def cache_response_if_allowed(api_key, auth_response)
99
+ cache_control = auth_response.headers['Cache-Control'].split(/,\s*/)
100
+ cache_max_age = 0
101
+ cache_public = false
102
+ cache_control.each do |c|
103
+ if c =~ /^max-age=\s*(\d+)$/
104
+ cache_max_age = $1.to_i
105
+ elsif c == 'public'
106
+ cache_public = true
107
+ end
108
+ end
109
+ caching_allowed = (cache_max_age > 0 && cache_public)
110
+ expiration = cache_max_age.seconds.since(Time.now)
111
+ cache_auth(api_key,expiration) if caching_allowed
112
+ end
113
+
114
+ def response_headers(auth_response)
115
+ headers = {}
116
+ auth_response.headers.each_pair do |h,v|
117
+ if h =~ /^X-RateLimit-/
118
+ headers[h] = v
119
+ elsif h =~ /^X-LockBox-/
120
+ headers[h] = v
121
+ end
122
+ end
123
+ headers
124
+ end
125
+
126
+ def auth_headers(env)
127
+ headers = {}
128
+ headers['Referer'] = "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}#{env['PATH_INFO']}"
129
+ headers['Referer'] << "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].blank?
130
+ {'Content-Type' => 'Content-Type', 'Content-MD5' => 'Content-MD5', 'Date' => 'HTTP_DATE', 'Method' => 'REQUEST_METHOD', 'Authorization' => 'HTTP_AUTHORIZATION'}.each_pair do |h,e|
131
+ headers["X-Referer-#{h}"] = env[e] unless env[e].blank?
132
+ end
133
+ headers
134
+ end
135
+
136
+ def cache_key(api_key)
137
+ "lockbox_#{api_key}"
138
+ end
139
+
140
+ def auth_cache(api_key)
141
+ expiration = @cache.read(cache_key(api_key))
142
+ return nil if expiration.nil?
143
+ expiration = Time.at(expiration)
144
+ if expiration <= Time.now
145
+ @cache.delete(cache_key(api_key))
146
+ nil
147
+ elsif expiration > Time.now
148
+ true
149
+ end
150
+ end
151
+
152
+ def cache_auth(api_key,expiration)
153
+ @cache.write(cache_key(api_key),expiration.to_i)
154
+ end
155
+
156
+ end
@@ -0,0 +1,229 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+ require 'lockbox_middleware'
4
+
5
+ describe 'LockBox' do
6
+ include Rack::Test::Methods
7
+
8
+ def app
9
+ # Initialize our LockBox middleware with an "app" that just always returns 200, if it gets .called
10
+ LockBox.new(Proc.new {|env| [200,{},"successfully hit rails app"]})
11
+ end
12
+
13
+ def safely_edit_config_file(settings, env=nil)
14
+ env ||= Rails.env if defined?(Rails)
15
+ env ||= ENV['RACK_ENV']
16
+ env ||= 'test'
17
+ @config_file = File.join(File.dirname(__FILE__),'..','..','config','lockbox.yml')
18
+ @tmp_config_file = "#{@config_file}.testing"
19
+ FileUtils.cp(@config_file, @tmp_config_file)
20
+ config = YAML.load_file(@config_file)
21
+ settings.each_pair do |setting,value|
22
+ config[env][setting.to_s] = value
23
+ end
24
+ File.open( @config_file, 'w' ) do |out|
25
+ YAML.dump( config, out )
26
+ end
27
+ end
28
+
29
+ context "setting the base_uri" do
30
+ let(:base_uri) { "http://localhost:3001" }
31
+
32
+ it "should use the base_uri specified in the config" do
33
+ safely_edit_config_file({:base_uri => base_uri})
34
+ LockBox.base_uri.should == base_uri
35
+ end
36
+
37
+ after :each do
38
+ if @tmp_config_file && @config_file
39
+ FileUtils.mv(@tmp_config_file, @config_file)
40
+ end
41
+ end
42
+ end
43
+
44
+ context "setting the protected paths" do
45
+ let(:path1) { "^/api/" }
46
+ let(:path2) { "^/foo/bar/" }
47
+ let(:path3) { "/lookup/?$" }
48
+
49
+ before :each do
50
+ safely_edit_config_file({:protect_paths => [path1, path2, path3]}, 'all')
51
+ successful_response = mock("MockResponse")
52
+ successful_response.stubs(:code).returns(200)
53
+ successful_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
54
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
55
+ bad_response = mock("MockResponse")
56
+ bad_response.stubs(:code).returns(401)
57
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
58
+ LockBox.stubs(:get).with("/authentication/invalid", any_parameters).returns(bad_response)
59
+ end
60
+
61
+ it "should protect path1" do
62
+ get "/api/foo?key=invalid"
63
+ last_response.status.should == 401
64
+ get "/api/foo?key=123456"
65
+ last_response.status.should == 200
66
+ end
67
+
68
+ it "should protect path2" do
69
+ get "/foo/bar/baz?key=invalid"
70
+ last_response.status.should == 401
71
+ get "/foo/bar/baz?key=123456"
72
+ last_response.status.should == 200
73
+ end
74
+
75
+ it "should protect path3" do
76
+ get "/polling_place/lookup?key=invalid"
77
+ last_response.status.should == 401
78
+ get "/polling_place/lookup?key=123456"
79
+ last_response.status.should == 200
80
+ end
81
+
82
+ it "should not protect other paths" do
83
+ get "/bar/baz"
84
+ last_response.status.should == 200
85
+ last_response.body.should == "successfully hit rails app"
86
+ end
87
+
88
+ after :each do
89
+ if @tmp_config_file && @config_file
90
+ FileUtils.mv(@tmp_config_file, @config_file)
91
+ end
92
+ end
93
+ end
94
+
95
+ context "hitting API actions" do
96
+ before :each do
97
+ @max_age = 3600
98
+ successful_response = mock("MockResponse")
99
+ successful_response.stubs(:code).returns(200)
100
+ successful_response.stubs(:headers).returns({'Cache-Control' => "public,max-age=#{@max_age},must-revalidate"})
101
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
102
+ bad_response = mock("MockResponse")
103
+ bad_response.stubs(:code).returns(401)
104
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
105
+ LockBox.stubs(:get).with("/authentication/blah", any_parameters).returns(bad_response)
106
+ end
107
+
108
+ it "should return 401 for a request that starts with /api with invalid api key" do
109
+ get "/api/some_controller/some_action?key=blah"
110
+ last_response.status.should == 401
111
+ end
112
+
113
+ it "should return 200 for a request that starts with /api and has api key" do
114
+ get "/api/some_controller/some_action?key=123456"
115
+ last_response.status.should == 200
116
+ end
117
+
118
+ it "should cache lockbox responses for max-age when Cache-Control allows it" do
119
+ get "/api/some_controller/some_action?key=123456"
120
+ last_response.status.should == 200
121
+ bad_response = mock("MockResponse")
122
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
123
+ bad_response.stubs(:code).returns(401)
124
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
125
+ get "/api/some_controller/some_action?key=123456"
126
+ last_response.status.should == 200
127
+ end
128
+
129
+ it "should expire cached lockbox responses when max-age seconds have passed" do
130
+ get "/api/some_controller/some_action?key=123456"
131
+ last_response.status.should == 200
132
+ bad_response = mock("MockResponse")
133
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
134
+ bad_response.stubs(:code).returns(401)
135
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
136
+ expired_time = @max_age.seconds.since(Time.now)
137
+ Time.stubs(:now).returns(expired_time)
138
+ get "/api/some_controller/some_action?key=123456"
139
+ last_response.status.should == 401
140
+ end
141
+
142
+ it "should not cache lockbox responses when Cache-Control does not allow it" do
143
+ successful_response = mock("MockResponse")
144
+ successful_response.stubs(:code).returns(200)
145
+ successful_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
146
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
147
+ get "/api/some_controller/some_action?key=123456"
148
+ last_response.status.should == 200
149
+ bad_response = mock("MockResponse")
150
+ bad_response.stubs(:code).returns(401)
151
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
152
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
153
+ get "/api/some_controller/some_action?key=123456"
154
+ last_response.status.should == 401
155
+ end
156
+
157
+ it "should pass along the rate limit headers to the client if they exist" do
158
+ successful_response = mock("MockResponse")
159
+ successful_response.stubs(:code).returns(200)
160
+ headers = {
161
+ 'X-RateLimit-Limit' => '100',
162
+ 'X-RateLimit-Remaining' => '99',
163
+ 'X-RateLimit-Reset' => 1.hour.from_now.to_i.to_s
164
+ }
165
+ successful_response.stubs(:headers).returns(headers.merge({'Cache-Control' => 'public,no-cache'}))
166
+ LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
167
+ get "/api/some_controller/some_action?key=123456"
168
+ headers.each_pair do |header,value|
169
+ # just tests that the headers are present; the stubs above ensure the values are what we expect
170
+ last_response.headers[header].should == value
171
+ end
172
+ end
173
+
174
+ end
175
+
176
+ context "hitting API actions with HMAC auth" do
177
+ before :each do
178
+ successful_response = mock("MockResponse")
179
+ successful_response.stubs(:code).returns(200)
180
+ successful_response.stubs(:headers).returns({'Cache-Control' => 'public, no-cache'})
181
+ Time.stubs(:now).returns(Time.parse("2010-05-10 16:30:00 EDT"))
182
+ valid_headers = {'X-Referer-Method' => 'GET', 'X-Referer-Date' => [Time.now.httpdate], 'X-Referer-Authorization' => ['AuthHMAC key-id:uxx+EgyzWBKBgS+Y8MzpcWcfy7k='], 'Referer' => 'http://example.org/api/some_controller/some_action'}
183
+ LockBox.stubs(:get).with("/authentication/hmac", {:headers => valid_headers}).returns(successful_response)
184
+
185
+ bad_response = mock("MockResponse")
186
+ bad_response.stubs(:code).returns(401)
187
+ bad_response.stubs(:headers).returns({'Cache-Control' => 'public, no-cache'})
188
+ invalid_headers = {'X-Referer-Method' => 'GET', 'X-Referer-Date' => [Time.now.httpdate], 'X-Referer-Authorization' => 'foo', 'Referer' => 'http://example.org/api/some_controller/some_action'}
189
+ LockBox.stubs(:get).with("/authentication/hmac", {:headers => invalid_headers}).returns(bad_response)
190
+
191
+ @path = "/api/some_controller/some_action"
192
+
193
+ hmac_request = Net::HTTP::Get.new(@path, {'Date' => Time.now.httpdate})
194
+ store = mock("MockStore")
195
+ store.stubs(:[]).with('key-id').returns("123456")
196
+ authhmac = AuthHMAC.new(store)
197
+ authhmac.sign!(hmac_request, 'key-id')
198
+ @hmac_headers = hmac_request.to_hash
199
+ end
200
+
201
+ it "should return 200 for an HMAC request with a valid auth header" do
202
+ @hmac_headers.each_pair do |key,value|
203
+ header key, value
204
+ end
205
+ get @path
206
+ last_response.status.should == 200
207
+ end
208
+
209
+ it "should return 401 for an HMAC request with an invalid auth header" do
210
+ @hmac_headers['authorization'] = 'foo'
211
+ @hmac_headers.each_pair do |key,value|
212
+ header key, value
213
+ end
214
+ get @path
215
+ last_response.status.should == 401
216
+ end
217
+ end
218
+
219
+ context "hitting actions without API" do
220
+
221
+ it "should not try to authenticate a request that doesn't start with /api" do
222
+ get "/"
223
+ last_response.status.should == 200
224
+ last_response.body.should == "successfully hit rails app"
225
+ end
226
+
227
+ end
228
+
229
+ end
@@ -0,0 +1,27 @@
1
+ # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2
+ # from the project root directory.
3
+ ENV["RAILS_ENV"] ||= 'test'
4
+ env_file = File.expand_path(File.join(File.dirname(__FILE__),'..','config','environment'))
5
+ if File.exists?("#{env_file}.rb")
6
+ require env_file
7
+ require 'spec/autorun'
8
+ require 'spec/rails'
9
+ require 'authlogic/test_case'
10
+ else
11
+ require 'rubygems'
12
+ require 'mocha' # gem install jferris-mocha, not regular mocha
13
+ end
14
+
15
+ # Requires supporting files with custom matchers and macros, etc,
16
+ # in ./support/ and its subdirectories.
17
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
18
+
19
+ Spec::Runner.configure do |config|
20
+ if defined?(Rails)
21
+ config.use_transactional_fixtures = true
22
+ config.use_instantiated_fixtures = false
23
+ config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
24
+ config.include(Authlogic::TestCase)
25
+ end
26
+ config.mock_with Mocha::API
27
+ end
@@ -0,0 +1,13 @@
1
+ module Mocha
2
+ module API
3
+ def setup_mocks_for_rspec
4
+ mocha_setup
5
+ end
6
+ def verify_mocks_for_rspec
7
+ mocha_verify
8
+ end
9
+ def teardown_mocks_for_rspec
10
+ mocha_teardown
11
+ end
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lockbox_middleware
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 2
8
+ - 0
9
+ version: 1.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Chris Gill
13
+ - Brian Cardarella
14
+ - Nathan Woodhull
15
+ - Wes Morgan
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2010-06-15 00:00:00 -04:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: httparty
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ segments:
31
+ - 0
32
+ - 5
33
+ - 2
34
+ version: 0.5.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: auth-hmac
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rack
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 1
59
+ - 0
60
+ version: 1.1.0
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ description: Rack middleware for the Lockbox centralized API authorization service. Brought to you by the DNC Innovation Lab.
64
+ email: innovationlab@dnc.org
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files:
70
+ - LICENSE
71
+ - README.rdoc
72
+ files:
73
+ - lib/lockbox_cache.rb
74
+ - lib/lockbox_middleware.rb
75
+ - LICENSE
76
+ - README.rdoc
77
+ has_rdoc: true
78
+ homepage:
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --charset=UTF-8
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ requirements: []
101
+
102
+ rubyforge_project:
103
+ rubygems_version: 1.3.6
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Centralized API authorization
107
+ test_files:
108
+ - spec/lib/lockbox_middleware_spec.rb
109
+ - spec/spec_helper.rb
110
+ - spec/support/mocha.rb