clearance 2.0.0.beta2 → 2.3.0
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/.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
@@ -13,10 +13,7 @@ module Clearance
|
|
13
13
|
# `password=(new_password)`. For an example of how to implement these methods,
|
14
14
|
# see {Clearance::PasswordStrategies::BCrypt}.
|
15
15
|
module PasswordStrategies
|
16
|
-
autoload :BCrypt,
|
17
|
-
autoload :
|
18
|
-
'clearance/password_strategies/bcrypt_migration_from_sha1'
|
19
|
-
autoload :Blowfish, 'clearance/password_strategies/blowfish'
|
20
|
-
autoload :SHA1, 'clearance/password_strategies/sha1'
|
16
|
+
autoload :BCrypt, "clearance/password_strategies/bcrypt"
|
17
|
+
autoload :Argon2, "clearance/password_strategies/argon2"
|
21
18
|
end
|
22
19
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Clearance
|
2
|
+
module PasswordStrategies
|
3
|
+
# Uses Argon2 to authenticate users and store encrypted passwords.
|
4
|
+
|
5
|
+
module Argon2
|
6
|
+
require "argon2"
|
7
|
+
|
8
|
+
def authenticated?(password)
|
9
|
+
if encrypted_password.present?
|
10
|
+
::Argon2::Password.verify_password(password, encrypted_password)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def password=(new_password)
|
15
|
+
@password = new_password
|
16
|
+
|
17
|
+
if new_password.present?
|
18
|
+
self.encrypted_password = ::Argon2::Password.new.create(new_password)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -2,10 +2,14 @@ module Clearance
|
|
2
2
|
module PasswordStrategies
|
3
3
|
# Uses BCrypt to authenticate users and store encrypted passwords.
|
4
4
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
5
|
+
# BCrypt has a `cost` argument which determines how computationally
|
6
|
+
# expensive the hash is to calculate. The higher the cost, the harder it is
|
7
|
+
# for attackers to crack passwords even if they posess a database dump of
|
8
|
+
# the encrypted passwords. Clearance uses the `bcrypt-ruby` default cost
|
9
|
+
# except in the test environment, where it uses the minimum cost value for
|
10
|
+
# speed. If you wish to increase the cost over the default, you can do so
|
11
|
+
# by setting a higher cost in an initializer:
|
12
|
+
# `BCrypt::Engine.cost = 12`
|
9
13
|
module BCrypt
|
10
14
|
require 'bcrypt'
|
11
15
|
|
@@ -19,18 +23,20 @@ module Clearance
|
|
19
23
|
@password = new_password
|
20
24
|
|
21
25
|
if new_password.present?
|
22
|
-
cost = if defined?(::Rails) && ::Rails.env.test?
|
23
|
-
::BCrypt::Engine::MIN_COST
|
24
|
-
else
|
25
|
-
::BCrypt::Engine::DEFAULT_COST
|
26
|
-
end
|
27
|
-
|
28
26
|
self.encrypted_password = ::BCrypt::Password.create(
|
29
27
|
new_password,
|
30
|
-
cost:
|
28
|
+
cost: configured_bcrypt_cost,
|
31
29
|
)
|
32
30
|
end
|
33
31
|
end
|
32
|
+
|
33
|
+
def configured_bcrypt_cost
|
34
|
+
if defined?(::Rails) && ::Rails.env.test?
|
35
|
+
::BCrypt::Engine::MIN_COST
|
36
|
+
else
|
37
|
+
::BCrypt::Engine.cost
|
38
|
+
end
|
39
|
+
end
|
34
40
|
end
|
35
41
|
end
|
36
42
|
end
|
@@ -21,7 +21,11 @@ module Clearance
|
|
21
21
|
session = Clearance::Session.new(env)
|
22
22
|
env[:clearance] = session
|
23
23
|
response = @app.call(env)
|
24
|
-
|
24
|
+
|
25
|
+
if session.authentication_successful?
|
26
|
+
session.add_cookie_to_headers response[1]
|
27
|
+
end
|
28
|
+
|
25
29
|
response
|
26
30
|
end
|
27
31
|
end
|
data/lib/clearance/session.rb
CHANGED
@@ -81,7 +81,7 @@ module Clearance
|
|
81
81
|
end
|
82
82
|
|
83
83
|
@current_user = nil
|
84
|
-
cookies.delete remember_token_cookie
|
84
|
+
cookies.delete remember_token_cookie, delete_cookie_options
|
85
85
|
end
|
86
86
|
|
87
87
|
# True if {#current_user} is set.
|
@@ -98,6 +98,13 @@ module Clearance
|
|
98
98
|
! signed_in?
|
99
99
|
end
|
100
100
|
|
101
|
+
# True if a successful authentication has been performed
|
102
|
+
#
|
103
|
+
# @return [Boolean]
|
104
|
+
def authentication_successful?
|
105
|
+
!!@current_user
|
106
|
+
end
|
107
|
+
|
101
108
|
private
|
102
109
|
|
103
110
|
# @api private
|
@@ -147,7 +154,7 @@ module Clearance
|
|
147
154
|
guards = Clearance.configuration.sign_in_guards
|
148
155
|
|
149
156
|
guards.inject(default_guard) do |stack, guard_class|
|
150
|
-
guard_class.new(self, stack)
|
157
|
+
guard_class.to_s.constantize.new(self, stack)
|
151
158
|
end
|
152
159
|
end
|
153
160
|
|
@@ -157,12 +164,22 @@ module Clearance
|
|
157
164
|
domain: domain,
|
158
165
|
expires: remember_token_expires,
|
159
166
|
httponly: Clearance.configuration.httponly,
|
167
|
+
same_site: Clearance.configuration.same_site,
|
160
168
|
path: Clearance.configuration.cookie_path,
|
161
169
|
secure: Clearance.configuration.secure_cookie,
|
162
170
|
value: remember_token,
|
163
171
|
}
|
164
172
|
end
|
165
173
|
|
174
|
+
# @api private
|
175
|
+
def delete_cookie_options
|
176
|
+
Hash.new.tap do |options|
|
177
|
+
if configured_cookie_domain
|
178
|
+
options[:domain] = domain
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
166
183
|
# @api private
|
167
184
|
def domain
|
168
185
|
if configured_cookie_domain.respond_to?(:call)
|
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
@@ -122,6 +122,16 @@ module Clearance
|
|
122
122
|
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
123
123
|
end
|
124
124
|
|
125
|
+
def migration_primary_key_type_string
|
126
|
+
if configured_key_type
|
127
|
+
", id: :#{configured_key_type}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def configured_key_type
|
132
|
+
Rails.configuration.generators.active_record[:primary_key_type]
|
133
|
+
end
|
134
|
+
|
125
135
|
def models_inherit_from
|
126
136
|
"ApplicationRecord"
|
127
137
|
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>
|
@@ -46,6 +46,18 @@ describe Clearance::BackDoor do
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
it "strips 'as' from the params" do
|
50
|
+
user_id = "123"
|
51
|
+
user = double("user")
|
52
|
+
allow(User).to receive(:find).with(user_id).and_return(user)
|
53
|
+
env = build_env(as: user_id, foo: :bar)
|
54
|
+
back_door = Clearance::BackDoor.new(mock_app)
|
55
|
+
|
56
|
+
back_door.call(env)
|
57
|
+
|
58
|
+
expect(env["QUERY_STRING"]).to eq("foo=bar")
|
59
|
+
end
|
60
|
+
|
49
61
|
context "when the environments are disabled" do
|
50
62
|
before do
|
51
63
|
Clearance.configuration.allowed_backdoor_environments = nil
|
@@ -84,14 +96,18 @@ describe Clearance::BackDoor do
|
|
84
96
|
env_for_user_id("")
|
85
97
|
end
|
86
98
|
|
87
|
-
def
|
99
|
+
def build_env(params)
|
100
|
+
query = Rack::Utils.build_query(params)
|
88
101
|
clearance = double("clearance", sign_in: true)
|
89
|
-
Rack::MockRequest.env_for("
|
102
|
+
Rack::MockRequest.env_for("/?#{query}").merge(clearance: clearance)
|
103
|
+
end
|
104
|
+
|
105
|
+
def env_for_user_id(user_id)
|
106
|
+
build_env(as: user_id)
|
90
107
|
end
|
91
108
|
|
92
109
|
def env_for_username(username)
|
93
|
-
|
94
|
-
Rack::MockRequest.env_for("/?as=#{username}").merge(clearance: clearance)
|
110
|
+
build_env(as: username)
|
95
111
|
end
|
96
112
|
|
97
113
|
def mock_app
|
@@ -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
|
@@ -309,14 +340,63 @@ describe Clearance::Session do
|
|
309
340
|
expect(headers["Set-Cookie"]).to be nil
|
310
341
|
end
|
311
342
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
381
|
+
|
382
|
+
context 'with callable cookie domain' do
|
383
|
+
it 'clears cookie' do
|
384
|
+
domain = '.example.com'
|
385
|
+
Clearance.configuration.cookie_domain = ->(_) { domain }
|
386
|
+
user = create(:user)
|
387
|
+
env = env_with_remember_token(
|
388
|
+
value: user.remember_token,
|
389
|
+
domain: domain
|
390
|
+
)
|
391
|
+
session = Clearance::Session.new(env)
|
392
|
+
cookie_jar = ActionDispatch::Request.new(env).cookie_jar
|
393
|
+
expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
|
394
|
+
|
395
|
+
session.sign_out
|
396
|
+
|
397
|
+
expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
|
398
|
+
end
|
399
|
+
end
|
320
400
|
end
|
321
401
|
|
322
402
|
def env_with_cookies(cookies)
|