1
0
Fork 0
mirror of https://github.com/heartcombo/devise.git synced 2022-11-09 12:18:31 -05:00

Make #increment_failed_attempts concurrency safe (#4996)

As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was
not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example:

```
Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2
    Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2
```

In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. 

This commit handles this case by calling `ActiveRecord::CounterCache.increment_counter` method, which will do both steps at once, reading the value straight from the database.

This commit also adds a `ActiveRecord::AttributeMethods::Dirty#reload` call to ensure that the application gets the updated value - i.e. that other request might have updated. 
Although this does not ensure that the value is in fact the most recent one - other request could've updated it after the `reload` call - it seems good enough for this implementation. 
Even if a request does not locks the account because it has a stale value, the next one - that updated that value - will do it. That's why we decided not to use a pessimistic lock here.

Closes #4981.
This commit is contained in:
Leonardo Tegon 2018-12-28 17:00:50 -02:00 committed by GitHub
parent e3a00b27d1
commit 62703943be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 13 additions and 2 deletions

View file

@ -112,8 +112,8 @@ module Devise
end
def increment_failed_attempts
self.failed_attempts ||= 0
self.failed_attempts += 1
self.class.increment_counter(:failed_attempts, id)
reload
end
def unauthenticated_message

View file

@ -39,6 +39,17 @@ class LockableTest < ActiveSupport::TestCase
end
end
test "should read failed_attempts from database when incrementing" do
user = create_user
initial_failed_attempts = user.failed_attempts
same_user = User.find(user.id)
user.increment_failed_attempts
same_user.increment_failed_attempts
assert_equal initial_failed_attempts + 2, user.reload.failed_attempts
end
test 'should be valid for authentication with a unlocked user' do
user = create_user
user.lock_access!