clearance 2.1.0 → 2.4.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/.github/workflows/tests.yml +52 -0
- data/Appraisals +14 -19
- data/Gemfile +11 -7
- data/Gemfile.lock +140 -85
- data/NEWS.md +62 -0
- data/README.md +23 -12
- data/RELEASING.md +25 -0
- data/Rakefile +6 -1
- data/app/controllers/clearance/passwords_controller.rb +2 -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 +1 -0
- 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/back_door.rb +2 -1
- data/lib/clearance/configuration.rb +19 -0
- data/lib/clearance/password_strategies.rb +2 -5
- data/lib/clearance/password_strategies/argon2.rb +23 -0
- data/lib/clearance/rack_session.rb +1 -1
- data/lib/clearance/session.rb +24 -12
- data/lib/clearance/user.rb +11 -2
- data/lib/clearance/version.rb +1 -1
- data/lib/generators/clearance/install/install_generator.rb +4 -1
- data/spec/clearance/back_door_spec.rb +20 -4
- data/spec/clearance/rack_session_spec.rb +1 -2
- data/spec/clearance/session_spec.rb +116 -43
- data/spec/configuration_spec.rb +28 -0
- data/spec/generators/clearance/install/install_generator_spec.rb +8 -2
- data/spec/mailers/clearance_mailer_spec.rb +33 -0
- data/spec/models/user_spec.rb +29 -0
- data/spec/password_strategies/argon2_spec.rb +79 -0
- data/spec/support/clearance.rb +11 -0
- data/spec/support/request_with_remember_token.rb +8 -6
- metadata +29 -4
- data/.travis.yml +0 -27
data/lib/clearance/user.rb
CHANGED
@@ -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: true },
|
147
156
|
unless: :email_optional?
|
148
157
|
|
149
158
|
validates :password, presence: true, unless: :skip_password_validation?
|
data/lib/clearance/version.rb
CHANGED
@@ -129,7 +129,10 @@ module Clearance
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def configured_key_type
|
132
|
-
Rails.configuration.generators.active_record
|
132
|
+
active_record = Rails.configuration.generators.active_record
|
133
|
+
active_record ||= Rails.configuration.generators.options[:active_record]
|
134
|
+
|
135
|
+
active_record[:primary_key_type]
|
133
136
|
end
|
134
137
|
|
135
138
|
def models_inherit_from
|
@@ -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
|
@@ -20,7 +20,6 @@ describe Clearance::RackSession do
|
|
20
20
|
response = Rack::MockResponse.new(*app.call(env))
|
21
21
|
|
22
22
|
expect(response.body).to eq expected_session
|
23
|
-
expect(expected_session).to have_received(:add_cookie_to_headers)
|
24
|
-
with(hash_including(headers))
|
23
|
+
expect(expected_session).to have_received(:add_cookie_to_headers)
|
25
24
|
end
|
26
25
|
end
|
@@ -4,7 +4,6 @@ describe Clearance::Session do
|
|
4
4
|
before { Timecop.freeze }
|
5
5
|
after { Timecop.return }
|
6
6
|
|
7
|
-
let(:headers) { {} }
|
8
7
|
let(:session) { Clearance::Session.new(env_without_remember_token) }
|
9
8
|
let(:user) { create(:user) }
|
10
9
|
|
@@ -35,9 +34,63 @@ describe Clearance::Session do
|
|
35
34
|
Clearance.configuration.cookie_name = "custom_cookie_name"
|
36
35
|
|
37
36
|
session.sign_in user
|
38
|
-
session.add_cookie_to_headers
|
37
|
+
session.add_cookie_to_headers
|
39
38
|
|
40
|
-
expect(
|
39
|
+
expect(remember_token_cookie(session, "custom_cookie_name")).to be_present
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "with signed cookies == false" do
|
44
|
+
it "uses cookies.signed" do
|
45
|
+
Clearance.configuration.signed_cookie = true
|
46
|
+
|
47
|
+
cookie_jar = {}
|
48
|
+
expect(session).to receive(:cookies).and_return(cookie_jar)
|
49
|
+
expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
|
50
|
+
|
51
|
+
session.sign_in user
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "with signed cookies == true" do
|
56
|
+
it "uses cookies.signed" do
|
57
|
+
Clearance.configuration.signed_cookie = true
|
58
|
+
|
59
|
+
cookie_jar = {}
|
60
|
+
expect(session).to receive(:cookies).and_return(cookie_jar)
|
61
|
+
expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
|
62
|
+
|
63
|
+
session.sign_in user
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with signed cookies == :migrate" do
|
68
|
+
before do
|
69
|
+
Clearance.configuration.signed_cookie = :migrate
|
70
|
+
end
|
71
|
+
|
72
|
+
context "signed cookie exists" do
|
73
|
+
it "uses cookies.signed[remember_token]" do
|
74
|
+
cookie_jar = { "remember_token" => "signed cookie" }
|
75
|
+
expect(session).to receive(:cookies).and_return(cookie_jar)
|
76
|
+
expect(cookie_jar).to receive(:signed).and_return(cookie_jar)
|
77
|
+
|
78
|
+
session.sign_in user
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "signed cookie does not exist yet" do
|
83
|
+
it "uses cookies[remember_token] instead" do
|
84
|
+
cookie_jar = { "remember_token" => "signed cookie" }
|
85
|
+
# first call will try to get the signed cookie
|
86
|
+
expect(session).to receive(:cookies).and_return(cookie_jar)
|
87
|
+
# ... but signed_cookie doesn't exist
|
88
|
+
expect(cookie_jar).to receive(:signed).and_return({})
|
89
|
+
# then it attempts to retrieve the unsigned cookie
|
90
|
+
expect(session).to receive(:cookies).and_return(cookie_jar)
|
91
|
+
|
92
|
+
session.sign_in user
|
93
|
+
end
|
41
94
|
end
|
42
95
|
end
|
43
96
|
|
@@ -157,9 +210,9 @@ describe Clearance::Session do
|
|
157
210
|
end
|
158
211
|
|
159
212
|
it 'sets a httponly cookie' do
|
160
|
-
session.add_cookie_to_headers
|
213
|
+
session.add_cookie_to_headers
|
161
214
|
|
162
|
-
expect(
|
215
|
+
expect(remember_token_cookie(session)[:httponly]).to be_truthy
|
163
216
|
end
|
164
217
|
end
|
165
218
|
|
@@ -170,9 +223,9 @@ describe Clearance::Session do
|
|
170
223
|
end
|
171
224
|
|
172
225
|
it 'sets a standard cookie' do
|
173
|
-
session.add_cookie_to_headers
|
226
|
+
session.add_cookie_to_headers
|
174
227
|
|
175
|
-
expect(
|
228
|
+
expect(remember_token_cookie(session)[:httponly]).to be_falsey
|
176
229
|
end
|
177
230
|
end
|
178
231
|
|
@@ -183,9 +236,9 @@ describe Clearance::Session do
|
|
183
236
|
end
|
184
237
|
|
185
238
|
it "sets a same-site cookie" do
|
186
|
-
session.add_cookie_to_headers
|
239
|
+
session.add_cookie_to_headers
|
187
240
|
|
188
|
-
expect(
|
241
|
+
expect(remember_token_cookie(session)[:same_site]).to eq(:lax)
|
189
242
|
end
|
190
243
|
end
|
191
244
|
|
@@ -195,9 +248,9 @@ describe Clearance::Session do
|
|
195
248
|
end
|
196
249
|
|
197
250
|
it "sets a standard cookie" do
|
198
|
-
session.add_cookie_to_headers
|
251
|
+
session.add_cookie_to_headers
|
199
252
|
|
200
|
-
expect(
|
253
|
+
expect(remember_token_cookie(session)[:same_site]).to be_nil
|
201
254
|
end
|
202
255
|
end
|
203
256
|
|
@@ -205,15 +258,11 @@ describe Clearance::Session do
|
|
205
258
|
context 'default configuration' do
|
206
259
|
it 'is set to 1 year from now' do
|
207
260
|
user = double("User", remember_token: "123abc")
|
208
|
-
headers = {}
|
209
261
|
session = Clearance::Session.new(env_without_remember_token)
|
210
262
|
session.sign_in user
|
211
|
-
session.add_cookie_to_headers
|
263
|
+
session.add_cookie_to_headers
|
212
264
|
|
213
|
-
expect(
|
214
|
-
'remember_token',
|
215
|
-
user.remember_token, 1.year.from_now
|
216
|
-
)
|
265
|
+
expect(remember_token_cookie(session)[:expires]).to eq(1.year.from_now)
|
217
266
|
end
|
218
267
|
end
|
219
268
|
|
@@ -225,18 +274,14 @@ describe Clearance::Session do
|
|
225
274
|
end
|
226
275
|
with_custom_expiration expires_at do
|
227
276
|
user = double("User", remember_token: "123abc")
|
228
|
-
headers = {}
|
229
277
|
environment = env_with_cookies(remember_me: 'true')
|
230
278
|
session = Clearance::Session.new(environment)
|
231
279
|
session.sign_in user
|
232
|
-
session.add_cookie_to_headers
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
user.remember_token
|
237
|
-
remembered_expires
|
238
|
-
)
|
239
|
-
|
280
|
+
session.add_cookie_to_headers
|
281
|
+
expect(remember_token_cookie(session)[:expires]).to \
|
282
|
+
eq(remembered_expires)
|
283
|
+
expect(remember_token_cookie(session)[:value]).to \
|
284
|
+
eq(user.remember_token)
|
240
285
|
end
|
241
286
|
end
|
242
287
|
end
|
@@ -249,9 +294,9 @@ describe Clearance::Session do
|
|
249
294
|
end
|
250
295
|
|
251
296
|
it 'sets a standard cookie' do
|
252
|
-
session.add_cookie_to_headers
|
297
|
+
session.add_cookie_to_headers
|
253
298
|
|
254
|
-
expect(
|
299
|
+
expect(remember_token_cookie(session)[:secure]).to be_falsey
|
255
300
|
end
|
256
301
|
end
|
257
302
|
|
@@ -262,9 +307,9 @@ describe Clearance::Session do
|
|
262
307
|
end
|
263
308
|
|
264
309
|
it 'sets a secure cookie' do
|
265
|
-
session.add_cookie_to_headers
|
310
|
+
session.add_cookie_to_headers
|
266
311
|
|
267
|
-
expect(
|
312
|
+
expect(remember_token_cookie(session)[:secure]).to be_truthy
|
268
313
|
end
|
269
314
|
end
|
270
315
|
end
|
@@ -280,9 +325,9 @@ describe Clearance::Session do
|
|
280
325
|
let(:cookie_domain) { ".example.com" }
|
281
326
|
|
282
327
|
it "sets a standard cookie" do
|
283
|
-
session.add_cookie_to_headers
|
328
|
+
session.add_cookie_to_headers
|
284
329
|
|
285
|
-
expect(
|
330
|
+
expect(remember_token_cookie(session)[:domain]).to eq(cookie_domain)
|
286
331
|
end
|
287
332
|
end
|
288
333
|
|
@@ -290,9 +335,9 @@ describe Clearance::Session do
|
|
290
335
|
let(:cookie_domain) { lambda { |_r| ".example.com" } }
|
291
336
|
|
292
337
|
it "sets a standard cookie" do
|
293
|
-
session.add_cookie_to_headers
|
338
|
+
session.add_cookie_to_headers
|
294
339
|
|
295
|
-
expect(
|
340
|
+
expect(remember_token_cookie(session)[:domain]).to eq(".example.com")
|
296
341
|
end
|
297
342
|
end
|
298
343
|
end
|
@@ -301,9 +346,9 @@ describe Clearance::Session do
|
|
301
346
|
before { session.sign_in(user) }
|
302
347
|
|
303
348
|
it 'sets a standard cookie' do
|
304
|
-
session.add_cookie_to_headers
|
349
|
+
session.add_cookie_to_headers
|
305
350
|
|
306
|
-
expect(
|
351
|
+
expect(remember_token_cookie(session)[:domain]).to be_nil
|
307
352
|
end
|
308
353
|
end
|
309
354
|
end
|
@@ -313,9 +358,9 @@ describe Clearance::Session do
|
|
313
358
|
before { session.sign_in(user) }
|
314
359
|
|
315
360
|
it 'sets a standard cookie' do
|
316
|
-
session.add_cookie_to_headers
|
361
|
+
session.add_cookie_to_headers
|
317
362
|
|
318
|
-
expect(
|
363
|
+
expect(remember_token_cookie(session)[:domain]).to be_nil
|
319
364
|
end
|
320
365
|
end
|
321
366
|
|
@@ -326,18 +371,17 @@ describe Clearance::Session do
|
|
326
371
|
end
|
327
372
|
|
328
373
|
it 'sets a standard cookie' do
|
329
|
-
session.add_cookie_to_headers
|
374
|
+
session.add_cookie_to_headers
|
330
375
|
|
331
|
-
expect(
|
376
|
+
expect(remember_token_cookie(session)[:path]).to eq("/user")
|
332
377
|
end
|
333
378
|
end
|
334
379
|
end
|
335
380
|
|
336
381
|
it 'does not set a remember token when signed out' do
|
337
|
-
headers = {}
|
338
382
|
session = Clearance::Session.new(env_without_remember_token)
|
339
|
-
session.add_cookie_to_headers
|
340
|
-
expect(
|
383
|
+
session.add_cookie_to_headers
|
384
|
+
expect(remember_token_cookie(session)).to be_nil
|
341
385
|
end
|
342
386
|
|
343
387
|
describe "#sign_out" do
|
@@ -378,6 +422,35 @@ describe Clearance::Session do
|
|
378
422
|
expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
|
379
423
|
end
|
380
424
|
end
|
425
|
+
|
426
|
+
context 'with callable cookie domain' do
|
427
|
+
it 'clears cookie' do
|
428
|
+
domain = '.example.com'
|
429
|
+
Clearance.configuration.cookie_domain = ->(_) { domain }
|
430
|
+
user = create(:user)
|
431
|
+
env = env_with_remember_token(
|
432
|
+
value: user.remember_token,
|
433
|
+
domain: domain
|
434
|
+
)
|
435
|
+
session = Clearance::Session.new(env)
|
436
|
+
cookie_jar = ActionDispatch::Request.new(env).cookie_jar
|
437
|
+
expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be false
|
438
|
+
|
439
|
+
session.sign_out
|
440
|
+
|
441
|
+
expect(cookie_jar.deleted?(:remember_token, domain: domain)).to be true
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# a bit of a hack to get the cookies that ActionDispatch sets inside session
|
447
|
+
def remember_token_cookie(session, cookie_name = "remember_token")
|
448
|
+
cookies = session.send(:cookies)
|
449
|
+
# see https://stackoverflow.com/a/21315095
|
450
|
+
set_cookies = cookies.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
451
|
+
@set_cookies
|
452
|
+
RUBY
|
453
|
+
set_cookies[cookie_name]
|
381
454
|
end
|
382
455
|
|
383
456
|
def env_with_cookies(cookies)
|
data/spec/configuration_spec.rb
CHANGED
@@ -66,6 +66,34 @@ describe Clearance::Configuration do
|
|
66
66
|
end
|
67
67
|
end
|
68
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
|
+
|
69
97
|
context "when no redirect URL specified" do
|
70
98
|
it 'returns "/" as redirect URL' do
|
71
99
|
expect(Clearance::Configuration.new.redirect_url).to eq "/"
|
@@ -147,9 +147,15 @@ describe Clearance::Generators::InstallGenerator, :generator do
|
|
147
147
|
end
|
148
148
|
|
149
149
|
def preserve_original_primary_key_type_setting
|
150
|
-
|
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
|
+
|
151
154
|
yield
|
152
|
-
|
155
|
+
|
156
|
+
Rails.application.config.generators do |g|
|
157
|
+
g.orm :active_record, primary_key_type: original
|
158
|
+
end
|
153
159
|
end
|
154
160
|
|
155
161
|
def contain_models_inherit_from
|