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.
- checksums.yaml +4 -4
- data/Gemfile.lock +6 -6
- data/README.md +36 -13
- data/lib/heroku/bouncer.rb +127 -22
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f43138f01404ab7e324200582dfb741e4eaaccdd
|
4
|
+
data.tar.gz: 066eb52d989918961505cd21f760b7421a3fd40d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
15
|
-
|
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
|
-
|
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.
|
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
|
-
|
23
|
-
|
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
|
-
|
31
|
+
5. Use the middleware as follows:
|
26
32
|
|
27
|
-
**Rack
|
33
|
+
**Rack**
|
28
34
|
|
29
|
-
|
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
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
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
|
64
|
+
**Rails**
|
42
65
|
|
43
66
|
Add a middleware configuration line to `config/application.rb`:
|
44
67
|
|
data/lib/heroku/bouncer.rb
CHANGED
@@ -2,7 +2,7 @@ require 'sinatra/base'
|
|
2
2
|
require 'omniauth-heroku'
|
3
3
|
require 'faraday'
|
4
4
|
require 'multi_json'
|
5
|
-
require '
|
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
|
17
|
-
SECRET
|
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
|
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
|
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
|
-
|
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 ?
|
72
|
-
|
80
|
+
@expose_user ? store_write(:user, user) : store_write(:user, true)
|
81
|
+
store_write(:email, user['email']) if @expose_email
|
73
82
|
else
|
74
|
-
|
83
|
+
store_write(:user, true)
|
75
84
|
end
|
76
85
|
|
77
|
-
|
78
|
-
redirect 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
|
103
|
-
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
|
119
|
-
session[:
|
120
|
-
|
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
|
-
|
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.
|
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-
|
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:
|
70
|
+
name: minitest
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ~>
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
76
|
-
type: :
|
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:
|
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:
|
113
|
+
version: 1.3.1
|
114
114
|
requirements: []
|
115
115
|
rubyforge_project:
|
116
116
|
rubygems_version: 2.0.3
|