clearance 2.0.0.beta1 → 2.2.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/.travis.yml +8 -14
- data/Appraisals +11 -3
- data/Gemfile +1 -3
- data/Gemfile.lock +91 -86
- data/NEWS.md +86 -4
- data/README.md +54 -28
- data/app/controllers/clearance/base_controller.rb +8 -1
- data/app/controllers/clearance/passwords_controller.rb +23 -5
- data/clearance.gemspec +15 -9
- data/config/locales/clearance.en.yml +1 -0
- data/config/routes.rb +1 -1
- data/gemfiles/rails_5.0.gemfile +3 -3
- data/gemfiles/rails_5.1.gemfile +3 -3
- data/gemfiles/rails_5.2.gemfile +3 -3
- data/gemfiles/{rails_4.2.gemfile → rails_6.0.gemfile} +5 -4
- data/lib/clearance/authentication.rb +1 -1
- data/lib/clearance/back_door.rb +1 -1
- data/lib/clearance/configuration.rb +30 -19
- data/lib/clearance/password_strategies.rb +5 -4
- 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 +39 -3
- 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 +11 -7
- 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/controllers/home_controller.rb +1 -5
- data/spec/app_templates/testapp/app/views/layouts/application.html.erb +24 -0
- data/spec/clearance/back_door_spec.rb +12 -6
- data/spec/clearance/rack_session_spec.rb +2 -0
- data/spec/clearance/session_spec.rb +91 -16
- data/spec/clearance/testing/deny_access_matcher_spec.rb +32 -0
- data/spec/configuration_spec.rb +46 -15
- data/spec/controllers/passwords_controller_spec.rb +36 -0
- data/spec/controllers/permissions_controller_spec.rb +1 -1
- data/spec/dummy/app/controllers/application_controller.rb +1 -5
- data/spec/dummy/application.rb +7 -1
- data/spec/generators/clearance/install/install_generator_spec.rb +31 -6
- data/spec/generators/clearance/views/views_generator_spec.rb +0 -2
- 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/requests/token_expiration_spec.rb +5 -0
- data/spec/spec_helper.rb +4 -7
- data/spec/support/generator_spec_helpers.rb +1 -9
- metadata +52 -26
- data/app/views/layouts/application.html.erb +0 -23
- data/spec/app_templates/app/models/rails5/user.rb +0 -5
- data/spec/support/environment.rb +0 -12
- data/spec/support/http_method_shim.rb +0 -25
data/lib/clearance/user.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/clearance/version.rb
CHANGED
@@ -119,17 +119,21 @@ module Clearance
|
|
119
119
|
end
|
120
120
|
|
121
121
|
def migration_version
|
122
|
-
|
123
|
-
|
122
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
123
|
+
end
|
124
|
+
|
125
|
+
def migration_primary_key_type_string
|
126
|
+
if configured_key_type
|
127
|
+
", id: :#{configured_key_type}"
|
124
128
|
end
|
125
129
|
end
|
126
130
|
|
131
|
+
def configured_key_type
|
132
|
+
Rails.configuration.generators.active_record[:primary_key_type]
|
133
|
+
end
|
134
|
+
|
127
135
|
def models_inherit_from
|
128
|
-
|
129
|
-
"ApplicationRecord"
|
130
|
-
else
|
131
|
-
"ActiveRecord::Base"
|
132
|
-
end
|
136
|
+
"ApplicationRecord"
|
133
137
|
end
|
134
138
|
end
|
135
139
|
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
|
11
|
+
In the production environment it should be your application's full hostname.
|
12
12
|
|
13
|
-
2. Display user session
|
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
|
-
|
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
|
@@ -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
|
|
@@ -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>
|
@@ -1,9 +1,6 @@
|
|
1
1
|
require "spec_helper"
|
2
|
-
require "support/environment"
|
3
2
|
|
4
3
|
describe Clearance::BackDoor do
|
5
|
-
include EnvironmentSupport
|
6
|
-
|
7
4
|
it "signs in as a given user" do
|
8
5
|
user_id = "123"
|
9
6
|
user = double("user")
|
@@ -42,7 +39,7 @@ describe Clearance::BackDoor do
|
|
42
39
|
end
|
43
40
|
|
44
41
|
it "can't be used outside the allowed environments" do
|
45
|
-
with_environment("
|
42
|
+
with_environment("production") do
|
46
43
|
expect { Clearance::BackDoor.new(mock_app) }.
|
47
44
|
to raise_exception "Can't use auth backdoor outside of configured \
|
48
45
|
environments (test, ci, development).".squish
|
@@ -55,7 +52,7 @@ describe Clearance::BackDoor do
|
|
55
52
|
end
|
56
53
|
|
57
54
|
it "raises an error for a default allowed env" do
|
58
|
-
with_environment("
|
55
|
+
with_environment("test") do
|
59
56
|
expect { Clearance::BackDoor.new(mock_app) }.
|
60
57
|
to raise_exception "BackDoor auth is disabled."
|
61
58
|
end
|
@@ -68,7 +65,7 @@ describe Clearance::BackDoor do
|
|
68
65
|
end
|
69
66
|
|
70
67
|
it "can be used with configured allowed environments" do
|
71
|
-
with_environment("
|
68
|
+
with_environment("demo") do
|
72
69
|
user_id = "123"
|
73
70
|
user = double("user")
|
74
71
|
allow(User).to receive(:find).with(user_id).and_return(user)
|
@@ -100,4 +97,13 @@ describe Clearance::BackDoor do
|
|
100
97
|
def mock_app
|
101
98
|
lambda { |env| [200, {}, ["okay"]] }
|
102
99
|
end
|
100
|
+
|
101
|
+
def with_environment(environment)
|
102
|
+
original_env = Rails.env
|
103
|
+
Rails.env = environment
|
104
|
+
|
105
|
+
yield
|
106
|
+
ensure
|
107
|
+
Rails.env = original_env
|
108
|
+
end
|
103
109
|
end
|
@@ -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
|
@@ -238,17 +269,31 @@ describe Clearance::Session do
|
|
238
269
|
end
|
239
270
|
end
|
240
271
|
|
241
|
-
describe
|
242
|
-
context
|
272
|
+
describe "cookie domain option" do
|
273
|
+
context "when set" do
|
243
274
|
before do
|
244
|
-
Clearance.configuration.cookie_domain =
|
275
|
+
Clearance.configuration.cookie_domain = cookie_domain
|
245
276
|
session.sign_in(user)
|
246
277
|
end
|
247
278
|
|
248
|
-
|
249
|
-
|
279
|
+
context "with string" do
|
280
|
+
let(:cookie_domain) { ".example.com" }
|
250
281
|
|
251
|
-
|
282
|
+
it "sets a standard cookie" do
|
283
|
+
session.add_cookie_to_headers(headers)
|
284
|
+
|
285
|
+
expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context "with lambda" do
|
290
|
+
let(:cookie_domain) { lambda { |_r| ".example.com" } }
|
291
|
+
|
292
|
+
it "sets a standard cookie" do
|
293
|
+
session.add_cookie_to_headers(headers)
|
294
|
+
|
295
|
+
expect(headers['Set-Cookie']).to match(/domain=\.example\.com; path/)
|
296
|
+
end
|
252
297
|
end
|
253
298
|
end
|
254
299
|
|
@@ -258,7 +303,7 @@ describe Clearance::Session do
|
|
258
303
|
it 'sets a standard cookie' do
|
259
304
|
session.add_cookie_to_headers(headers)
|
260
305
|
|
261
|
-
expect(headers[
|
306
|
+
expect(headers["Set-Cookie"]).not_to match(/domain=.+; path/)
|
262
307
|
end
|
263
308
|
end
|
264
309
|
end
|
@@ -270,7 +315,7 @@ describe Clearance::Session do
|
|
270
315
|
it 'sets a standard cookie' do
|
271
316
|
session.add_cookie_to_headers(headers)
|
272
317
|
|
273
|
-
expect(headers[
|
318
|
+
expect(headers["Set-Cookie"]).to_not match(/domain=.+; path/)
|
274
319
|
end
|
275
320
|
end
|
276
321
|
|
@@ -295,14 +340,44 @@ describe Clearance::Session do
|
|
295
340
|
expect(headers["Set-Cookie"]).to be nil
|
296
341
|
end
|
297
342
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
306
381
|
end
|
307
382
|
|
308
383
|
def env_with_cookies(cookies)
|
@@ -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
|