clearance 2.0.0 → 2.3.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.erb-lint.yml +5 -0
  3. data/.github/workflows/tests.yml +52 -0
  4. data/Appraisals +14 -19
  5. data/Gemfile +11 -7
  6. data/Gemfile.lock +142 -87
  7. data/NEWS.md +94 -0
  8. data/README.md +4 -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 +16 -3
  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/app/views/passwords/edit.html.erb +1 -1
  16. data/clearance.gemspec +9 -2
  17. data/config/locales/clearance.en.yml +1 -0
  18. data/config/routes.rb +1 -1
  19. data/gemfiles/rails_5.0.gemfile +10 -9
  20. data/gemfiles/rails_5.1.gemfile +11 -10
  21. data/gemfiles/rails_5.2.gemfile +11 -10
  22. data/gemfiles/rails_6.0.gemfile +11 -10
  23. data/gemfiles/rails_6.1.gemfile +21 -0
  24. data/lib/clearance/authentication.rb +1 -1
  25. data/lib/clearance/back_door.rb +2 -1
  26. data/lib/clearance/configuration.rb +37 -18
  27. data/lib/clearance/password_strategies.rb +2 -5
  28. data/lib/clearance/password_strategies/argon2.rb +23 -0
  29. data/lib/clearance/rack_session.rb +5 -1
  30. data/lib/clearance/session.rb +40 -12
  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 +13 -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 +3 -2
  43. data/spec/clearance/session_spec.rb +154 -51
  44. data/spec/configuration_spec.rb +60 -14
  45. data/spec/controllers/passwords_controller_spec.rb +19 -5
  46. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  47. data/spec/generators/clearance/install/install_generator_spec.rb +36 -1
  48. data/spec/generators/clearance/views/views_generator_spec.rb +0 -1
  49. data/spec/mailers/clearance_mailer_spec.rb +33 -0
  50. data/spec/models/user_spec.rb +34 -5
  51. data/spec/password_strategies/argon2_spec.rb +79 -0
  52. data/spec/requests/authentication_cookie_spec.rb +55 -0
  53. data/spec/spec_helper.rb +0 -1
  54. data/spec/support/clearance.rb +11 -0
  55. data/spec/support/generator_spec_helpers.rb +1 -5
  56. data/spec/support/request_with_remember_token.rb +8 -6
  57. metadata +42 -12
  58. data/.travis.yml +0 -32
  59. data/app/views/layouts/application.html.erb +0 -23
  60. data/spec/app_templates/app/models/rails5/user.rb +0 -5
@@ -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
@@ -29,6 +31,28 @@ describe Clearance::Configuration do
29
31
  end
30
32
  end
31
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
54
+ end
55
+
32
56
  context "when secure_cookie is set to true" do
33
57
  it "returns true" do
34
58
  Clearance.configure { |config| config.secure_cookie = true }
@@ -42,6 +66,34 @@ describe Clearance::Configuration do
42
66
  end
43
67
  end
44
68
 
69
+ context "when signed_cookie is set to true" do
70
+ it "returns true" do
71
+ Clearance.configure { |config| config.signed_cookie = true }
72
+ expect(Clearance.configuration.signed_cookie).to eq true
73
+ end
74
+ end
75
+
76
+ context "when signed_cookie is not specified" do
77
+ it "defaults to false" do
78
+ expect(Clearance.configuration.signed_cookie).to eq false
79
+ end
80
+ end
81
+
82
+ context "when signed_cookie is set to :migrate" do
83
+ it "returns :migrate" do
84
+ Clearance.configure { |config| config.signed_cookie = :migrate }
85
+ expect(Clearance.configuration.signed_cookie).to eq :migrate
86
+ end
87
+ end
88
+
89
+ context "when signed_cookie is set to an unexpected value" do
90
+ it "returns :migrate" do
91
+ expect {
92
+ Clearance.configure { |config| config.signed_cookie = "unknown" }
93
+ }.to raise_exception(RuntimeError)
94
+ end
95
+ end
96
+
45
97
  context "when no redirect URL specified" do
46
98
  it 'returns "/" as redirect URL' do
47
99
  expect(Clearance::Configuration.new.redirect_url).to eq "/"
@@ -159,28 +211,22 @@ describe Clearance::Configuration do
159
211
  end
160
212
 
161
213
  describe "#rotate_csrf_on_sign_in?" do
162
- it "defaults to falsey and warns" do
163
- Clearance.configuration = Clearance::Configuration.new
164
- allow(Clearance.configuration).to receive(:warn)
165
-
166
- expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be_falsey
167
- expect(Clearance.configuration).to have_received(:warn)
168
- end
169
-
170
- it "is true and does not warn when `rotate_csrf_on_sign_in` is true" do
214
+ it "is true when `rotate_csrf_on_sign_in` is set to true" do
171
215
  Clearance.configure { |config| config.rotate_csrf_on_sign_in = true }
172
- allow(Clearance.configuration).to receive(:warn)
173
216
 
174
217
  expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be true
175
- expect(Clearance.configuration).not_to have_received(:warn)
176
218
  end
177
219
 
178
- it "is false and does not warn when `rotate_csrf_on_sign_in` is false" do
220
+ it "is false when `rotate_csrf_on_sign_in` is set to false" do
179
221
  Clearance.configure { |config| config.rotate_csrf_on_sign_in = false }
180
- allow(Clearance.configuration).to receive(:warn)
181
222
 
182
223
  expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false
183
- expect(Clearance.configuration).not_to have_received(:warn)
224
+ end
225
+
226
+ it "is false when `rotate_csrf_on_sign_in` is set to nil" do
227
+ Clearance.configure { |config| config.rotate_csrf_on_sign_in = nil }
228
+
229
+ expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false
184
230
  end
185
231
  end
186
232
  end
@@ -38,12 +38,26 @@ describe Clearance::PasswordsController do
38
38
  end
39
39
 
40
40
  context "email param is missing" do
41
- it "does not raise error" do
42
- expect do
43
- post :create, params: {
44
- password: {},
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: "",
45
56
  }
46
- end.not_to raise_error
57
+ }
58
+
59
+ expect(flash.now[:alert]).to match(/email can't be blank/i)
60
+ expect(response).to render_template(:new)
47
61
  end
48
62
  end
49
63
 
@@ -2,6 +2,6 @@ class ApplicationController < ActionController::Base
2
2
  include Clearance::Controller
3
3
 
4
4
  def show
5
- render html: "", layout: "application"
5
+ render inline: "Hello user #<%= current_user.id %>", layout: false
6
6
  end
7
7
  end
@@ -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,6 +146,18 @@ 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
+ active_record = Rails.configuration.generators.active_record
151
+ active_record ||= Rails.configuration.generators.options[:active_record]
152
+ original = active_record[:primary_key_type]
153
+
154
+ yield
155
+
156
+ Rails.application.config.generators do |g|
157
+ g.orm :active_record, primary_key_type: original
158
+ end
159
+ end
160
+
126
161
  def contain_models_inherit_from
127
162
  contain "< #{models_inherit_from}\n"
128
163
  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
@@ -55,4 +55,37 @@ describe ClearanceMailer do
55
55
  text: I18n.t("clearance_mailer.change_password.link_text")
56
56
  )
57
57
  end
58
+
59
+ context "when using a custom model" do
60
+ it "contains a link for a custom model" do
61
+ define_people_routes
62
+ Person = Class.new(User)
63
+ person = Person.new(email: "person@example.com", password: "password")
64
+
65
+ person.forgot_password!
66
+ host = ActionMailer::Base.default_url_options[:host]
67
+ link = "http://#{host}/people/#{person.id}/password/edit" \
68
+ "?token=#{person.confirmation_token}"
69
+
70
+ email = ClearanceMailer.change_password(person)
71
+
72
+ expect(email.text_part.body).to include(link)
73
+ expect(email.html_part.body).to include(link)
74
+
75
+ Object.send(:remove_const, :Person)
76
+ Rails.application.reload_routes!
77
+ end
78
+
79
+ def define_people_routes
80
+ Rails.application.routes.draw do
81
+ resources :people, controller: "clearance/users", only: :create do
82
+ resource(
83
+ :password,
84
+ controller: "clearance/passwords",
85
+ only: %i[edit update],
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
58
91
  end
@@ -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.01).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.01).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
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -46,5 +46,4 @@ end
46
46
 
47
47
  def restore_default_warning_free_config
48
48
  Clearance.configuration = nil
49
- Clearance.configure { |config| config.rotate_csrf_on_sign_in = true }
50
49
  end