clearance 2.0.0.beta1 → 2.2.1

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.

Potentially problematic release.


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

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -14
  3. data/Appraisals +11 -3
  4. data/Gemfile +1 -3
  5. data/Gemfile.lock +91 -86
  6. data/NEWS.md +86 -4
  7. data/README.md +54 -28
  8. data/app/controllers/clearance/base_controller.rb +8 -1
  9. data/app/controllers/clearance/passwords_controller.rb +23 -5
  10. data/clearance.gemspec +15 -9
  11. data/config/locales/clearance.en.yml +1 -0
  12. data/config/routes.rb +1 -1
  13. data/gemfiles/rails_5.0.gemfile +3 -3
  14. data/gemfiles/rails_5.1.gemfile +3 -3
  15. data/gemfiles/rails_5.2.gemfile +3 -3
  16. data/gemfiles/{rails_4.2.gemfile → rails_6.0.gemfile} +5 -4
  17. data/lib/clearance/authentication.rb +1 -1
  18. data/lib/clearance/back_door.rb +1 -1
  19. data/lib/clearance/configuration.rb +30 -19
  20. data/lib/clearance/password_strategies.rb +5 -4
  21. data/lib/clearance/password_strategies/argon2.rb +23 -0
  22. data/lib/clearance/password_strategies/bcrypt.rb +17 -11
  23. data/lib/clearance/rack_session.rb +5 -1
  24. data/lib/clearance/session.rb +39 -3
  25. data/lib/clearance/testing/deny_access_matcher.rb +1 -5
  26. data/lib/clearance/user.rb +12 -3
  27. data/lib/clearance/version.rb +1 -1
  28. data/lib/generators/clearance/install/install_generator.rb +11 -7
  29. data/lib/generators/clearance/install/templates/README +10 -4
  30. data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +1 -1
  31. data/lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb +1 -1
  32. data/lib/generators/clearance/routes/templates/routes.rb +1 -1
  33. data/spec/acceptance/clearance_installation_spec.rb +0 -4
  34. data/spec/app_templates/app/models/user.rb +1 -1
  35. data/spec/app_templates/testapp/app/controllers/home_controller.rb +1 -5
  36. data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
  37. data/spec/clearance/back_door_spec.rb +12 -6
  38. data/spec/clearance/rack_session_spec.rb +2 -0
  39. data/spec/clearance/session_spec.rb +91 -16
  40. data/spec/clearance/testing/deny_access_matcher_spec.rb +32 -0
  41. data/spec/configuration_spec.rb +46 -15
  42. data/spec/controllers/passwords_controller_spec.rb +36 -0
  43. data/spec/controllers/permissions_controller_spec.rb +1 -1
  44. data/spec/dummy/app/controllers/application_controller.rb +1 -5
  45. data/spec/dummy/application.rb +7 -1
  46. data/spec/generators/clearance/install/install_generator_spec.rb +31 -6
  47. data/spec/generators/clearance/views/views_generator_spec.rb +0 -2
  48. data/spec/models/user_spec.rb +34 -5
  49. data/spec/password_strategies/argon2_spec.rb +79 -0
  50. data/spec/password_strategies/bcrypt_spec.rb +18 -1
  51. data/spec/requests/authentication_cookie_spec.rb +55 -0
  52. data/spec/requests/token_expiration_spec.rb +5 -0
  53. data/spec/spec_helper.rb +4 -7
  54. data/spec/support/generator_spec_helpers.rb +1 -9
  55. metadata +52 -26
  56. data/app/views/layouts/application.html.erb +0 -23
  57. data/spec/app_templates/app/models/rails5/user.rb +0 -5
  58. data/spec/support/environment.rb +0 -12
  59. data/spec/support/http_method_shim.rb +0 -25
@@ -1,6 +1,8 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Clearance::Configuration do
4
+ let(:config) { Clearance.configuration }
5
+
4
6
  context "when no user_model_name is specified" do
5
7
  it "defaults to User" do
6
8
  expect(Clearance.configuration.user_model).to eq ::User
@@ -8,12 +10,47 @@ describe Clearance::Configuration do
8
10
  end
9
11
 
10
12
  context "when a custom user_model_name is specified" do
11
- it "is used instead of User" do
13
+ before(:each) do
12
14
  MyUser = Class.new
15
+ end
16
+
17
+ after(:each) do
18
+ Object.send(:remove_const, :MyUser)
19
+ end
20
+
21
+ it "is used instead of User" do
13
22
  Clearance.configure { |config| config.user_model = MyUser }
14
23
 
15
24
  expect(Clearance.configuration.user_model).to eq ::MyUser
16
25
  end
26
+
27
+ it "can be specified as a string to avoid triggering autoloading" do
28
+ Clearance.configure { |config| config.user_model = "MyUser" }
29
+
30
+ expect(Clearance.configuration.user_model).to eq ::MyUser
31
+ end
32
+ end
33
+
34
+ context "when no parent_controller is specified" do
35
+ it "defaults to ApplicationController" do
36
+ expect(config.parent_controller).to eq ::ApplicationController
37
+ end
38
+ end
39
+
40
+ context "when a custom parent_controller is specified" do
41
+ before(:each) do
42
+ MyController = Class.new
43
+ end
44
+
45
+ after(:each) do
46
+ Object.send(:remove_const, :MyController)
47
+ end
48
+
49
+ it "is used instead of ApplicationController" do
50
+ Clearance.configure { |config| config.parent_controller = MyController }
51
+
52
+ expect(config.parent_controller).to eq ::MyController
53
+ end
17
54
  end
18
55
 
19
56
  context "when secure_cookie is set to true" do
@@ -146,28 +183,22 @@ describe Clearance::Configuration do
146
183
  end
147
184
 
148
185
  describe "#rotate_csrf_on_sign_in?" do
149
- it "defaults to falsey and warns" do
150
- Clearance.configuration = Clearance::Configuration.new
151
- allow(Clearance.configuration).to receive(:warn)
152
-
153
- expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be_falsey
154
- expect(Clearance.configuration).to have_received(:warn)
155
- end
156
-
157
- it "is true and does not warn when `rotate_csrf_on_sign_in` is true" do
186
+ it "is true when `rotate_csrf_on_sign_in` is set to true" do
158
187
  Clearance.configure { |config| config.rotate_csrf_on_sign_in = true }
159
- allow(Clearance.configuration).to receive(:warn)
160
188
 
161
189
  expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be true
162
- expect(Clearance.configuration).not_to have_received(:warn)
163
190
  end
164
191
 
165
- it "is false and does not warn when `rotate_csrf_on_sign_in` is false" do
192
+ it "is false when `rotate_csrf_on_sign_in` is set to false" do
166
193
  Clearance.configure { |config| config.rotate_csrf_on_sign_in = false }
167
- allow(Clearance.configuration).to receive(:warn)
168
194
 
169
195
  expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false
170
- expect(Clearance.configuration).not_to have_received(:warn)
196
+ end
197
+
198
+ it "is false when `rotate_csrf_on_sign_in` is set to nil" do
199
+ Clearance.configure { |config| config.rotate_csrf_on_sign_in = nil }
200
+
201
+ expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false
171
202
  end
172
203
  end
173
204
  end
@@ -37,6 +37,30 @@ describe Clearance::PasswordsController do
37
37
  end
38
38
  end
39
39
 
40
+ context "email param is missing" do
41
+ it "displays flash error on new page" do
42
+ post :create, params: {
43
+ password: {},
44
+ }
45
+
46
+ expect(flash.now[:alert]).to match(/email can't be blank/i)
47
+ expect(response).to render_template(:new)
48
+ end
49
+ end
50
+
51
+ context "email param is blank" do
52
+ it "displays flash error on new page" do
53
+ post :create, params: {
54
+ password: {
55
+ email: "",
56
+ }
57
+ }
58
+
59
+ expect(flash.now[:alert]).to match(/email can't be blank/i)
60
+ expect(response).to render_template(:new)
61
+ end
62
+ end
63
+
40
64
  context "email does not belong to an existing user" do
41
65
  it "does not deliver an email" do
42
66
  ActionMailer::Base.deliveries.clear
@@ -166,6 +190,18 @@ describe Clearance::PasswordsController do
166
190
  expect(user.confirmation_token).to be_present
167
191
  end
168
192
 
193
+ it "does not raise NoMethodError from incomplete password_reset params" do
194
+ user = create(:user, :with_forgotten_password)
195
+
196
+ expect do
197
+ put :update, params: {
198
+ user_id: user,
199
+ token: user.confirmation_token,
200
+ password_reset: {},
201
+ }
202
+ end.not_to raise_error
203
+ end
204
+
169
205
  it "re-renders the password edit form" do
170
206
  user = create(:user, :with_forgotten_password)
171
207
 
@@ -65,7 +65,7 @@ describe PermissionsController do
65
65
  context 'when remember_token is blank' do
66
66
  it 'denies acess to show' do
67
67
  user = create(:user)
68
- user.update_attributes(remember_token: '')
68
+ user.update(remember_token: '')
69
69
  cookies[:remember_token] = ''
70
70
 
71
71
  get :show
@@ -2,10 +2,6 @@ class ApplicationController < ActionController::Base
2
2
  include Clearance::Controller
3
3
 
4
4
  def show
5
- if Rails::VERSION::MAJOR >= 5
6
- render html: "", layout: "application"
7
- else
8
- render text: "", layout: "application"
9
- end
5
+ render inline: "Hello user #<%= current_user.id %>", layout: false
10
6
  end
11
7
  end
@@ -28,7 +28,13 @@ module Dummy
28
28
  config.secret_key_base = "SECRET_KEY_BASE"
29
29
 
30
30
  if config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer)
31
- config.active_record.sqlite3.represent_boolean_as_integer = true
31
+ if Rails::VERSION::MAJOR < 6
32
+ config.active_record.sqlite3.represent_boolean_as_integer = true
33
+ end
34
+ end
35
+
36
+ if Rails::VERSION::MAJOR >= 6
37
+ config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
32
38
  end
33
39
 
34
40
  config.active_job.queue_adapter = :inline
@@ -70,7 +70,30 @@ describe Clearance::Generators::InstallGenerator, :generator do
70
70
 
71
71
  expect(migration).to exist
72
72
  expect(migration).to have_correct_syntax
73
- expect(migration).to contain("create_table :users")
73
+ expect(migration).to contain("create_table :users do")
74
+ end
75
+
76
+ context "active record configured for uuid" do
77
+ around do |example|
78
+ preserve_original_primary_key_type_setting do
79
+ Rails.application.config.generators do |g|
80
+ g.orm :active_record, primary_key_type: :uuid
81
+ end
82
+ example.run
83
+ end
84
+ end
85
+
86
+ it "creates a migration to create the users table with key type set" do
87
+ provide_existing_application_controller
88
+ table_does_not_exist(:users)
89
+
90
+ run_generator
91
+ migration = migration_file("db/migrate/create_users.rb")
92
+
93
+ expect(migration).to exist
94
+ expect(migration).to have_correct_syntax
95
+ expect(migration).to contain("create_table :users, id: :uuid do")
96
+ end
74
97
  end
75
98
  end
76
99
 
@@ -123,15 +146,17 @@ describe Clearance::Generators::InstallGenerator, :generator do
123
146
  and_return(false)
124
147
  end
125
148
 
149
+ def preserve_original_primary_key_type_setting
150
+ original = Rails.configuration.generators.active_record[:primary_key_type]
151
+ yield
152
+ Rails.configuration.generators.active_record[:primary_key_type] = original
153
+ end
154
+
126
155
  def contain_models_inherit_from
127
156
  contain "< #{models_inherit_from}\n"
128
157
  end
129
158
 
130
159
  def models_inherit_from
131
- if Rails.version >= "5.0.0"
132
- "ApplicationRecord"
133
- else
134
- "ActiveRecord::Base"
135
- end
160
+ "ApplicationRecord"
136
161
  end
137
162
  end
@@ -8,7 +8,6 @@ describe Clearance::Generators::ViewsGenerator, :generator do
8
8
  views = %w(
9
9
  clearance_mailer/change_password.html.erb
10
10
  clearance_mailer/change_password.text.erb
11
- layouts/application.html.erb
12
11
  passwords/create.html.erb
13
12
  passwords/edit.html.erb
14
13
  passwords/new.html.erb
@@ -22,7 +21,6 @@ describe Clearance::Generators::ViewsGenerator, :generator do
22
21
 
23
22
  view_files.each do |each|
24
23
  expect(each).to exist
25
- expect(each).to have_correct_syntax
26
24
  end
27
25
  end
28
26
 
@@ -5,15 +5,15 @@ describe User do
5
5
  it { is_expected.to have_db_index(:remember_token) }
6
6
  it { is_expected.to validate_presence_of(:email) }
7
7
  it { is_expected.to validate_presence_of(:password) }
8
+ it { is_expected.to allow_value("foo;@example.com").for(:email) }
9
+ it { is_expected.to allow_value("foo@.example.com").for(:email) }
10
+ it { is_expected.to allow_value("foo@example..com").for(:email) }
8
11
  it { is_expected.to allow_value("foo@example.co.uk").for(:email) }
9
12
  it { is_expected.to allow_value("foo@example.com").for(:email) }
10
13
  it { is_expected.to allow_value("foo+bar@example.com").for(:email) }
11
- it { is_expected.not_to allow_value("foo@").for(:email) }
12
- it { is_expected.not_to allow_value("foo@example..com").for(:email) }
13
- it { is_expected.not_to allow_value("foo@.example.com").for(:email) }
14
- it { is_expected.not_to allow_value("foo").for(:email) }
15
14
  it { is_expected.not_to allow_value("example.com").for(:email) }
16
- it { is_expected.not_to allow_value("foo;@example.com").for(:email) }
15
+ it { is_expected.not_to allow_value("foo").for(:email) }
16
+ it { is_expected.not_to allow_value("foo@").for(:email) }
17
17
 
18
18
  describe "#email" do
19
19
  it "stores email in down case and removes whitespace" do
@@ -47,6 +47,35 @@ describe User do
47
47
  expect(User.authenticate(user.email, "bad_password")).to be_nil
48
48
  end
49
49
 
50
+ it "takes the same amount of time to authenticate regardless of whether user exists" do
51
+ user = create(:user)
52
+ password = user.password
53
+
54
+ user_exists_time = Benchmark.realtime do
55
+ User.authenticate(user.email, password)
56
+ end
57
+
58
+ user_does_not_exist_time = Benchmark.realtime do
59
+ User.authenticate("bad_email@example.com", password)
60
+ end
61
+
62
+ expect(user_does_not_exist_time). to be_within(0.001).of(user_exists_time)
63
+ end
64
+
65
+ it "takes the same amount of time to fail authentication regardless of whether user exists" do
66
+ user = create(:user)
67
+
68
+ user_exists_time = Benchmark.realtime do
69
+ User.authenticate(user.email, "bad_password")
70
+ end
71
+
72
+ user_does_not_exist_time = Benchmark.realtime do
73
+ User.authenticate("bad_email@example.com", "bad_password")
74
+ end
75
+
76
+ expect(user_does_not_exist_time). to be_within(0.001).of(user_exists_time)
77
+ end
78
+
50
79
  it "is retrieved via a case-insensitive search" do
51
80
  user = create(:user)
52
81
 
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ describe Clearance::PasswordStrategies::Argon2 do
4
+ include FakeModelWithPasswordStrategy
5
+
6
+ describe "#password=" do
7
+ it "encrypts the password into encrypted_password" do
8
+ stub_argon2_password
9
+ model_instance = fake_model_with_argon2_strategy
10
+
11
+ model_instance.password = password
12
+
13
+ expect(model_instance.encrypted_password).to eq encrypted_password
14
+ end
15
+
16
+ it "encrypts with Argon2 using default cost in non test environments" do
17
+ hasher = stub_argon2_password
18
+ model_instance = fake_model_with_argon2_strategy
19
+ allow(Rails).to receive(:env).
20
+ and_return(ActiveSupport::StringInquirer.new("production"))
21
+
22
+ model_instance.password = password
23
+
24
+ expect(hasher).to have_received(:create).with(password)
25
+ end
26
+
27
+ it "encrypts with Argon2 using minimum cost in test environment" do
28
+ hasher = stub_argon2_password
29
+ model_instance = fake_model_with_argon2_strategy
30
+
31
+ model_instance.password = password
32
+
33
+ expect(hasher).to have_received(:create).with(password)
34
+ end
35
+
36
+ def stub_argon2_password
37
+ hasher = double(Argon2::Password)
38
+ allow(hasher).to receive(:create).and_return(encrypted_password)
39
+ allow(Argon2::Password).to receive(:new).and_return(hasher)
40
+ hasher
41
+ end
42
+
43
+ def encrypted_password
44
+ @encrypted_password ||= double("encrypted password")
45
+ end
46
+ end
47
+
48
+ describe "#authenticated?" do
49
+ context "given a password" do
50
+ it "is authenticated with Argon2" do
51
+ model_instance = fake_model_with_argon2_strategy
52
+
53
+ model_instance.password = password
54
+
55
+ expect(model_instance).to be_authenticated(password)
56
+ end
57
+ end
58
+
59
+ context "given no password" do
60
+ it "is not authenticated" do
61
+ model_instance = fake_model_with_argon2_strategy
62
+
63
+ password = nil
64
+
65
+ expect(model_instance).not_to be_authenticated(password)
66
+ end
67
+ end
68
+ end
69
+
70
+ def fake_model_with_argon2_strategy
71
+ @fake_model_with_argon2_strategy ||= fake_model_with_password_strategy(
72
+ Clearance::PasswordStrategies::Argon2,
73
+ )
74
+ end
75
+
76
+ def password
77
+ "password"
78
+ end
79
+ end
@@ -22,10 +22,23 @@ describe Clearance::PasswordStrategies::BCrypt do
22
22
 
23
23
  expect(BCrypt::Password).to have_received(:create).with(
24
24
  password,
25
- cost: ::BCrypt::Engine::DEFAULT_COST
25
+ cost: ::BCrypt::Engine::DEFAULT_COST,
26
26
  )
27
27
  end
28
28
 
29
+ it "uses an explicity configured BCrypt cost" do
30
+ stub_bcrypt_cost(8)
31
+ bcrypt_password = BCrypt::Password.create(password, cost: nil)
32
+
33
+ expect(bcrypt_password.cost).to eq(8)
34
+ end
35
+
36
+ it "uses the default BCrypt cost value implicitly" do
37
+ bcrypt_password = BCrypt::Password.create(password, cost: nil)
38
+
39
+ expect(bcrypt_password.cost).to eq(BCrypt::Engine::DEFAULT_COST)
40
+ end
41
+
29
42
  it "encrypts with BCrypt using minimum cost in test environment" do
30
43
  stub_bcrypt_password
31
44
  model_instance = fake_model_with_bcrypt_strategy
@@ -42,6 +55,10 @@ describe Clearance::PasswordStrategies::BCrypt do
42
55
  allow(BCrypt::Password).to receive(:create).and_return(encrypted_password)
43
56
  end
44
57
 
58
+ def stub_bcrypt_cost(cost)
59
+ allow(BCrypt::Engine).to receive(:cost).and_return(cost)
60
+ end
61
+
45
62
  def encrypted_password
46
63
  @encrypted_password ||= double("encrypted password")
47
64
  end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ class PagesController < ApplicationController
4
+ include Clearance::Controller
5
+ before_action :require_login, only: :private
6
+
7
+ # A page requiring user authentication
8
+ def private
9
+ head :ok
10
+ end
11
+
12
+ # A page that does not require user authentication
13
+ def public
14
+ head :ok
15
+ end
16
+ end
17
+
18
+ describe "Authentication cookies in the response" do
19
+ before do
20
+ draw_test_routes
21
+ create_user_and_sign_in
22
+ end
23
+
24
+ after do
25
+ Rails.application.reload_routes!
26
+ end
27
+
28
+ it "are not present if the request does not authenticate" do
29
+ get public_path
30
+
31
+ expect(headers["Set-Cookie"]).to be_nil
32
+ end
33
+
34
+ it "are present if the request does authenticate" do
35
+ get private_path
36
+
37
+ expect(headers["Set-Cookie"]).to match(/remember_token=/)
38
+ end
39
+
40
+ def draw_test_routes
41
+ Rails.application.routes.draw do
42
+ get "/private" => "pages#private", as: :private
43
+ get "/public" => "pages#public", as: :public
44
+ resource :session, controller: "clearance/sessions", only: [:create]
45
+ end
46
+ end
47
+
48
+ def create_user_and_sign_in
49
+ user = create(:user, password: "password")
50
+
51
+ post session_path, params: {
52
+ session: { email: user.email, password: "password" },
53
+ }
54
+ end
55
+ end