heroku-bouncer 0.4.0.pre → 0.4.0.pre2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f43138f01404ab7e324200582dfb741e4eaaccdd
4
- data.tar.gz: 066eb52d989918961505cd21f760b7421a3fd40d
3
+ metadata.gz: 251889f5d6ea897a0ff12014d09e83ecc1daec83
4
+ data.tar.gz: 35458dfffc28c61a9b1ec3391a38039308a81840
5
5
  SHA512:
6
- metadata.gz: f9b41150e950a3f5342d5f909197f38c7896a240a2e384b28e470d1217c600586a9631e5735fe1e5bf65eb3c50363f83190b03df507f985f551780707fde1cf0
7
- data.tar.gz: c12572fc71cb948758547a3580e73bb0393cb75479f6e64eb6b514b54ee9997185ac511955e8dd381917eebc33cbb298ebc2032c1a192a9f380cc5dc2aab1cae
6
+ metadata.gz: c8f8233a0789b6be629ed528ff9d7e6b0529bf6c3a93ae16b673ba54538383655f793989d785a4dadf1b8c25052a7ba9dabb9cdcd662a9129d2c8cdc5eef6271
7
+ data.tar.gz: cda090b0f6ce0803f1981d318f0ca02bd2dbf04362c18ee6603523d864efeccf696268a9db59c673867306585bb24038412dff9b7da81f1f49b0bd90cfa06629
data/README.md CHANGED
@@ -25,10 +25,7 @@ Sinatra app that uses heroku-bouncer.
25
25
  heroku clients:register myapp https://myapp.herokuapp.com/auth/heroku/callback
26
26
  ```
27
27
 
28
- 3. Set `HEROKU_OAUTH_ID` and `HEROKU_OAUTH_SECRET` in your environment.
29
- 4. Set the `COOKIE_SECRET` environment variable to a long random string.
30
- Otherwise, the OAuth ID and secret are concatenated for use as a secret.
31
- 5. Use the middleware as follows:
28
+ 3. Configure the middleware as follows:
32
29
 
33
30
  **Rack**
34
31
 
@@ -69,10 +66,29 @@ Sinatra app that uses heroku-bouncer.
69
66
  config.middleware.use ::Heroku::Bouncer
70
67
  ```
71
68
 
72
- ## Options
69
+ 4. Add the required options `:oauth` and `:secret` as explained
70
+ below.
73
71
 
74
- There are 4 boolean options you can pass to the middleware:
72
+ ## Settings
75
73
 
74
+ Two settings are **required**:
75
+
76
+ * `oauth`: Your OAuth credentials as a hash - `:id` and `:secret`.
77
+ * `secret`: A random string used as an encryption secret used to secure
78
+ the user information in the session.
79
+
80
+ For example:
81
+
82
+ ```ruby
83
+ use Heroku::Bouncer,
84
+ oauth: { id: "...", secret: "..." },
85
+ secret: "..."
86
+ ```
87
+
88
+ There are 5 options you can pass to the middleware:
89
+
90
+ * `oauth[:scope]`: The [OAuth scope][] to use when requesting the OAuth
91
+ token. Default: `identity`.
76
92
  * `herokai_only`: Automatically redirects non-Heroku accounts to
77
93
  `www.heroku.com`. Alternatively, pass a valid URL and non-Herokai will
78
94
  be redirected there. Default: `false`
@@ -86,7 +102,10 @@ There are 4 boolean options you can pass to the middleware:
86
102
  You use these by passing a hash to the `use` call, for example:
87
103
 
88
104
  ```ruby
89
- use Heroku::Bouncer, expose_token: true
105
+ use Heroku::Bouncer,
106
+ oauth: { id: "...", secret: "...", scope: "global" },
107
+ secret: "...",
108
+ expose_token: true
90
109
  ```
91
110
 
92
111
  ## How to get the data
@@ -125,12 +144,15 @@ logging in again.
125
144
  ## Conditionally enable the middleware
126
145
 
127
146
  Don't want to OAuth on every request? Use a middleware to conditionally
128
- enable this middleware, like
129
- [Rack::Builder](http://rack.rubyforge.org/doc/Rack/Builder.html).
147
+ enable this middleware, like [Rack::Builder][].
130
148
  Alternatively, [use inheritance to extend the middleware to act any way
131
- you like](https://gist.github.com/wuputah/5534428).
149
+ you like][inheritance].
132
150
 
133
151
  ## There be dragons
134
152
 
135
153
  * There's no tests yet. You may encounter bugs. Please report them (or
136
154
  fix them in a pull request).
155
+
156
+ [OAuth scope]: https://devcenter.heroku.com/articles/oauth#scopes
157
+ [Rack::Builder]: http://rack.rubyforge.org/doc/Rack/Builder.html
158
+ [inheritance]: https://gist.github.com/wuputah/5534428
@@ -0,0 +1,53 @@
1
+ require 'heroku/bouncer/middleware'
2
+ require 'rack/builder'
3
+ require 'omniauth-heroku'
4
+
5
+ class Heroku::Bouncer::Builder
6
+
7
+ def self.new(app, options = {})
8
+ builder = Rack::Builder.new
9
+ id, secret, scope = extract_options!(options)
10
+ unless id.empty? || secret.empty?
11
+ builder.use OmniAuth::Builder do
12
+ provider :heroku, id, secret, :scope => scope
13
+ end
14
+ end
15
+ builder.run Heroku::Bouncer::Middleware.new(app, options)
16
+ builder
17
+ end
18
+
19
+ def self.extract_options!(options)
20
+ oauth = options.delete(:oauth) || {}
21
+ id = oauth.delete(:id)
22
+ secret = oauth.delete(:secret)
23
+ scope = oauth.delete(:scope) || 'identity'
24
+
25
+ if id.nil? && (ENV.has_key?('HEROKU_ID') || ENV.has_key?('HEROKU_OAUTH_ID'))
26
+ $stderr.puts "[warn] heroku-bouncer: HEROKU_ID or HEROKU_OAUTH_ID detected in environment, please pass in :oauth hash instead"
27
+ id = ENV['HEROKU_OAUTH_ID'] || ENV['HEROKU_ID']
28
+ end
29
+
30
+ if secret.nil? && (ENV.has_key?('HEROKU_SECRET') || ENV.has_key?('HEROKU_OAUTH_SECRET'))
31
+ $stderr.puts "[warn] heroku-bouncer: HEROKU_SECRET or HEROKU_OAUTH_SECRET detected in environment, please pass in :oauth hash instead"
32
+ secret = ENV['HEROKU_OAUTH_SECRET'] || ENV['HEROKU_SECRET']
33
+ end
34
+
35
+ if id.nil? || secret.nil?
36
+ $stderr.puts "[fatal] heroku-bouncer: HEROKU_OAUTH_ID or HEROKU_OAUTH_SECRET not set, middleware disabled"
37
+ options[:disabled] = true
38
+ end
39
+
40
+ # we have to do this here because we wont have id+secret later
41
+ if options[:secret].nil?
42
+ if ENV.has_key?('COOKIE_SECRET')
43
+ $stderr.puts "[warn] heroku-bouncer: COOKIE_SECRET detected in environment, please pass in :secret instead"
44
+ options[:secret] = ENV['COOKIE_SECRET']
45
+ else
46
+ $stderr.puts "[warn] heroku-bouncer: :secret is missing, using id + secret"
47
+ options[:secret] = id.to_s + secret.to_s
48
+ end
49
+ end
50
+
51
+ [id, secret, scope]
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ require 'heroku/bouncer/lockbox'
2
+
3
+ # Encapsulates encrypting and decrypting a hash of data. Does not store the
4
+ # key that is passed in.
5
+ class Heroku::Bouncer::DecryptedHash < Hash
6
+
7
+ Lockbox = ::Heroku::Bouncer::Lockbox
8
+
9
+ def initialize(decrypted_hash = nil)
10
+ super
11
+ replace(decrypted_hash) if decrypted_hash
12
+ end
13
+
14
+ def self.unlock(data, key)
15
+ if data && data = Lockbox.new(key).unlock(data)
16
+ data, digest = data.split("--")
17
+ if digest == Lockbox.generate_hmac(data, key)
18
+ data = data.unpack('m*').first
19
+ data = Marshal.load(data)
20
+ new(data)
21
+ else
22
+ new
23
+ end
24
+ else
25
+ new
26
+ end
27
+ end
28
+
29
+ def lock(key)
30
+ # marshal a Hash, not a DecryptedHash
31
+ data = {}.replace(self)
32
+ data = Marshal.dump(data)
33
+ data = [data].pack('m*')
34
+ data = "#{data}--#{Lockbox.generate_hmac(data, key)}"
35
+ Lockbox.new(key).lock(data)
36
+ end
37
+
38
+ end
@@ -0,0 +1,18 @@
1
+ # json parsers, all the way down
2
+ Heroku::Bouncer::JsonParser = begin
3
+ require 'oj'
4
+ lambda { |json| Oj.load(json, :mode => :strict) }
5
+ rescue LoadError
6
+ begin
7
+ require 'yajl'
8
+ lambda { |json| Yajl::Parser.parse(json) }
9
+ rescue LoadError
10
+ begin
11
+ require 'multi_json'
12
+ lambda { |json| MultiJson.decode(json) }
13
+ rescue LoadError
14
+ require 'json'
15
+ lambda { |json| JSON.parse(json) }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ require 'openssl'
2
+
3
+ class Heroku::Bouncer::Lockbox < BasicObject
4
+
5
+ def initialize(key)
6
+ @key = key
7
+ end
8
+
9
+ def lock(str)
10
+ aes = ::OpenSSL::Cipher::Cipher.new('aes-128-cbc').encrypt
11
+ aes.key = @key
12
+ iv = ::OpenSSL::Random.random_bytes(aes.iv_len)
13
+ aes.iv = iv
14
+ [iv + (aes.update(str) << aes.final)].pack('m0')
15
+ end
16
+
17
+ # decrypts string. returns nil if an error occurs
18
+ #
19
+ # returns nil if openssl raises an error during decryption (data
20
+ # manipulation, key change, implementation change), or if the text to
21
+ # decrypt is too short to possibly be good aes data.
22
+ def unlock(str)
23
+ str = str.unpack('m0').first
24
+ aes = ::OpenSSL::Cipher::Cipher.new('aes-128-cbc').decrypt
25
+ aes.key = @key
26
+ iv = str[0, aes.iv_len]
27
+ aes.iv = iv
28
+ crypted_text = str[aes.iv_len..-1]
29
+ return nil if crypted_text.nil? || iv.nil?
30
+ aes.update(crypted_text) << aes.final
31
+ rescue
32
+ nil
33
+ end
34
+
35
+ private
36
+
37
+ def self.generate_hmac(data, key)
38
+ ::OpenSSL::HMAC.hexdigest(::OpenSSL::Digest::SHA1.new, key, data)
39
+ end
40
+ end
@@ -0,0 +1,144 @@
1
+ require 'sinatra/base'
2
+ require 'faraday'
3
+ require 'securerandom'
4
+
5
+ require 'heroku/bouncer/json_parser'
6
+ require 'heroku/bouncer/decrypted_hash'
7
+
8
+ class Heroku::Bouncer::Middleware < Sinatra::Base
9
+
10
+ DecryptedHash = ::Heroku::Bouncer::DecryptedHash
11
+
12
+ enable :raise_errors
13
+ disable :show_exceptions
14
+
15
+ def initialize(app, options = {})
16
+ if options[:disabled]
17
+ @app = app
18
+ @disabled = true
19
+ # super is not called; we're not using sinatra if we're disabled
20
+ else
21
+ super(app)
22
+ @cookie_secret = extract_option(options, :secret, SecureRandom.base64(32))
23
+ @herokai_only = extract_option(options, :herokai_only, false)
24
+ @expose_token = extract_option(options, :expose_token, false)
25
+ @expose_email = extract_option(options, :expose_email, true)
26
+ @expose_user = extract_option(options, :expose_user, true)
27
+ end
28
+ end
29
+
30
+ def call(env)
31
+ if @disabled
32
+ @app.call(env)
33
+ else
34
+ unlock_session_data(env) do
35
+ super(env)
36
+ end
37
+ end
38
+ end
39
+
40
+ def unlock_session_data(env, &block)
41
+ decrypt_store(env)
42
+ return_value = yield
43
+ encrypt_store(env)
44
+ return_value
45
+ end
46
+
47
+ before do
48
+ if store_read(:user)
49
+ expose_store
50
+ elsif ! %w[/auth/heroku/callback /auth/heroku /auth/failure /auth/sso-logout /auth/logout].include?(request.path)
51
+ store_write(:return_to, request.url)
52
+ redirect to('/auth/heroku')
53
+ end
54
+ end
55
+
56
+ # callback when successful, time to save data
57
+ get '/auth/heroku/callback' do
58
+ token = request.env['omniauth.auth']['credentials']['token']
59
+ if @expose_email || @expose_user || @herokai_only
60
+ user = fetch_user(token)
61
+ if @herokai_only && !user['email'].end_with?("@heroku.com")
62
+ url = @herokai_only.is_a?(String) ? @herokai_only : 'https://www.heroku.com'
63
+ redirect to(url) and return
64
+ end
65
+ @expose_user ? store_write(:user, user) : store_write(:user, true)
66
+ store_write(:email, user['email']) if @expose_email
67
+ else
68
+ store_write(:user, true)
69
+ end
70
+
71
+ store_write(:token, token) if @expose_token
72
+ redirect to(store_delete(:return_to) || '/')
73
+ end
74
+
75
+ # something went wrong
76
+ get '/auth/failure' do
77
+ destroy_session
78
+ redirect to("/")
79
+ end
80
+
81
+ # logout, single sign-on style
82
+ get '/auth/sso-logout' do
83
+ destroy_session
84
+ auth_url = ENV["HEROKU_AUTH_URL"] || "https://id.heroku.com"
85
+ redirect to("#{auth_url}/logout")
86
+ end
87
+
88
+ # logout but only locally
89
+ get '/auth/logout' do
90
+ destroy_session
91
+ redirect to("/")
92
+ end
93
+
94
+ private
95
+
96
+ def extract_option(options, option, default = nil)
97
+ options.has_key?(option) ? options[option] : default
98
+ end
99
+
100
+ def fetch_user(token)
101
+ ::Heroku::Bouncer::JsonParser.call(
102
+ Faraday.new(ENV["HEROKU_API_URL"] || "https://api.heroku.com/").get('/account') do |r|
103
+ r.headers['Accept'] = 'application/json'
104
+ r.headers['Authorization'] = "Bearer #{token}"
105
+ end.body)
106
+ end
107
+
108
+ def decrypt_store(env)
109
+ env["rack.session"][:bouncer] =
110
+ DecryptedHash.unlock(env["rack.session"][:bouncer], @cookie_secret)
111
+ end
112
+
113
+ def encrypt_store(env)
114
+ env["rack.session"][:bouncer] =
115
+ env["rack.session"][:bouncer].lock(@cookie_secret)
116
+ end
117
+
118
+ def store
119
+ session[:bouncer]
120
+ end
121
+
122
+ def store_write(key, value)
123
+ store[key] = value
124
+ end
125
+
126
+ def store_read(key)
127
+ store.fetch(key, nil)
128
+ end
129
+
130
+ def store_delete(key)
131
+ store.delete(key)
132
+ end
133
+
134
+ def destroy_session
135
+ session = nil if session
136
+ end
137
+
138
+ def expose_store
139
+ store.each_pair do |key, value|
140
+ request.env["bouncer.#{key}"] = value
141
+ end
142
+ end
143
+
144
+ end
@@ -1,234 +1,10 @@
1
- require 'sinatra/base'
2
- require 'omniauth-heroku'
3
- require 'faraday'
4
- require 'multi_json'
5
- require 'openssl'
6
-
7
- unless defined?(Heroku)
8
- module Heroku; end
9
- end
10
-
11
- class Heroku::Bouncer < Sinatra::Base
12
-
13
- $stderr.puts "[warn] heroku-bouncer: HEROKU_ID detected, please use HEROKU_OAUTH_ID instead" if ENV.has_key?('HEROKU_ID')
14
- $stderr.puts "[warn] heroku-bouncer: HEROKU_SECRET detected, please use HEROKU_OAUTH_SECRET instead" if ENV.has_key?('HEROKU_SECRET')
15
-
16
- ID = (ENV['HEROKU_OAUTH_ID'] || ENV['HEROKU_ID']).to_s
17
- SECRET = (ENV['HEROKU_OAUTH_SECRET'] || ENV['HEROKU_SECRET']).to_s
18
- COOKIE_SECRET = (ENV['COOKIE_SECRET'] || (ID + SECRET)).to_s
19
-
20
- enable :raise_errors
21
- disable :show_exceptions
22
-
23
- # sets up the /auth/heroku endpoint
24
- unless ID.empty? || SECRET.empty?
25
- use OmniAuth::Builder do
26
- provider :heroku, ID, SECRET
27
- end
28
- end
29
-
30
- def initialize(app, options = {})
31
- if ID.empty? || SECRET.empty?
32
- $stderr.puts "[fatal] heroku-bouncer: HEROKU_OAUTH_ID or HEROKU_OAUTH_SECRET not set, middleware disabled"
33
- @app = app
34
- @disabled = true
35
- # super is not called; we're not using sinatra if we're disabled
36
- else
37
- super(app)
38
- @herokai_only = extract_option(options, :herokai_only, false)
39
- @expose_token = extract_option(options, :expose_token, false)
40
- @expose_email = extract_option(options, :expose_email, true)
41
- @expose_user = extract_option(options, :expose_user, true)
1
+ # define Heroku and Heroku::Bouncer
2
+ module Heroku
3
+ class Bouncer
4
+ def self.new(*args)
5
+ Heroku::Bouncer::Builder.new(*args)
42
6
  end
43
7
  end
44
-
45
- def call(env)
46
- if @disabled
47
- @app.call(env)
48
- else
49
- unlock_session_data(env) do
50
- super(env)
51
- end
52
- end
53
- end
54
-
55
- def unlock_session_data(env, &block)
56
- decrypt_store(env)
57
- return_value = yield
58
- encrypt_store(env)
59
- return_value
60
- end
61
-
62
- before do
63
- if store_read(:user)
64
- expose_store
65
- elsif ! %w[/auth/heroku/callback /auth/heroku /auth/failure /auth/sso-logout /auth/logout].include?(request.path)
66
- store_write(:return_to, request.url)
67
- redirect to('/auth/heroku')
68
- end
69
- end
70
-
71
- # callback when successful, time to save data
72
- get '/auth/heroku/callback' do
73
- token = request.env['omniauth.auth']['credentials']['token']
74
- if @expose_email || @expose_user || @herokai_only
75
- user = fetch_user(token)
76
- if @herokai_only && !user['email'].end_with?("@heroku.com")
77
- url = @herokai_only.is_a?(String) ? @herokai_only : 'https://www.heroku.com'
78
- redirect to(url) and return
79
- end
80
- @expose_user ? store_write(:user, user) : store_write(:user, true)
81
- store_write(:email, user['email']) if @expose_email
82
- else
83
- store_write(:user, true)
84
- end
85
-
86
- store_write(:token, token) if @expose_token
87
- redirect to(store_delete(:return_to) || '/')
88
- end
89
-
90
- # something went wrong
91
- get '/auth/failure' do
92
- destroy_session
93
- redirect to("/")
94
- end
95
-
96
- # logout, single sign-on style
97
- get '/auth/sso-logout' do
98
- destroy_session
99
- auth_url = ENV["HEROKU_AUTH_URL"] || "https://id.heroku.com"
100
- redirect to("#{auth_url}/logout")
101
- end
102
-
103
- # logout but only locally
104
- get '/auth/logout' do
105
- destroy_session
106
- redirect to("/")
107
- end
108
-
109
- # Encapsulates encrypting and decrypting a hash of data. Does not store the
110
- # key that is passed in.
111
- class DecryptedHash < Hash
112
-
113
- def initialize(decrypted_hash = nil)
114
- super
115
- replace(decrypted_hash) if decrypted_hash
116
- end
117
-
118
- def self.unlock(data, key)
119
- if data && data = Lockbox.new(key).unlock(data)
120
- data, digest = data.split("--")
121
- if digest == Lockbox.generate_hmac(data, key)
122
- data = data.unpack('m*').first
123
- data = Marshal.load(data)
124
- new(data)
125
- else
126
- new
127
- end
128
- else
129
- new
130
- end
131
- end
132
-
133
- def lock(key)
134
- # marshal a Hash, not a DecryptedHash
135
- data = {}.replace(self)
136
- data = Marshal.dump(data)
137
- data = [data].pack('m*')
138
- data = "#{data}--#{Lockbox.generate_hmac(data, key)}"
139
- Lockbox.new(key).lock(data)
140
- end
141
-
142
- end
143
-
144
- class Lockbox < BasicObject
145
-
146
- def initialize(key)
147
- @key = key
148
- end
149
-
150
- def lock(str)
151
- aes = ::OpenSSL::Cipher::Cipher.new('aes-128-cbc').encrypt
152
- aes.key = @key
153
- iv = ::OpenSSL::Random.random_bytes(aes.iv_len)
154
- aes.iv = iv
155
- [iv + (aes.update(str) << aes.final)].pack('m0')
156
- end
157
-
158
- # decrypts string. returns nil if an error occurs
159
- #
160
- # returns nil if openssl raises an error during decryption (data
161
- # manipulation, key change, implementation change), or if the text to
162
- # decrypt is too short to possibly be good aes data.
163
- def unlock(str)
164
- str = str.unpack('m0').first
165
- aes = ::OpenSSL::Cipher::Cipher.new('aes-128-cbc').decrypt
166
- aes.key = @key
167
- iv = str[0, aes.iv_len]
168
- aes.iv = iv
169
- crypted_text = str[aes.iv_len..-1]
170
- return nil if crypted_text.nil? || iv.nil?
171
- aes.update(crypted_text) << aes.final
172
- rescue
173
- nil
174
- end
175
-
176
- private
177
-
178
- def self.generate_hmac(data, key)
179
- ::OpenSSL::HMAC.hexdigest(::OpenSSL::Digest::SHA1.new, key, data)
180
- end
181
- end
182
-
183
- private
184
-
185
- def decrypt_store(env)
186
- env["rack.session"][:bouncer] =
187
- DecryptedHash.unlock(env["rack.session"][:bouncer], COOKIE_SECRET)
188
- end
189
-
190
- def encrypt_store(env)
191
- env["rack.session"][:bouncer] =
192
- env["rack.session"][:bouncer].lock(COOKIE_SECRET)
193
- end
194
-
195
- def extract_option(options, option, default = nil)
196
- options.has_key?(option) ? options[option] : default
197
- end
198
-
199
- def fetch_user(token)
200
- MultiJson.decode(
201
- Faraday.new(ENV["HEROKU_API_URL"] || "https://api.heroku.com/").get('/account') do |r|
202
- r.headers['Accept'] = 'application/json'
203
- r.headers['Authorization'] = "Bearer #{token}"
204
- end.body)
205
- end
206
-
207
- def store
208
- session[:bouncer]
209
- end
210
-
211
- def store_write(key, value)
212
- store[key] = value
213
- end
214
-
215
- def store_read(key)
216
- store.fetch(key, nil)
217
- end
218
-
219
- def store_delete(key)
220
- store.delete(key)
221
- end
222
-
223
- def destroy_session
224
- session = nil if session
225
- end
226
-
227
- def expose_store
228
- store.each_pair do |key, value|
229
- request.env["bouncer.#{key}"] = value
230
- end
231
- end
232
-
233
-
234
8
  end
9
+
10
+ require 'heroku/bouncer/builder'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku-bouncer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0.pre
4
+ version: 0.4.0.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Dance
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-25 00:00:00.000000000 Z
11
+ date: 2013-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth-heroku
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.8'
55
55
  - !ruby/object:Gem::Dependency
56
- name: multi_json
56
+ name: rack
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ~>
@@ -89,6 +89,11 @@ extra_rdoc_files:
89
89
  - README.md
90
90
  files:
91
91
  - lib/heroku/bouncer.rb
92
+ - lib/heroku/bouncer/decrypted_hash.rb
93
+ - lib/heroku/bouncer/lockbox.rb
94
+ - lib/heroku/bouncer/json_parser.rb
95
+ - lib/heroku/bouncer/builder.rb
96
+ - lib/heroku/bouncer/middleware.rb
92
97
  - README.md
93
98
  - Gemfile
94
99
  - Gemfile.lock