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 +4 -4
- data/README.md +32 -10
- data/lib/heroku/bouncer/builder.rb +53 -0
- data/lib/heroku/bouncer/decrypted_hash.rb +38 -0
- data/lib/heroku/bouncer/json_parser.rb +18 -0
- data/lib/heroku/bouncer/lockbox.rb +40 -0
- data/lib/heroku/bouncer/middleware.rb +144 -0
- data/lib/heroku/bouncer.rb +7 -231
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 251889f5d6ea897a0ff12014d09e83ecc1daec83
|
4
|
+
data.tar.gz: 35458dfffc28c61a9b1ec3391a38039308a81840
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
69
|
+
4. Add the required options `:oauth` and `:secret` as explained
|
70
|
+
below.
|
73
71
|
|
74
|
-
|
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,
|
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]
|
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
|
data/lib/heroku/bouncer.rb
CHANGED
@@ -1,234 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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.
|
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-
|
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:
|
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
|