heroku-bouncer 0.3.4 → 0.4.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +6 -6
  3. data/README.md +36 -13
  4. data/lib/heroku/bouncer.rb +127 -22
  5. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b0a0fcbf6c90f52b0178b11532f52f99c89d882f
4
- data.tar.gz: b98b20f813286ef2057f194b8060f325a78ffe89
3
+ metadata.gz: f43138f01404ab7e324200582dfb741e4eaaccdd
4
+ data.tar.gz: 066eb52d989918961505cd21f760b7421a3fd40d
5
5
  SHA512:
6
- metadata.gz: a64178645292a555e6dda738b02a795ce7405e649cf7467d458736da1108d74a67b03d99cc7f9f1d35681a251dc80e090df4dac9d8143a29ed07d4dadc15f0d0
7
- data.tar.gz: 1a6ae3ff4a85d68e9be0507edf3309dab70de8221d84587a19fdefe0918f5d2a617ebbf6cba46bbc981d3af159bb1ae9101675efafe40ae97ff04fd6560da40a
6
+ metadata.gz: f9b41150e950a3f5342d5f909197f38c7896a240a2e384b28e470d1217c600586a9631e5735fe1e5bf65eb3c50363f83190b03df507f985f551780707fde1cf0
7
+ data.tar.gz: c12572fc71cb948758547a3580e73bb0393cb75479f6e64eb6b514b54ee9997185ac511955e8dd381917eebc33cbb298ebc2032c1a192a9f380cc5dc2aab1cae
data/Gemfile.lock CHANGED
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- heroku-bouncer (0.2.1)
5
- encrypted_cookie (~> 0.0.4)
4
+ heroku-bouncer (0.3.3)
6
5
  faraday (~> 0.8)
7
6
  multi_json (~> 1.0)
8
7
  omniauth-heroku (>= 0.1.0)
@@ -11,14 +10,14 @@ PATH
11
10
  GEM
12
11
  remote: https://rubygems.org/
13
12
  specs:
14
- encrypted_cookie (0.0.4)
15
- faraday (0.8.7)
16
- multipart-post (~> 1.1)
13
+ faraday (0.8.8)
14
+ multipart-post (~> 1.2.0)
17
15
  hashie (2.0.5)
18
16
  httpauth (0.2.0)
19
17
  jwt (0.1.8)
20
18
  multi_json (>= 1.5)
21
- multi_json (1.7.7)
19
+ minitest (5.0.8)
20
+ multi_json (1.8.0)
22
21
  multipart-post (1.2.0)
23
22
  oauth2 (0.8.1)
24
23
  faraday (~> 0.8)
@@ -49,3 +48,4 @@ PLATFORMS
49
48
 
50
49
  DEPENDENCIES
51
50
  heroku-bouncer!
51
+ minitest (~> 5.0)
data/README.md CHANGED
@@ -10,7 +10,13 @@ Sinatra app that uses heroku-bouncer.
10
10
 
11
11
  ## Use
12
12
 
13
- 1. Create your OAuth client using `/auth/heroku/callback` as your
13
+ 1. Install the Heroku OAuth CLI plugin.
14
+
15
+ ```sh
16
+ heroku plugins:install git://github.com/heroku/heroku-oauth.git
17
+ ```
18
+
19
+ 2. Create your OAuth client using `/auth/heroku/callback` as your
14
20
  callback endpoint. Use `http://localhost:5000/auth/heroku/callback`
15
21
  for local development with Foreman.
16
22
 
@@ -19,26 +25,43 @@ Sinatra app that uses heroku-bouncer.
19
25
  heroku clients:register myapp https://myapp.herokuapp.com/auth/heroku/callback
20
26
  ```
21
27
 
22
- 2. Set `HEROKU_OAUTH_ID` and `HEROKU_OAUTH_SECRET` in your environment.
23
- 3. Set the `COOKIE_SECRET` environment variable to a long random string.
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.
24
30
  Otherwise, the OAuth ID and secret are concatenated for use as a secret.
25
- 4. Use the middleware.
31
+ 5. Use the middleware as follows:
26
32
 
27
- **Rack, Sinatra, and Rails 4**
33
+ **Rack**
28
34
 
29
- Add a `use` line to `config.ru`:
35
+ `Heroku::Bouncer` requires a session middleware to be mounted above
36
+ it. Pure Rack apps will need to add such a middleware if they don't
37
+ already have one. In `config.ru`:
30
38
 
31
39
  ```ruby
32
- require ::File.expand_path('../config/environment', __FILE__)
33
-
34
- use ::Heroku::Bouncer
35
- run Rails.application
40
+ require 'rack/session/cookie'
41
+ require 'heroku/bouncer'
42
+ require 'my_app'
43
+
44
+ # use `openssl rand -base64 32` to generate a secret
45
+ use Rack::Session::Cookie, secret: "..."
46
+ use Heroku::Bouncer
47
+ run MyApp
36
48
  ```
37
49
 
38
- The middleware does not work properly when configured inside
39
- Rails 4.
50
+ **Sinatra**
51
+
52
+ `Heroku::Bouncer` can be run like a Rack app, but since a Sinatra
53
+ app can mount Rack middleware, it may be easier to mount it inside
54
+ the app and use Sinatra's session.
55
+
56
+ ```ruby
57
+ class MyApp < Sinatra::Base
58
+ ...
59
+ enable :sessions, secret: "..."
60
+ use ::Heroku::Bouncer
61
+ ...
62
+ ```
40
63
 
41
- **Rails 3**
64
+ **Rails**
42
65
 
43
66
  Add a middleware configuration line to `config/application.rb`:
44
67
 
@@ -2,7 +2,7 @@ require 'sinatra/base'
2
2
  require 'omniauth-heroku'
3
3
  require 'faraday'
4
4
  require 'multi_json'
5
- require 'encrypted_cookie'
5
+ require 'openssl'
6
6
 
7
7
  unless defined?(Heroku)
8
8
  module Heroku; end
@@ -13,17 +13,13 @@ class Heroku::Bouncer < Sinatra::Base
13
13
  $stderr.puts "[warn] heroku-bouncer: HEROKU_ID detected, please use HEROKU_OAUTH_ID instead" if ENV.has_key?('HEROKU_ID')
14
14
  $stderr.puts "[warn] heroku-bouncer: HEROKU_SECRET detected, please use HEROKU_OAUTH_SECRET instead" if ENV.has_key?('HEROKU_SECRET')
15
15
 
16
- ID = (ENV['HEROKU_OAUTH_ID'] || ENV['HEROKU_ID']).to_s
17
- SECRET = (ENV['HEROKU_OAUTH_SECRET'] || ENV['HEROKU_SECRET']).to_s
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
18
19
 
19
20
  enable :raise_errors
20
21
  disable :show_exceptions
21
22
 
22
- use Rack::Session::EncryptedCookie,
23
- :secret => (ENV['COOKIE_SECRET'] || (ID + SECRET)).to_s,
24
- :expire_after => 8 * 60 * 60,
25
- :key => (ENV['COOKIE_NAME'] || '_bouncer_session').to_s
26
-
27
23
  # sets up the /auth/heroku endpoint
28
24
  unless ID.empty? || SECRET.empty?
29
25
  use OmniAuth::Builder do
@@ -47,14 +43,27 @@ class Heroku::Bouncer < Sinatra::Base
47
43
  end
48
44
 
49
45
  def call(env)
50
- @disabled ? @app.call(env) : super(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
51
60
  end
52
61
 
53
62
  before do
54
- if session[:store] && session[:store][:user]
63
+ if store_read(:user)
55
64
  expose_store
56
65
  elsif ! %w[/auth/heroku/callback /auth/heroku /auth/failure /auth/sso-logout /auth/logout].include?(request.path)
57
- session[:return_to] = request.url
66
+ store_write(:return_to, request.url)
58
67
  redirect to('/auth/heroku')
59
68
  end
60
69
  end
@@ -68,14 +77,14 @@ class Heroku::Bouncer < Sinatra::Base
68
77
  url = @herokai_only.is_a?(String) ? @herokai_only : 'https://www.heroku.com'
69
78
  redirect to(url) and return
70
79
  end
71
- @expose_user ? store(:user, user) : store(:user, true)
72
- store(:email, user['email']) if @expose_email
80
+ @expose_user ? store_write(:user, user) : store_write(:user, true)
81
+ store_write(:email, user['email']) if @expose_email
73
82
  else
74
- store(:user, true)
83
+ store_write(:user, true)
75
84
  end
76
85
 
77
- store(:token, token) if @expose_token
78
- redirect to(session.delete(:return_to) || '/')
86
+ store_write(:token, token) if @expose_token
87
+ redirect to(store_delete(:return_to) || '/')
79
88
  end
80
89
 
81
90
  # something went wrong
@@ -97,10 +106,90 @@ class Heroku::Bouncer < Sinatra::Base
97
106
  redirect to("/")
98
107
  end
99
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
+
100
183
  private
101
184
 
102
- def destroy_session
103
- session = nil if session
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)
104
193
  end
105
194
 
106
195
  def extract_option(options, option, default = nil)
@@ -115,15 +204,31 @@ private
115
204
  end.body)
116
205
  end
117
206
 
118
- def store(key, value)
119
- session[:store] ||= {}
120
- session[:store][key] = value
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
121
225
  end
122
226
 
123
227
  def expose_store
124
- session[:store].each_pair do |key, value|
228
+ store.each_pair do |key, value|
125
229
  request.env["bouncer.#{key}"] = value
126
230
  end
127
231
  end
128
232
 
233
+
129
234
  end
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.3.4
4
+ version: 0.4.0.pre
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-24 00:00:00.000000000 Z
11
+ date: 2013-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth-heroku
@@ -67,19 +67,19 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: encrypted_cookie
70
+ name: minitest
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ~>
74
74
  - !ruby/object:Gem::Version
75
- version: 0.0.4
76
- type: :runtime
75
+ version: '5.0'
76
+ type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ~>
81
81
  - !ruby/object:Gem::Version
82
- version: 0.0.4
82
+ version: '5.0'
83
83
  description: ID please.
84
84
  email:
85
85
  - jd@heroku.com
@@ -108,9 +108,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
108
  version: '0'
109
109
  required_rubygems_version: !ruby/object:Gem::Requirement
110
110
  requirements:
111
- - - '>='
111
+ - - '>'
112
112
  - !ruby/object:Gem::Version
113
- version: '0'
113
+ version: 1.3.1
114
114
  requirements: []
115
115
  rubyforge_project:
116
116
  rubygems_version: 2.0.3