balrog 1.1.0 → 2.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 232b0520af207a4da00798bba32874e5ef4ee33c55419793c7ec23e8312c5712
4
- data.tar.gz: 4fc0f719261215f6b2c657f4d2a1964df096d92b67cf2b94f1585ec99a609b94
3
+ metadata.gz: ed08c9a6aa5d68fc2c81f784ea2bd330353c64a1e8ee8729acc6dc65fa4e0da2
4
+ data.tar.gz: 826d7ab7ed00637b9ac8642b44b93909b727bd2ae0971a872fab695d96d9f677
5
5
  SHA512:
6
- metadata.gz: 4521321149b2a0636694f064e43cb20e8416c8eb2f93fd90668195d82a68dfd17bca183618664049139428dec095e7a74f1754b0b66a650d71d3d78610a91806
7
- data.tar.gz: 4d7fbda793ea374006246abae4c9f1aa62b9369972e33f94aa090a152c799a2800d0081b29ec65a1a8d9a1b2a351f11bfe7ce9a85a47ff0637b54b769a707e69
6
+ metadata.gz: 0c0577f269883d820021d35af147e6746868c1dd20581d25919e9f3938657bc780571ae5978f13d8c7caea91baf63c39539271b2cffbbf3f2d306eef7b911120
7
+ data.tar.gz: 884d62c762f25c50c562d190d93452438680b33607cd53d4dd50921a2c9fa13419e1aae2ccd5cfeb8b34f9f2582ab4348e87b53961f853c441f2607df3d04fd7
@@ -1,5 +1,16 @@
1
+ # 2.0.0
2
+
3
+ - Enhancements
4
+ - added support for single sign-on.
5
+
6
+ - BREAKING
7
+ - Balrog can no longer be initialized via `Rails.application.config.middleware.use Balrog::Middleware`. Instead, you need to configure Balrog with `Balrog::Middleware.setup`. See the [README](https://github.com/pixielabs/balrog#Upgrading-from-1.1-to-2.0) for more info.
8
+ - The instance method `Balrog::Middleware#password_hash` has been converted into a class method `Balrog::Middleware.set_password_hash`. See the [README](https://github.com/pixielabs/balrog#Upgrading-from-1.1-to-2.0) for more info.
9
+ - The instance method `Balrog::Middleware#set_session_expiry` has been converted into a class method `Balrog::Middleware.set_session_expiry`. See the [README](https://github.com/pixielabs/balrog#Upgrading-from-1.1-to-2.0) for more info.
10
+
1
11
  # 1.1.0
2
- - added `Balrog::Middleware#session_expires_after`, which would force end users to login again after a certain period of time.
12
+
13
+ - added `Balrog::Middleware#set_session_expiry`, which would force end users to login again after a certain period of time.
3
14
  - added `balrog:view` generator, enabling users to modify their Balrog gate view.
4
15
 
5
16
  # 1.0.0
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- balrog (1.1.0)
4
+ balrog (2.0.0)
5
5
  bcrypt (~> 3.0)
6
6
  rails (>= 5)
7
7
 
data/README.md CHANGED
@@ -6,17 +6,31 @@
6
6
  [![CircleCI](https://circleci.com/gh/pixielabs/balrog.svg?style=svg)](https://circleci.com/gh/pixielabs/balrog)
7
7
 
8
8
  Balrog is a lightweight authorization library for Ruby on Rails >= 5 written by
9
- [Pixie Labs](https://pixielabs.io) that can protect your routes with a single
10
- username & password combination.
11
-
12
- Balrog is an alternative to `http_basic_authentication_with` that provides some
13
- advantages:
14
-
15
- * Uses a password hash instead of a plaintext password.
16
- * Provides a lightweight HTML form instead of inconsistent basic
17
- authentication.
18
- * Better support for password managers (which often don't support basic
19
- authentication dialog boxes).
9
+ [Pixie Labs](https://pixielabs.io) that can protect your routes. Balrog can be
10
+ configured to authorize users using a simple password or single sign-on or both.
11
+
12
+ - If you choose to protect your routes with a password, the password will be
13
+ stored as a password hash, not plain text, and Balrog provides a lightweight
14
+ HTML form that can be styled and used with password managers.
15
+ - If you choose to configure Balrog to use SSO, you can whitelist multiple email
16
+ domains, allowing groups of users access parts of your app, without circulating
17
+ a password.
18
+ - Balrog's authentication can and should be configured to expire, requiring
19
+ users to sign-in again in accordance with [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-expiration) best practices.
20
+ - Balrog can also be used to restrict access to [mounted Rack applications](#Restricting-access-to-mounted-Rack-applications-within-config/routes.rb) like Sidekiq.
21
+
22
+ ## Table of Contents
23
+
24
+ - [Installation](#Installation)
25
+ - [Regenerating a password hash](#Regenerating-a-password-hash)
26
+ - [Restricting access in a controller](#Restricting-access-in-a-controller)
27
+ - [Restricting access to mounted Rack applications](#Restricting-access-to-mounted-Rack-applications-within-config/routes.rb)
28
+ - [Logout button](#Logout-button)
29
+ - [Changing session expiry length](#Changing-session-expiry-length)
30
+ - [Configuring the Balrog gate view](#Configuring-the-Balrog-gate-view)
31
+ - [Single Sign On](#Single-Sign-On)
32
+ - [Upgrading from 1.1 to 2.0](#Upgrading-from-1.1-to-2.0)
33
+ - [Contributing](#Contributing)
20
34
 
21
35
  ## Installation
22
36
 
@@ -120,11 +134,12 @@ If you don't want sessions to expire, remove `set_session_expiry`
120
134
  from the initializer completely.
121
135
 
122
136
  ```ruby
123
- Rails.application.config.middleware.use Balrog::Middleware do
124
- password_hash '$2a$12$BLz7XCFdG9YfwL64KlTgY.T3FY55aQk8SZEzHfpHfw15F2uN1kuSi'
125
- set_session_expiry 30.minutes
137
+ Balrog::Middleware.setup do |config|
138
+ config.password_hash '$2a$12$BLz7XCFdG9YfwL64KlTgY.T3FY55aQk8SZEzHfpHfw15F2uN1kuSi'
139
+ config.set_session_expiry 30.minutes
126
140
  end
127
141
  ```
142
+
128
143
  ## Configuring the Balrog gate view
129
144
 
130
145
  We built Balrog to have a default view and stylesheet so that you can drop
@@ -145,6 +160,62 @@ After running the generator, you can now add elements and classes to the
145
160
  `app/views/layouts/balrog.html.erb`. For an example, see the
146
161
  [dummy-rails-app](https://github.com/pixielabs/balrog/tree/master/spec/dummy-rails-app) in the spec folder.
147
162
 
163
+ ## Single Sign On
164
+
165
+ To add single sign on you will need to add the [omniauth gem](https://github.com/omniauth/omniauth)
166
+ to your gem file, along with the omniauth gem for your chosen
167
+ [provider](https://github.com/omniauth/omniauth/wiki/List-of-Strategies).
168
+
169
+ In `config/initializers/balrog.rb`, call `config.set_omniauth` in the setup block.
170
+ `.set_omniauth` takes the same arguments as the `OmniAuth::Builder#provider`
171
+ [method](https://github.com/omniauth/omniauth#getting-started),
172
+ a provider and any required keys.
173
+
174
+ To whitelist any email addresses with a specific domain, call
175
+ `config.set_domain_whitelist`in the setup block and pass in the domain.
176
+ If you want to whitelist multiple domains, you can pass multiple domains
177
+ to the `.set_domain_whitelist`.
178
+
179
+ Balrog does not require a password to be set if you wish to use single sign-on only.
180
+
181
+ ```ruby
182
+ Balrog::Middleware.setup do |config|
183
+ credentials = Rails.application.credentials
184
+ config.set_omniauth :google_oauth2, credentials.google_client_id, credentials.google_client_secret
185
+ config.set_domain_whitelist 'pixielabs.io', 'the_fellowship.com'
186
+ end
187
+ ```
188
+ **Please note:** there is currently a CSRF vulnerability which affects OmniAuth
189
+ (designated [CVE-2015-9284](https://nvd.nist.gov/vuln/detail/CVE-2015-9284))
190
+ that requires mitigation at the application level. More details on how to do
191
+ this can be found on the [Omniauth Wiki](https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284).
192
+
193
+ ## Upgrading from 1.1 to 2.0
194
+
195
+ To upgrade, you will need to change your Balrog initializer.
196
+
197
+ 1. Instead of calling `Rails.application.config.middleware.use Balrog::Middleware`, you will now need to call `Balrog::Middleware.setup`.
198
+
199
+ 2. Change the block you pass into these methods. `#password_hash` and `#set_session_expiry` now need to called on a block parameter, e.g `set_session_expiry 30.minutes` needs to be changed to `config.set_session_expiry 30.minutes`.
200
+
201
+ See below for code examples.
202
+
203
+ ```ruby
204
+ # Balrog 1.1
205
+ Rails.application.config.middleware.use Balrog::Middleware do
206
+ password_hash '$2a$12$I8Fp3e2GfSdM7KFyoMx56.BVdHeeyk9DQWKkdsxw7USvU/mC8a8.q'
207
+ set_session_expiry 30.minutes
208
+ end
209
+ ```
210
+
211
+ ```ruby
212
+ # Balrog 2.0
213
+ Balrog::Middleware.setup do |config|
214
+ config.set_password_hash '$2a$12$9lquJW6mVYYS1pD1xYMGzulyC6sEDuLIUfkA/Y7F3RQ8psLNYyLeO'
215
+ config.set_session_expiry 30.minutes
216
+ end
217
+ ```
218
+
148
219
  ## Contributing
149
220
 
150
221
  ### Running the tests
@@ -169,7 +240,6 @@ Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md).
169
240
  want to have your own version, or is otherwise necessary, that is fine, but
170
241
  please isolate to its own commit so we can cherry-pick around it.
171
242
 
172
-
173
243
  ## TODO
174
244
 
175
245
  * Restricting access via `routes.rb`
@@ -27,8 +27,7 @@ footer {
27
27
  align-self: center;
28
28
  }
29
29
 
30
- button {
31
- -webkit-appearance: button;
30
+ .button_background {
32
31
  overflow: visible;
33
32
  text-transform: none;
34
33
  -webkit-transition-duration: 0.4s;
@@ -39,13 +38,25 @@ button {
39
38
  font-size: 20px;
40
39
  font-family: 'Helvetica Neue', 'Helvetica', Calibri, 'Trebuchet MS', sans-serif;
41
40
  cursor: pointer;
42
- padding: 8px 10px;
43
41
  border: 0;
44
42
  font-weight: 100;
45
43
  letter-spacing: 1px;
44
+ margin: 5px 0px;
45
+ }
46
+
47
+ button {
48
+ -webkit-appearance: button;
49
+ padding: 8px 10px;
50
+ }
51
+
52
+ .sso_button {
53
+ display: inline-block;
54
+ text-decoration: none;
55
+ padding: 8px 0px;
56
+ width: 305.438px;
46
57
  }
47
58
 
48
- button:hover {
59
+ button:hover, .sso_button:hover {
49
60
  background-color: rgb(201, 41, 41);
50
61
  }
51
62
 
@@ -1,6 +1,17 @@
1
1
  <section>
2
- <form action='/balrog/signin' method='POST'>
3
- <input autofocus type='password' name='password' placeholder='Password'>
4
- <button type='submit'>Login</button>
5
- </form>
2
+ <div>
3
+ <% if show_balrog_password_prompt? %>
4
+ <form action='/balrog/signin' method='POST'>
5
+ <input autofocus type='password' name='password' placeholder='Password'>
6
+ <button type='submit' class="button_background">Login</button>
7
+ </form>
8
+ <% end %>
9
+ <% if balrog_omniauth_configured? %>
10
+ <%= button_to(
11
+ "Sign in with SSO",
12
+ "/auth/#{Balrog::Middleware.omniauth_config[:provider]}",
13
+ class: 'button_background sso_button'
14
+ ) %>
15
+ <% end %>
16
+ </div>
6
17
  </section>
@@ -22,4 +22,16 @@ class Balrog::Engine < Rails::Engine
22
22
  balrog/logo.png
23
23
  )
24
24
  end
25
+
26
+ # Insert Balrog into middleware stack.
27
+ initializer "Balrog.middleware", after: :load_config_initializers, before: :build_middleware_stack do |app|
28
+ # If OmniAuth configured, insert OmniAuth into middleware stack.
29
+ omniauth_config = Balrog::Middleware.omniauth_config
30
+ if omniauth_config
31
+ app.middleware.use OmniAuth::Builder do
32
+ provider omniauth_config[:provider], *omniauth_config[:args]
33
+ end
34
+ end
35
+ app.middleware.use Balrog::Middleware
36
+ end
25
37
  end
@@ -6,9 +6,9 @@ class Balrog::InstallGenerator < Rails::Generators::Base
6
6
  def create_initializer_file
7
7
  password_hash = PasswordHasher.encrypt_password
8
8
  contents = <<~EOF
9
- Rails.application.config.middleware.use Balrog::Middleware do
10
- password_hash '#{password_hash}'
11
- set_session_expiry 30.minutes
9
+ Balrog::Middleware.setup do |config|
10
+ config.set_password_hash '#{password_hash}'
11
+ config.set_session_expiry 30.minutes
12
12
  end
13
13
  EOF
14
14
  create_file "config/initializers/balrog.rb", contents
@@ -1,4 +1,5 @@
1
1
  require 'bcrypt'
2
+ require_relative 'middleware/controller'
2
3
 
3
4
  # Public: Balrog middleware that handles form submissions, checking the
4
5
  # password against the configured hash, and setting a session variable if
@@ -7,80 +8,73 @@ require 'bcrypt'
7
8
  # This is typically set up in an initialize when you run
8
9
  # `rails g balrog:install`, and looks a bit like this:
9
10
  #
10
- # Rails.application.config.middleware.use Balrog::Middleware do
11
- # password_hash '<bcrypt hash>'
12
- # end
11
+ # Balrog::Middleware.setup do |config|
12
+ # config.set_password_hash '<bcrypt hash>'
13
+ # end
14
+
13
15
  class Balrog::Middleware
14
- def initialize(app, &block)
16
+ include Controller
17
+
18
+ mattr_reader :password_hash
19
+ mattr_reader :session_length
20
+ mattr_reader :omniauth_config
21
+ mattr_reader :domain_whitelist
22
+
23
+ def initialize(app)
15
24
  @app = app
16
- instance_eval(&block) if block_given?
17
25
  end
18
26
 
19
27
  def call(env)
20
28
  path = env["PATH_INFO"]
21
29
  method = env["REQUEST_METHOD"]
22
- if method == 'POST' && path == '/balrog/signin'
23
- handle_login(env)
24
- elsif method == "DELETE" && path == '/balrog/logout'
25
- handle_logout(env)
30
+ if login_request?(path, method)
31
+ password_login(env)
32
+ elsif omniauth_request?(path, method)
33
+ omniauthentication(env)
34
+ elsif logout_request?(path, method)
35
+ logout(env)
26
36
  else
27
37
  @app.call(env)
28
38
  end
29
39
  end
30
40
 
41
+ def self.setup
42
+ yield self
43
+ end
44
+
31
45
  private
32
46
 
33
- def password_hash(input)
34
- @password_hash = BCrypt::Password.new(input)
47
+ def self.set_password_hash(input)
48
+ @@password_hash = BCrypt::Password.new(input)
35
49
  end
36
50
 
37
- def set_session_expiry(time_period)
38
- @session_length = time_period
51
+ def self.set_omniauth(provider, *args)
52
+ @@omniauth_config = {
53
+ provider: provider,
54
+ args: args
55
+ }
39
56
  end
40
57
 
41
- def handle_login(env)
42
- if env['rack.request.form_hash']
43
- submitted_password = env['rack.request.form_hash']['password']
44
- end
45
-
46
- unless submitted_password
47
- return [302, {"Location" => referer}, [""]]
48
- end
49
-
50
- unless @password_hash
51
- warn <<~EOF
52
-
53
- !! Balrog has not been configured with a password_hash. You shall not
54
- !! pass! When adding Balrog::Middleware to your middleware stack, pass
55
- !! in a block and call `password_hash` passing in a bcrypt hash.
56
- !!
57
- !! Check out https://github.com/pixielabs/balrog for more information.
58
-
59
- EOF
60
- end
61
-
62
- if @password_hash == submitted_password
63
- session_data = { value: 'authenticated' }
64
- add_expiry_date!(session_data)
65
- env['rack.session'][:balrog] = session_data
66
- end
58
+ def self.set_domain_whitelist(*domains)
59
+ @@domain_whitelist = domains
60
+ end
67
61
 
68
- referer = env["HTTP_REFERER"] || '/'
62
+ def self.set_session_expiry(time_period)
63
+ @@session_length = time_period
64
+ end
69
65
 
70
- [302, {"Location" => referer}, [""]]
66
+ def login_request?(path, method)
67
+ method == 'POST' && path == '/balrog/signin'
71
68
  end
72
69
 
73
- def handle_logout(env)
74
- env['rack.session'].delete(:balrog)
75
- [302, {"Location" => '/'}, [""]]
70
+ def omniauth_request?(path, method)
71
+ omniauth_config &&
72
+ method == "GET" &&
73
+ path == "/auth/#{omniauth_config[:provider]}/callback"
76
74
  end
77
75
 
78
- # If the user configured the Balrog session to expire, add the
79
- # expiry_date to the Balrog session.
80
- def add_expiry_date!(session_data)
81
- if @session_length
82
- session_data[:expiry_date] = DateTime.current + @session_length
83
- end
76
+ def logout_request?(path, method)
77
+ method == "DELETE" && path == '/balrog/logout'
84
78
  end
85
79
  end
86
80
 
@@ -0,0 +1,106 @@
1
+ # Methods that are called in response to specific application requests.
2
+ class Balrog::Middleware
3
+ module Controller
4
+
5
+ # This method is called if a user attempts to sign in with a password
6
+ # and will authenticate the user if the password is correct.
7
+ def password_login(env)
8
+ # Extract the submitted_password from the rack request hash.
9
+ if env['rack.request.form_hash']
10
+ submitted_password = env['rack.request.form_hash']['password']
11
+ end
12
+
13
+ # If there is no submitted_password, redirect the user before authentication.
14
+ unless submitted_password
15
+ return [302, {"Location" => referer}, [""]]
16
+ end
17
+
18
+ # If there is no password_hash, alert the developer.
19
+ unless password_hash
20
+ warn <<~EOF
21
+
22
+ !! Balrog has not been configured with a password_hash. You shall not
23
+ !! pass! When adding Balrog::Middleware to your middleware stack, pass
24
+ !! in a block and call `password_hash` passing in a bcrypt hash.
25
+ !!
26
+ !! Check out https://github.com/pixielabs/balrog for more information.
27
+
28
+ EOF
29
+ end
30
+
31
+ # Authenticate the user if the submitted_password matches the password_hash.
32
+ if password_hash == submitted_password
33
+ authenticate_user(env)
34
+ end
35
+
36
+ referer = env["HTTP_REFERER"] || '/'
37
+
38
+ [302, {"Location" => referer}, [""]]
39
+ end
40
+
41
+
42
+ # This method is called if a user attempts to sign in with Single Sign-on
43
+ # and will authenticate the user if email domain has been whitelisted.
44
+ def omniauthentication(env)
45
+ # Extract the email domain from the omniauth hash.
46
+ if env['omniauth.auth']['info']['email']
47
+ user_email = env['omniauth.auth']['info']['email']
48
+ email_domain = user_email.split("@").last
49
+ end
50
+
51
+ # If there is no email domain, redirect the user before authentication.
52
+ unless email_domain
53
+ return [302, {"Location" => referer}, [""]]
54
+ end
55
+
56
+ # If there is no domain_whitelist, alert the developer.
57
+ unless domain_whitelist
58
+ warn <<~EOF
59
+
60
+ !! Balrog has not been configured with a domain_whitelist. You shall not
61
+ !! pass! When setting up Balrog::Middleware, pass in a block and
62
+ !! call `set_domain_whitelist` passing in an omniauth provider and
63
+ !! required keys.
64
+ !!
65
+ !! Check out https://github.com/pixielabs/balrog for more information.
66
+
67
+ EOF
68
+ return [302, {"Location" => referer}, [""]]
69
+ end
70
+
71
+ # Authenticate the user if the user's email domain is whitelisted.
72
+ if domain_whitelist.include?(email_domain)
73
+ authenticate_user(env)
74
+ end
75
+
76
+ referer = env["omniauth.origin"] || '/'
77
+
78
+ [302, {"Location" => referer}, [""]]
79
+ end
80
+
81
+
82
+ # This method is called if a user logs out using a balrog logout button.
83
+ # It will achieve this by removing all balrog data from the session.
84
+ def logout(env)
85
+ env['rack.session'].delete(:balrog)
86
+ [302, {"Location" => '/'}, [""]]
87
+ end
88
+
89
+ private
90
+
91
+ # This method marks the user as 'authenicated'.
92
+ def authenticate_user(env)
93
+ session_data = { value: 'authenticated' }
94
+ add_expiry_date!(session_data)
95
+ env['rack.session'][:balrog] = session_data
96
+ end
97
+
98
+ # If the user configured the Balrog session to expire, add the
99
+ # expiry_date to the Balrog session.
100
+ def add_expiry_date!(session_data)
101
+ if session_length
102
+ session_data[:expiry_date] = DateTime.current + session_length
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,3 +1,3 @@
1
1
  module Balrog
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -1,4 +1,4 @@
1
- # ViewHelpers methods are made available in all controllers by the code in engine.rb.
1
+ # ViewHelpers methods are made available in all views by the code in engine.rb.
2
2
  module Balrog::ViewHelpers
3
3
  def balrog_logout_button(options = nil, html_options = nil)
4
4
  name = 'Logout'
@@ -13,4 +13,12 @@ module Balrog::ViewHelpers
13
13
 
14
14
  button_to(name, '/balrog/logout', html_options)
15
15
  end
16
+
17
+ def show_balrog_password_prompt?
18
+ !!Balrog::Middleware.password_hash || !Balrog::Middleware.omniauth_config
19
+ end
20
+
21
+ def balrog_omniauth_configured?
22
+ !!Balrog::Middleware.omniauth_config
23
+ end
16
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: balrog
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pixie Labs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-18 00:00:00.000000000 Z
11
+ date: 2019-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -114,6 +114,7 @@ files:
114
114
  - lib/balrog/guard.rb
115
115
  - lib/balrog/helpers.rb
116
116
  - lib/balrog/middleware.rb
117
+ - lib/balrog/middleware/controller.rb
117
118
  - lib/balrog/password_hasher.rb
118
119
  - lib/balrog/rake_tasks.rb
119
120
  - lib/balrog/routes_middleware.rb