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.
- checksums.yaml +4 -4
- data/.erb-lint.yml +5 -0
- data/.github/workflows/tests.yml +52 -0
- data/Appraisals +14 -19
- data/Gemfile +11 -7
- data/Gemfile.lock +142 -87
- data/NEWS.md +94 -0
- data/README.md +4 -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 +16 -3
- data/app/views/clearance_mailer/change_password.html.erb +2 -2
- data/app/views/clearance_mailer/change_password.text.erb +2 -2
- data/app/views/passwords/edit.html.erb +1 -1
- data/clearance.gemspec +9 -2
- 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/gemfiles/rails_6.1.gemfile +21 -0
- data/lib/clearance/authentication.rb +1 -1
- data/lib/clearance/back_door.rb +2 -1
- data/lib/clearance/configuration.rb +37 -18
- data/lib/clearance/password_strategies.rb +2 -5
- data/lib/clearance/password_strategies/argon2.rb +23 -0
- data/lib/clearance/rack_session.rb +5 -1
- data/lib/clearance/session.rb +40 -12
- data/lib/clearance/user.rb +12 -3
- data/lib/clearance/version.rb +1 -1
- data/lib/generators/clearance/install/install_generator.rb +13 -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 +3 -2
- data/spec/clearance/session_spec.rb +154 -51
- data/spec/configuration_spec.rb +60 -14
- data/spec/controllers/passwords_controller_spec.rb +19 -5
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/generators/clearance/install/install_generator_spec.rb +36 -1
- data/spec/generators/clearance/views/views_generator_spec.rb +0 -1
- data/spec/mailers/clearance_mailer_spec.rb +33 -0
- data/spec/models/user_spec.rb +34 -5
- data/spec/password_strategies/argon2_spec.rb +79 -0
- data/spec/requests/authentication_cookie_spec.rb +55 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/clearance.rb +11 -0
- data/spec/support/generator_spec_helpers.rb +1 -5
- data/spec/support/request_with_remember_token.rb +8 -6
- metadata +42 -12
- data/.travis.yml +0 -32
- data/app/views/layouts/application.html.erb +0 -23
- data/spec/app_templates/app/models/rails5/user.rb +0 -5
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 }
|
@@ -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 "
|
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
|
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
|
-
|
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 "
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
|
@@ -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
|
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.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
|