New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lockable with two factor backupable causing double failed attempts #127
Comments
any update or workaround for this? |
Is there any update for this one? |
Definitely an issue. I don't have a P/R for this, but the issue is pretty straightforward. Each of the two strategies calls the base class validate method. If that method fails (including a two factor fail), it records a failure attempt. My work around was fairly simple:
initializers/devise.rb
Register a new strategy that does one check for authentication and two factor. (Mine implements a remember_me, that's the third call):
|
There is an open PR for this from over 8 months ago. #136. Is this likely to ever be fixed? It's been a problem for 4 years #28. I would happily offer a PR, but my knowledge of the inner workings of this are lacking.
This sadly just stops the failed_attempts increase completely for me. |
After researching for a couple days and trying many solutions, I found it simplest to just work around this bug in the library for the time being. You can override the Add the following into your User model:
I should note that if you are having an issue with the |
Is this fixed? PR is still open |
@verenion I had some trouble with it myself. As it is now 2022 and there is still no fix for this problem a response anyway: @BiggerNoise's solution did work for me. Make sure you create a custom strategy like this: lib/custom_authenticatable.rb module Devise
module Strategies
class CustomAuthenticatable < TwoFactorAuthenticatable
def authenticate!
resource = mapping.to.find_for_database_authentication(authentication_hash)
if validate(resource) {validate_otp(resource) || recover_otp(resource) || validate_remembered_computer(resource)}
super
end
fail(:not_found_in_database) unless resource
# We want to cascade to the next strategy if this one fails,
# but database authenticatable automatically halts on a bad password
@halted = false if @result == :failure
end
end
end
end
Warden::Strategies.add(:custom_authenticatable, Devise::Strategies::CustomAuthenticatable) initializers/devise.rb require 'custom_authenticatable'
Devise.setup do |config|
config.warden do |manager|
strategies = manager.default_strategies(:scope => :user)
strategies.delete :two_factor_backupable
strategies.delete :two_factor_authenticatable
strategies.unshift :custom_authenticatable
end
end |
For anyone reading this today, this is what I had to do to make this work: module Devise
module Strategies
# This combines database_authenticatable with two_factor_authenticatable
# and two_factor_backupable in such a way that does not generate more than
# one lockable failed-attempt per login attempt.
class CustomAuthenticatable < Devise::Strategies::DatabaseAuthenticatable
# Warden authentication Strategy method
# @see Warden::Strategies::Base#authenticate!
# @see Devise::Strategies::TwoFactorAuthenticatable
# @see Devise::Strategies::TwoFactorBackupable
def authenticate!
resource = mapping.to.find_for_database_authentication(authentication_hash)
validation_result = validate(resource) do
validate_otp(resource) ||
resource.invalidate_otp_backup_code!(params[scope]['otp_attempt'])
end
if validation_result
# Devise fails to authenticate invalidated resources, but if we've
# gotten here, the object changed (Since we potentially deleted a
# recovery code)
resource.save!
super
end
fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource
end
# Validate provided OTP Attempt if this user has OTP enabled
# @params resource [User]
# @see Devise::Strategies::TwoFactorAuthenticatable#validate_otp
def validate_otp(resource)
return true unless resource.otp_required_for_login
return if params[scope].nil? || params[scope]['otp_attempt'].nil?
resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
end
end
end
end
Warden::Strategies.add(:custom_authenticatable, Devise::Strategies::CustomAuthenticatable) initializers/devise.rb require 'custom_authenticatable'
Devise.setup do |config|
config.warden do |manager|
strategies = manager.default_strategies(:scope => :user)
strategies.delete :two_factor_backupable
strategies.delete :two_factor_authenticatable
strategies.unshift :custom_authenticatable
end
end The main difference is that this custom authenticator performs everything that TwoFactorAuthenticatable and TwoFactorBackupable does without inheriting TwoFactorAuthenticatable. The reason that inheriting TwoFactorAuthenticatable does not work is because calling Since this inherits from DatabaseAuthenticatable, the |
module Devise
OTP_AUTH_PARAM_NAME = "otp_attempt".freeze
module Strategies
class CustomDatabaseAuthenticatable < Devise::Strategies::DatabaseAuthenticatable
def authenticate!
resource = mapping.to.find_for_database_authentication(authentication_hash)
# We authenticate in two cases:
# 1. The password and the OTP or backup-code are correct
# 2. The password is correct, and OTP is not required for login
# We check the OTP, then defer to DatabaseAuthenticatable (super)
super if validate(resource) { validate_otp(resource) || validate_backup(resource) }
# user not found (doesnt exits on db)
fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource
# super has been called and set the result
fail(error_code) if @result == :failure
end
def validate_otp(resource)
return true unless resource.otp_required_for_login
return if params[scope].nil? || params[scope][Devise::OTP_AUTH_PARAM_NAME].nil?
otp_result = resource.validate_and_consume_otp!(params[scope][Devise::OTP_AUTH_PARAM_NAME])
@otp_failed = !otp_result
otp_result
end
def validate_backup(resource)
return if params[scope].nil? || params[scope][Devise::OTP_AUTH_PARAM_NAME].nil?
result = resource.invalidate_otp_backup_code!(params[scope][Devise::OTP_AUTH_PARAM_NAME])
resource.save! if result
@otp_failed = !result
result
end
def error_code
if @otp_failed
:invalid_otp
else
Devise.paranoid ? :invalid : :not_found_in_database
end
end
end
end
end
Warden::Strategies.add(:custom_database_authenticatable, Devise::Strategies::CustomDatabaseAuthenticatable) |
Heyo,
In our User model we have
When I enter an incorrect 2fa code the users
failed_attempts
is incremented by two instead of 1.I found where
failed_attempts
incremented in thelockable
code and added a log ofcaller
to see the call stack and I can see that bothtwo_factor_authenticatable
andtwo_factor_backupable
are incrementing thefailed_attempts
.I've tried various changes to the configuration and re-followed all the setup instructions with no luck. Any insight would be welcome! I'm happy to make a pull request for a fix I just don't know what's wrong.
Thanks!
Rails version: 5.1.4
Ruby: 2.4
gem versions:
The text was updated successfully, but these errors were encountered: