clearance 2.0.0.beta2 → 2.3.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.
Potentially problematic release.
This version of clearance might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.erb-lint.yml +5 -0
- data/.travis.yml +5 -9
- data/Appraisals +14 -19
- data/Gemfile +11 -7
- data/Gemfile.lock +137 -84
- data/NEWS.md +90 -11
- data/README.md +11 -24
- data/RELEASING.md +25 -0
- data/Rakefile +6 -1
- data/app/controllers/clearance/base_controller.rb +8 -1
- data/app/controllers/clearance/passwords_controller.rb +24 -7
- data/app/views/clearance_mailer/change_password.html.erb +2 -2
- data/app/views/clearance_mailer/change_password.text.erb +2 -2
- data/clearance.gemspec +10 -3
- data/config/locales/clearance.en.yml +1 -0
- data/config/routes.rb +1 -1
- data/gemfiles/rails_5.0.gemfile +10 -9
- data/gemfiles/rails_5.1.gemfile +11 -10
- data/gemfiles/rails_5.2.gemfile +11 -10
- data/gemfiles/rails_6.0.gemfile +11 -10
- data/lib/clearance/authentication.rb +1 -1
- data/lib/clearance/back_door.rb +2 -1
- data/lib/clearance/configuration.rb +29 -18
- data/lib/clearance/password_strategies.rb +2 -5
- data/lib/clearance/password_strategies/argon2.rb +23 -0
- data/lib/clearance/password_strategies/bcrypt.rb +17 -11
- data/lib/clearance/rack_session.rb +5 -1
- data/lib/clearance/session.rb +19 -2
- data/lib/clearance/testing/deny_access_matcher.rb +1 -5
- data/lib/clearance/user.rb +12 -3
- data/lib/clearance/version.rb +1 -1
- data/lib/generators/clearance/install/install_generator.rb +10 -0
- data/lib/generators/clearance/install/templates/README +10 -4
- data/lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb +1 -1
- data/lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb +1 -1
- data/lib/generators/clearance/routes/templates/routes.rb +1 -1
- data/spec/acceptance/clearance_installation_spec.rb +0 -4
- data/spec/app_templates/app/models/user.rb +1 -1
- data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
- data/spec/clearance/back_door_spec.rb +20 -4
- data/spec/clearance/rack_session_spec.rb +2 -0
- data/spec/clearance/session_spec.rb +88 -8
- data/spec/clearance/testing/deny_access_matcher_spec.rb +32 -0
- data/spec/configuration_spec.rb +32 -14
- data/spec/controllers/passwords_controller_spec.rb +36 -0
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/generators/clearance/install/install_generator_spec.rb +30 -1
- data/spec/generators/clearance/views/views_generator_spec.rb +0 -1
- data/spec/models/user_spec.rb +34 -5
- data/spec/password_strategies/argon2_spec.rb +79 -0
- data/spec/password_strategies/bcrypt_spec.rb +18 -1
- data/spec/requests/authentication_cookie_spec.rb +55 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/generator_spec_helpers.rb +1 -5
- metadata +45 -15
- data/app/views/layouts/application.html.erb +0 -23
- data/spec/app_templates/app/models/rails5/user.rb +0 -5
@@ -0,0 +1,32 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class PretendFriendsController < ActionController::Base
|
4
|
+
include Clearance::Controller
|
5
|
+
before_action :require_login
|
6
|
+
|
7
|
+
def index
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe PretendFriendsController, type: :controller do
|
12
|
+
before do
|
13
|
+
Rails.application.routes.draw do
|
14
|
+
resources :pretend_friends, only: :index
|
15
|
+
get "/sign_in" => "clearance/sessions#new", as: "sign_in"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
Rails.application.reload_routes!
|
21
|
+
end
|
22
|
+
|
23
|
+
it "checks contents of deny access flash" do
|
24
|
+
get :index
|
25
|
+
|
26
|
+
expect(subject).to deny_access(flash: failure_message)
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_message
|
30
|
+
I18n.t("flashes.failure_when_not_signed_in")
|
31
|
+
end
|
32
|
+
end
|
data/spec/configuration_spec.rb
CHANGED
@@ -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 }
|
@@ -159,28 +183,22 @@ describe Clearance::Configuration do
|
|
159
183
|
end
|
160
184
|
|
161
185
|
describe "#rotate_csrf_on_sign_in?" do
|
162
|
-
it "
|
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
|
186
|
+
it "is true when `rotate_csrf_on_sign_in` is set to true" do
|
171
187
|
Clearance.configure { |config| config.rotate_csrf_on_sign_in = true }
|
172
|
-
allow(Clearance.configuration).to receive(:warn)
|
173
188
|
|
174
189
|
expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be true
|
175
|
-
expect(Clearance.configuration).not_to have_received(:warn)
|
176
190
|
end
|
177
191
|
|
178
|
-
it "is false
|
192
|
+
it "is false when `rotate_csrf_on_sign_in` is set to false" do
|
179
193
|
Clearance.configure { |config| config.rotate_csrf_on_sign_in = false }
|
180
|
-
allow(Clearance.configuration).to receive(:warn)
|
181
194
|
|
182
195
|
expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false
|
183
|
-
|
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
|
184
202
|
end
|
185
203
|
end
|
186
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
|
|
@@ -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,12 @@ 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
|
@@ -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
|
data/spec/models/user_spec.rb
CHANGED
@@ -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
|
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
|