Skip to content

Commit 8998f92

Browse files
moritzhoeppnerThiagoAnunciacao
authored andcommitted
Don't leak information about the existence of accounts in SessionsController (lynndylanhurley#1600)
* Don't leak information about the existence of accounts in DeivseTokenAuth::SessionsController * Refactor SessionsController
1 parent f811af0 commit 8998f92

File tree

2 files changed

+117
-43
lines changed

2 files changed

+117
-43
lines changed

app/controllers/devise_token_auth/sessions_controller.rb

+11-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ def new
1111
end
1212

1313
def create
14-
# Check
15-
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
16-
17-
@resource = nil
18-
if field
14+
if field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
1915
q_value = get_case_insensitive_field_from_resource_params(field)
2016

2117
@resource = find_resource(field, q_value)
@@ -34,13 +30,14 @@ def create
3430
yield @resource if block_given?
3531

3632
render_create_success
37-
elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
33+
elsif @resource && !Devise.paranoid && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
3834
if @resource.respond_to?(:locked_at) && @resource.locked_at
3935
render_create_error_account_locked
4036
else
4137
render_create_error_not_confirmed
4238
end
4339
else
40+
hash_password_in_paranoid_mode
4441
render_create_error_bad_credentials
4542
end
4643
end
@@ -144,5 +141,13 @@ def create_and_assign_token
144141
@resource.save!
145142
end
146143
end
144+
145+
def hash_password_in_paranoid_mode
146+
# In order to avoid timing attacks in paranoid mode, we want the password hash to be
147+
# calculated even if no resource has been found. Devise's DatabaseAuthenticatable warden
148+
# strategy handles this case similarly:
149+
# https://github.com/heartcombo/devise/blob/main/lib/devise/strategies/database_authenticatable.rb
150+
resource_class.new.password = resource_params[:password] if Devise.paranoid
151+
end
147152
end
148153
end

test/controllers/devise_token_auth/sessions_controller_test.rb

+106-37
Original file line numberDiff line numberDiff line change
@@ -310,23 +310,47 @@ def @controller.reset_session
310310
end
311311

312312
describe 'Unconfirmed user' do
313-
before do
314-
@unconfirmed_user = create(:user)
315-
post :create, params: { email: @unconfirmed_user.email,
316-
password: @unconfirmed_user.password }
317-
@resource = assigns(:resource)
318-
@data = JSON.parse(response.body)
319-
end
313+
describe 'Without paranoid mode' do
314+
before do
315+
@unconfirmed_user = create(:user)
316+
post :create, params: { email: @unconfirmed_user.email,
317+
password: @unconfirmed_user.password }
318+
@resource = assigns(:resource)
319+
@data = JSON.parse(response.body)
320+
end
320321

321-
test 'request should fail' do
322-
assert_equal 401, response.status
322+
test 'request should fail' do
323+
assert_equal 401, response.status
324+
end
325+
326+
test 'response should contain errors' do
327+
assert @data['errors']
328+
assert_equal @data['errors'],
329+
[I18n.t('devise_token_auth.sessions.not_confirmed',
330+
email: @unconfirmed_user.email)]
331+
end
323332
end
333+
334+
describe 'With paranoid mode' do
335+
before do
336+
@unconfirmed_user = create(:user)
337+
swap Devise, paranoid: true do
338+
post :create, params: { email: @unconfirmed_user.email,
339+
password: @unconfirmed_user.password }
340+
end
341+
@resource = assigns(:resource)
342+
@data = JSON.parse(response.body)
343+
end
324344

325-
test 'response should contain errors' do
326-
assert @data['errors']
327-
assert_equal @data['errors'],
328-
[I18n.t('devise_token_auth.sessions.not_confirmed',
329-
email: @unconfirmed_user.email)]
345+
test 'request should fail' do
346+
assert_equal 401, response.status
347+
end
348+
349+
test 'response should contain errors that do not leak the existence of the account' do
350+
assert @data['errors']
351+
assert_equal @data['errors'],
352+
[I18n.t('devise_token_auth.sessions.bad_credentials')]
353+
end
330354
end
331355
end
332356

@@ -375,20 +399,42 @@ def @controller.reset_session
375399
end
376400

377401
describe 'Non-existing user' do
378-
before do
379-
post :create,
380-
params: { email: -> { Faker::Internet.email },
381-
password: -> { Faker::Number.number(10) } }
382-
@resource = assigns(:resource)
383-
@data = JSON.parse(response.body)
384-
end
402+
describe 'Without paranoid mode' do
403+
before do
404+
post :create,
405+
params: { email: -> { Faker::Internet.email },
406+
password: -> { Faker::Number.number(10) } }
407+
@resource = assigns(:resource)
408+
@data = JSON.parse(response.body)
409+
end
385410

386-
test 'request should fail' do
387-
assert_equal 401, response.status
411+
test 'request should fail' do
412+
assert_equal 401, response.status
413+
end
414+
415+
test 'response should contain errors' do
416+
assert @data['errors']
417+
end
388418
end
389419

390-
test 'response should contain errors' do
391-
assert @data['errors']
420+
describe 'With paranoid mode' do
421+
before do
422+
mock_hash = '$2a$04$MUWADkfA6MHXDdWHoep6QOvX1o0Y56pNqt3NMWQ9zCRwKSp1HZJba'
423+
@bcrypt_mock = MiniTest::Mock.new
424+
@bcrypt_mock.expect(:call, mock_hash, [Object, String])
425+
426+
swap Devise, paranoid: true do
427+
BCrypt::Engine.stub :hash_secret, @bcrypt_mock do
428+
post :create,
429+
params: { email: -> { Faker::Internet.email },
430+
password: -> { Faker::Number.number(10) } }
431+
end
432+
end
433+
end
434+
435+
test 'password should be hashed' do
436+
@bcrypt_mock.verify
437+
end
392438
end
393439
end
394440

@@ -472,21 +518,44 @@ def @controller.reset_session
472518
end
473519

474520
describe 'locked user' do
475-
before do
476-
@locked_user = create(:lockable_user, :locked)
477-
post :create,
478-
params: { email: @locked_user.email,
479-
password: @locked_user.password }
480-
@data = JSON.parse(response.body)
481-
end
521+
describe 'Without paranoid mode' do
522+
before do
523+
@locked_user = create(:lockable_user, :locked)
524+
post :create,
525+
params: { email: @locked_user.email,
526+
password: @locked_user.password }
527+
@data = JSON.parse(response.body)
528+
end
482529

483-
test 'request should fail' do
484-
assert_equal 401, response.status
530+
test 'request should fail' do
531+
assert_equal 401, response.status
532+
end
533+
534+
test 'response should contain errors' do
535+
assert @data['errors']
536+
assert_equal @data['errors'], [I18n.t('devise.mailer.unlock_instructions.account_lock_msg')]
537+
end
485538
end
486539

487-
test 'response should contain errors' do
488-
assert @data['errors']
489-
assert_equal @data['errors'], [I18n.t('devise.mailer.unlock_instructions.account_lock_msg')]
540+
describe 'With paranoid mode' do
541+
before do
542+
@locked_user = create(:lockable_user, :locked)
543+
swap Devise, paranoid: true do
544+
post :create,
545+
params: { email: @locked_user.email,
546+
password: @locked_user.password }
547+
end
548+
@data = JSON.parse(response.body)
549+
end
550+
551+
test 'request should fail' do
552+
assert_equal 401, response.status
553+
end
554+
555+
test 'response should contain errors that do not leak the existence of the account' do
556+
assert @data['errors']
557+
assert_equal @data['errors'], [I18n.t('devise_token_auth.sessions.bad_credentials')]
558+
end
490559
end
491560
end
492561

0 commit comments

Comments
 (0)