heroku-bouncer 0.3.4 → 0.4.0.pre
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 +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
|