heroku-bouncer 0.4.0.pre → 0.4.0.pre2

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