lockbox_middleware 1.2.0

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