clearance 2.0.0.beta2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of clearance might be problematic. Click here for more details.

Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.erb-lint.yml +5 -0
  3. data/.travis.yml +5 -9
  4. data/Appraisals +14 -19
  5. data/Gemfile +11 -7
  6. data/Gemfile.lock +137 -84
  7. data/NEWS.md +90 -11
  8. data/README.md +11 -24
  9. data/RELEASING.md +25 -0
  10. data/Rakefile +6 -1
  11. data/app/controllers/clearance/base_controller.rb +8 -1
  12. data/app/controllers/clearance/passwords_controller.rb +24 -7
  13. data/app/views/clearance_mailer/change_password.html.erb +2 -2
  14. data/app/views/clearance_mailer/change_password.text.erb +2 -2
  15. data/clearance.gemspec +10 -3
  16. data/config/locales/clearance.en.yml +1 -0
  17. data/config/routes.rb +1 -1
  18. data/gemfiles/rails_5.0.gemfile +10 -9
  19. data/gemfiles/rails_5.1.gemfile +11 -10
  20. data/gemfiles/rails_5.2.gemfile +11 -10
  21. data/gemfiles/rails_6.0.gemfile +11 -10
  22. data/lib/clearance/authentication.rb +1 -1
  23. data/lib/clearance/back_door.rb +2 -1
  24. data/lib/clearance/configuration.rb +29 -18
  25. data/lib/clearance/password_strategies.rb +2 -5
  26. data/lib/clearance/password_strategies/argon2.rb +23 -0
  27. data/lib/clearance/password_strategies/bcrypt.rb +17 -11
  28. data/lib/clearance/rack_session.rb +5 -1
  29. data/lib/clearance/session.rb +19 -2
  30. data/lib/clearance/testing/deny_access_matcher.rb +1 -5
  31. data/lib/clearance/user.rb +12 -3
  32. data/lib/clearance/version.rb +1 -1
  33. data/lib/generators/clearance/install/install_generator.rb +10 -0
  34. data/lib/generators/clearance/install/templates/README +10 -4
  35. data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +1 -1
  36. data/lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb +1 -1
  37. data/lib/generators/clearance/routes/templates/routes.rb +1 -1
  38. data/spec/acceptance/clearance_installation_spec.rb +0 -4
  39. data/spec/app_templates/app/models/user.rb +1 -1
  40. data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
  41. data/spec/clearance/back_door_spec.rb +20 -4
  42. data/spec/clearance/rack_session_spec.rb +2 -0
  43. data/spec/clearance/session_spec.rb +88 -8
  44. data/spec/clearance/testing/deny_access_matcher_spec.rb +32 -0
  45. data/spec/configuration_spec.rb +32 -14
  46. data/spec/controllers/passwords_controller_spec.rb +36 -0
  47. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  48. data/spec/generators/clearance/install/install_generator_spec.rb +30 -1
  49. data/spec/generators/clearance/views/views_generator_spec.rb +0 -1
  50. data/spec/models/user_spec.rb +34 -5
  51. data/spec/password_strategies/argon2_spec.rb +79 -0
  52. data/spec/password_strategies/bcrypt_spec.rb +18 -1
  53. data/spec/requests/authentication_cookie_spec.rb +55 -0
  54. data/spec/spec_helper.rb +0 -1
  55. data/spec/support/generator_spec_helpers.rb +1 -5
  56. metadata +45 -15
  57. data/app/views/layouts/application.html.erb +0 -23
  58. data/spec/app_templates/app/models/rails5/user.rb +0 -5
@@ -13,10 +13,7 @@ module Clearance
13
13
  # `password=(new_password)`. For an example of how to implement these methods,
14
14
  # see {Clearance::PasswordStrategies::BCrypt}.
15
15
  module PasswordStrategies
16
- autoload :BCrypt, 'clearance/password_strategies/bcrypt'
17
- autoload :BCryptMigrationFromSHA1,
18
- 'clearance/password_strategies/bcrypt_migration_from_sha1'
19
- autoload :Blowfish, 'clearance/password_strategies/blowfish'
20
- autoload :SHA1, 'clearance/password_strategies/sha1'
16
+ autoload :BCrypt, "clearance/password_strategies/bcrypt"
17
+ autoload :Argon2, "clearance/password_strategies/argon2"
21
18
  end
22
19
  end
@@ -0,0 +1,23 @@
1
+ module Clearance
2
+ module PasswordStrategies
3
+ # Uses Argon2 to authenticate users and store encrypted passwords.
4
+
5
+ module Argon2
6
+ require "argon2"
7
+
8
+ def authenticated?(password)
9
+ if encrypted_password.present?
10
+ ::Argon2::Password.verify_password(password, encrypted_password)
11
+ end
12
+ end
13
+
14
+ def password=(new_password)
15
+ @password = new_password
16
+
17
+ if new_password.present?
18
+ self.encrypted_password = ::Argon2::Password.new.create(new_password)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,10 +2,14 @@ module Clearance
2
2
  module PasswordStrategies
3
3
  # Uses BCrypt to authenticate users and store encrypted passwords.
4
4
  #
5
- # The BCrypt cost (the measure of how many key expansion iterations BCrypt
6
- # will perform) is automatically set to the minimum allowed value when
7
- # Rails is operating in the test environment and the default cost in all
8
- # other envionments. This provides a speed boost in tests.
5
+ # BCrypt has a `cost` argument which determines how computationally
6
+ # expensive the hash is to calculate. The higher the cost, the harder it is
7
+ # for attackers to crack passwords even if they posess a database dump of
8
+ # the encrypted passwords. Clearance uses the `bcrypt-ruby` default cost
9
+ # except in the test environment, where it uses the minimum cost value for
10
+ # speed. If you wish to increase the cost over the default, you can do so
11
+ # by setting a higher cost in an initializer:
12
+ # `BCrypt::Engine.cost = 12`
9
13
  module BCrypt
10
14
  require 'bcrypt'
11
15
 
@@ -19,18 +23,20 @@ module Clearance
19
23
  @password = new_password
20
24
 
21
25
  if new_password.present?
22
- cost = if defined?(::Rails) && ::Rails.env.test?
23
- ::BCrypt::Engine::MIN_COST
24
- else
25
- ::BCrypt::Engine::DEFAULT_COST
26
- end
27
-
28
26
  self.encrypted_password = ::BCrypt::Password.create(
29
27
  new_password,
30
- cost: cost,
28
+ cost: configured_bcrypt_cost,
31
29
  )
32
30
  end
33
31
  end
32
+
33
+ def configured_bcrypt_cost
34
+ if defined?(::Rails) && ::Rails.env.test?
35
+ ::BCrypt::Engine::MIN_COST
36
+ else
37
+ ::BCrypt::Engine.cost
38
+ end
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -21,7 +21,11 @@ module Clearance
21
21
  session = Clearance::Session.new(env)
22
22
  env[:clearance] = session
23
23
  response = @app.call(env)
24
- session.add_cookie_to_headers response[1]
24
+
25
+ if session.authentication_successful?
26
+ session.add_cookie_to_headers response[1]
27
+ end
28
+
25
29
  response
26
30
  end
27
31
  end
@@ -81,7 +81,7 @@ module Clearance
81
81
  end
82
82
 
83
83
  @current_user = nil
84
- cookies.delete remember_token_cookie
84
+ cookies.delete remember_token_cookie, delete_cookie_options
85
85
  end
86
86
 
87
87
  # True if {#current_user} is set.
@@ -98,6 +98,13 @@ module Clearance
98
98
  ! signed_in?
99
99
  end
100
100
 
101
+ # True if a successful authentication has been performed
102
+ #
103
+ # @return [Boolean]
104
+ def authentication_successful?
105
+ !!@current_user
106
+ end
107
+
101
108
  private
102
109
 
103
110
  # @api private
@@ -147,7 +154,7 @@ module Clearance
147
154
  guards = Clearance.configuration.sign_in_guards
148
155
 
149
156
  guards.inject(default_guard) do |stack, guard_class|
150
- guard_class.new(self, stack)
157
+ guard_class.to_s.constantize.new(self, stack)
151
158
  end
152
159
  end
153
160
 
@@ -157,12 +164,22 @@ module Clearance
157
164
  domain: domain,
158
165
  expires: remember_token_expires,
159
166
  httponly: Clearance.configuration.httponly,
167
+ same_site: Clearance.configuration.same_site,
160
168
  path: Clearance.configuration.cookie_path,
161
169
  secure: Clearance.configuration.secure_cookie,
162
170
  value: remember_token,
163
171
  }
164
172
  end
165
173
 
174
+ # @api private
175
+ def delete_cookie_options
176
+ Hash.new.tap do |options|
177
+ if configured_cookie_domain
178
+ options[:domain] = domain
179
+ end
180
+ end
181
+ end
182
+
166
183
  # @api private
167
184
  def domain
168
185
  if configured_cookie_domain.respond_to?(:call)
@@ -78,12 +78,8 @@ module Clearance
78
78
  @controller.request.env[:clearance]
79
79
  end
80
80
 
81
- def flash_alert
82
- @controller.flash[:alert]
83
- end
84
-
85
81
  def flash_alert_value
86
- flash_alert.values.first
82
+ @controller.flash[:alert]
87
83
  end
88
84
 
89
85
  def redirects_to_url?
@@ -60,7 +60,7 @@ module Clearance
60
60
  # @see PasswordStrategies
61
61
  # @return [void]
62
62
  #
63
- # @!method authenticated?
63
+ # @!method authenticated?(password)
64
64
  # Check's the provided password against the user's encrypted password using
65
65
  # the configured password strategy. By default, this will be
66
66
  # {PasswordStrategies::BCrypt#authenticated?}, but can be changed with
@@ -117,11 +117,13 @@ module Clearance
117
117
  if password.present? && user.authenticated?(password)
118
118
  user
119
119
  end
120
+ else
121
+ prevent_timing_attack
120
122
  end
121
123
  end
122
124
 
123
125
  def find_by_normalized_email(email)
124
- find_by_email normalize_email(email)
126
+ find_by(email: normalize_email(email))
125
127
  end
126
128
 
127
129
  def normalize_email(email)
@@ -130,6 +132,13 @@ module Clearance
130
132
 
131
133
  private
132
134
 
135
+ DUMMY_PASSWORD = "*"
136
+
137
+ def prevent_timing_attack
138
+ new(password: DUMMY_PASSWORD)
139
+ nil
140
+ end
141
+
133
142
  def password_strategy
134
143
  Clearance.configuration.password_strategy || PasswordStrategies::BCrypt
135
144
  end
@@ -143,7 +152,7 @@ module Clearance
143
152
  validates :email,
144
153
  email: { strict_mode: true },
145
154
  presence: true,
146
- uniqueness: { allow_blank: true },
155
+ uniqueness: { allow_blank: true, case_sensitive: false },
147
156
  unless: :email_optional?
148
157
 
149
158
  validates :password, presence: true, unless: :skip_password_validation?
@@ -1,3 +1,3 @@
1
1
  module Clearance
2
- VERSION = "2.0.0.beta2".freeze
2
+ VERSION = "2.3.0".freeze
3
3
  end
@@ -122,6 +122,16 @@ module Clearance
122
122
  "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
123
123
  end
124
124
 
125
+ def migration_primary_key_type_string
126
+ if configured_key_type
127
+ ", id: :#{configured_key_type}"
128
+ end
129
+ end
130
+
131
+ def configured_key_type
132
+ Rails.configuration.generators.active_record[:primary_key_type]
133
+ end
134
+
125
135
  def models_inherit_from
126
136
  "ApplicationRecord"
127
137
  end
@@ -8,9 +8,11 @@ Next steps:
8
8
  # config/environments/{development,test}.rb
9
9
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
10
10
 
11
- In production it should be your app's domain name.
11
+ In the production environment it should be your application's full hostname.
12
12
 
13
- 2. Display user session and flashes. For example, in your application layout:
13
+ 2. Display user session status.
14
+
15
+ From somewhere in your layout, render sign in and sign out buttons:
14
16
 
15
17
  <% if signed_in? %>
16
18
  Signed in as: <%= current_user.email %>
@@ -19,14 +21,18 @@ Next steps:
19
21
  <%= link_to 'Sign in', sign_in_path %>
20
22
  <% end %>
21
23
 
24
+ 3. Render the flash contents.
25
+
26
+ Make sure the flash is being rendered in your views using something like:
27
+
22
28
  <div id="flash">
23
29
  <% flash.each do |key, value| %>
24
30
  <div class="flash <%= key %>"><%= value %></div>
25
31
  <% end %>
26
32
  </div>
27
33
 
28
- 3. Migrate:
34
+ 4. Migrate:
29
35
 
30
- rails db:migrate
36
+ Run `rails db:migrate` to add the clearance database changes.
31
37
 
32
38
  *******************************************************************************
@@ -13,7 +13,7 @@ class AddClearanceToUsers < ActiveRecord::Migration<%= migration_version %>
13
13
  users = select_all("SELECT id FROM users WHERE remember_token IS NULL")
14
14
 
15
15
  users.each do |user|
16
- update <<-SQL
16
+ update <<-SQL.squish
17
17
  UPDATE users
18
18
  SET remember_token = '#{Clearance::Token.new}'
19
19
  WHERE id = '#{user['id']}'
@@ -1,6 +1,6 @@
1
1
  class CreateUsers < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :users do |t|
3
+ create_table :users<%= migration_primary_key_type_string %> do |t|
4
4
  t.timestamps null: false
5
5
  t.string :email, null: false
6
6
  t.string :encrypted_password, limit: 128, null: false
@@ -4,7 +4,7 @@
4
4
  resources :users, controller: "clearance/users", only: [:create] do
5
5
  resource :password,
6
6
  controller: "clearance/passwords",
7
- only: [:create, :edit, :update]
7
+ only: [:edit, :update]
8
8
  end
9
9
 
10
10
  get "/sign_in" => "clearance/sessions#new", as: "sign_in"
@@ -36,9 +36,6 @@ describe "Clearance Installation" do
36
36
  --skip-keeps
37
37
  --skip-sprockets
38
38
  CMD
39
-
40
- FileUtils.rm_f("public/index.html")
41
- FileUtils.rm_f("app/views/layouts/application.html.erb")
42
39
  end
43
40
 
44
41
  def testapp_templates
@@ -47,7 +44,6 @@ describe "Clearance Installation" do
47
44
 
48
45
  def configure_test_app
49
46
  FileUtils.rm_f("public/index.html")
50
- FileUtils.rm_f("app/views/layouts/application.html.erb")
51
47
  FileUtils.cp_r(testapp_templates, "..")
52
48
  end
53
49
 
@@ -1,4 +1,4 @@
1
- class User < ActiveRecord::Base
1
+ class User < ApplicationRecord
2
2
  def previously_existed?
3
3
  true
4
4
  end
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <%= javascript_include_tag 'application' %>
5
+ <%= csrf_meta_tag %>
6
+ </head>
7
+ <body>
8
+ <div id="header">
9
+ <% if signed_in? -%>
10
+ <%= button_to t(".sign_out"), sign_out_path, method: :delete %>
11
+ <% else -%>
12
+ <%= link_to t(".sign_in"), sign_in_path %>
13
+ <% end -%>
14
+ </div>
15
+
16
+ <div id="flash">
17
+ <% flash.each do |key, value| -%>
18
+ <div id="flash_<%= key %>"><%=h value %></div>
19
+ <% end %>
20
+ </div>
21
+
22
+ <%= yield %>
23
+ </body>
24
+ </html>
@@ -46,6 +46,18 @@ describe Clearance::BackDoor do
46
46
  end
47
47
  end
48
48
 
49
+ it "strips 'as' from the params" do
50
+ user_id = "123"
51
+ user = double("user")
52
+ allow(User).to receive(:find).with(user_id).and_return(user)
53
+ env = build_env(as: user_id, foo: :bar)
54
+ back_door = Clearance::BackDoor.new(mock_app)
55
+
56
+ back_door.call(env)
57
+
58
+ expect(env["QUERY_STRING"]).to eq("foo=bar")
59
+ end
60
+
49
61
  context "when the environments are disabled" do
50
62
  before do
51
63
  Clearance.configuration.allowed_backdoor_environments = nil
@@ -84,14 +96,18 @@ describe Clearance::BackDoor do
84
96
  env_for_user_id("")
85
97
  end
86
98
 
87
- def env_for_user_id(user_id)
99
+ def build_env(params)
100
+ query = Rack::Utils.build_query(params)
88
101
  clearance = double("clearance", sign_in: true)
89
- Rack::MockRequest.env_for("/?as=#{user_id}").merge(clearance: clearance)
102
+ Rack::MockRequest.env_for("/?#{query}").merge(clearance: clearance)
103
+ end
104
+
105
+ def env_for_user_id(user_id)
106
+ build_env(as: user_id)
90
107
  end
91
108
 
92
109
  def env_for_username(username)
93
- clearance = double("clearance", sign_in: true)
94
- Rack::MockRequest.env_for("/?as=#{username}").merge(clearance: clearance)
110
+ build_env(as: username)
95
111
  end
96
112
 
97
113
  def mock_app
@@ -11,6 +11,8 @@ describe Clearance::RackSession do
11
11
  env = Rack::MockRequest.env_for('/')
12
12
  expected_session = "the session"
13
13
  allow(expected_session).to receive(:add_cookie_to_headers)
14
+ allow(expected_session).to receive(:authentication_successful?).
15
+ and_return(true)
14
16
  allow(Clearance::Session).to receive(:new).
15
17
  with(env).
16
18
  and_return(expected_session)
@@ -129,6 +129,12 @@ describe Clearance::Session do
129
129
 
130
130
  def stub_guard_class(guard)
131
131
  double("guard_class").tap do |guard_class|
132
+ allow(guard_class).to receive(:to_s).
133
+ and_return(guard_class)
134
+
135
+ allow(guard_class).to receive(:constantize).
136
+ and_return(guard_class)
137
+
132
138
  allow(guard_class).to receive(:new).
133
139
  with(session, stub_default_sign_in_guard).
134
140
  and_return(guard)
@@ -170,6 +176,31 @@ describe Clearance::Session do
170
176
  end
171
177
  end
172
178
 
179
+ context "if same_site is set" do
180
+ before do
181
+ Clearance.configuration.same_site = :lax
182
+ session.sign_in(user)
183
+ end
184
+
185
+ it "sets a same-site cookie" do
186
+ session.add_cookie_to_headers(headers)
187
+
188
+ expect(headers["Set-Cookie"]).to match(/remember_token=.+; SameSite/)
189
+ end
190
+ end
191
+
192
+ context "if same_site is not set" do
193
+ before do
194
+ session.sign_in(user)
195
+ end
196
+
197
+ it "sets a standard cookie" do
198
+ session.add_cookie_to_headers(headers)
199
+
200
+ expect(headers["Set-Cookie"]).to_not match(/remember_token=.+; SameSite/)
201
+ end
202
+ end
203
+
173
204
  describe 'remember token cookie expiration' do
174
205
  context 'default configuration' do
175
206
  it 'is set to 1 year from now' do
@@ -309,14 +340,63 @@ describe Clearance::Session do
309
340
  expect(headers["Set-Cookie"]).to be nil
310
341
  end
311
342
 
312
- it 'signs out a user' do
313
- user = create(:user)
314
- old_remember_token = user.remember_token
315
- env = env_with_remember_token(old_remember_token)
316
- session = Clearance::Session.new(env)
317
- session.sign_out
318
- expect(session.current_user).to be_nil
319
- expect(user.reload.remember_token).not_to eq old_remember_token
343
+ describe "#sign_out" do
344
+ it "signs out a user" do
345
+ user = create(:user)
346
+ old_remember_token = user.remember_token
347
+ env = env_with_remember_token(old_remember_token)
348
+ session = Clearance::Session.new(env)
349
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
350
+ expect(cookie_jar.deleted?(:remember_token)).to be false
351
+
352
+ session.sign_out
353
+
354
+ expect(cookie_jar.deleted?(:remember_token)).to be true
355
+ expect(session.current_user).to be_nil
356
+ expect(user.reload.remember_token).not_to eq old_remember_token
357
+ end
358
+
359
+ context "with custom cookie domain" do
360
+ let(:domain) { ".example.com" }
361
+
362
+ before do
363
+ Clearance.configuration.cookie_domain = domain
364
+ end
365
+
366
+ it "clears cookie" do
367
+ user = create(:user)
368
+ env = env_with_remember_token(
369
+ value: user.remember_token,
370
+ domain: domain,
371
+ )
372
+ session = Clearance::Session.new(env)
373
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
374
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
375
+
376
+ session.sign_out
377
+
378
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
379
+ end
380
+ end
381
+
382
+ context 'with callable cookie domain' do
383
+ it 'clears cookie' do
384
+ domain = '.example.com'
385
+ Clearance.configuration.cookie_domain = ->(_) { domain }
386
+ user = create(:user)
387
+ env = env_with_remember_token(
388
+ value: user.remember_token,
389
+ domain: domain
390
+ )
391
+ session = Clearance::Session.new(env)
392
+ cookie_jar = ActionDispatch::Request.new(env).cookie_jar
393
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
394
+
395
+ session.sign_out
396
+
397
+ expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
398
+ end
399
+ end
320
400
  end
321
401
 
322
402
  def env_with_cookies(cookies)