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
Fix acceptance of invalid otp attempts #173
Open
kmwhite
wants to merge
2
commits into
devise-two-factor:main
Choose a base branch
from
buildoutinc:fix-acceptance-of-invalid-otp-attempts
base: main
Could not load branches
Branch not found: {{ refName }}
Could not load tags
Nothing to show
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Fix acceptance of invalid otp attempts #173
kmwhite
wants to merge
2
commits into
devise-two-factor:main
from
buildoutinc:fix-acceptance-of-invalid-otp-attempts
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In the event that an OTP attempt is invalid, the super method is never called. If the authentication_hash did describe a record, it was not rejecting the authentication. The find_for_database_authentication method is inherited from Devise::Strategies::DatabaseAuthenticatable which pays no attention to the otp_attempt, considering this pry session: From: /usr/local/bundle/bundler/gems/devise-two-factor-07acb0ae5068/lib/devise_two_factor/strategies/two_factor_authenticatable.rb @ line 12 Devise::Strategies::TwoFactorAuthenticatable#authenticate!: 1: module Devise 2: module Strategies 3: class TwoFactorAuthenticatable < Devise::Strategies::DatabaseAuthenticatable 4: 5: def authenticate! 6: resource = mapping.to.find_for_database_authentication(authentication_hash) 7: # We authenticate in two cases: 8: # 1. The password and the OTP are correct 9: # 2. The password is correct, and OTP is not required for login 10: # We check the OTP, then defer to DatabaseAuthenticatable 11: require "pry"; binding.pry => 12: if validate(resource) { validate_otp(resource) } 13: super 14: end 15: 16: fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource 17: 18: # We want to cascade to the next strategy if this one fails, 19: # but database authenticatable automatically halts on a bad password 20: @Halted = false if @Result == :failure 21: end 22: 23: def validate_otp(resource) 24: return true unless resource.otp_required_for_login 25: return if params[scope]['otp_attempt'].nil? 26: resource.validate_and_consume_otp!(params[scope]['otp_attempt']) 27: end 28: end 29: end 30: end 31: 32: Warden::Strategies.add(:two_factor_authenticatable, Devise::Strategies::TwoFactorAuthenticatable) [6] pry(#<Devise::Strategies::TwoFactorAuthenticatable>)> params[scope] => {"login"=>"miranda", "password"=>"password", "otp_attempt"=>"123456"} [7] pry(#<Devise::Strategies::TwoFactorAuthenticatable>)> validate(resource) => true [8] pry(#<Devise::Strategies::TwoFactorAuthenticatable>)> validate_otp(resource) => false [9] pry(#<Devise::Strategies::TwoFactorAuthenticatable>)> next From: /usr/local/bundle/bundler/gems/devise-two-factor-07acb0ae5068/lib/devise_two_factor/strategies/two_factor_authenticatable.rb @ line 16 Devise::Strategies::TwoFactorAuthenticatable#authenticate!: 5: def authenticate! 6: resource = mapping.to.find_for_database_authentication(authentication_hash) 7: # We authenticate in two cases: 8: # 1. The password and the OTP are correct 9: # 2. The password is correct, and OTP is not required for login 10: # We check the OTP, then defer to DatabaseAuthenticatable 11: require "pry"; binding.pry 12: if validate(resource) { validate_otp(resource) } 13: super 14: end 15: => 16: fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource 17: 18: # We want to cascade to the next strategy if this one fails, 19: # but database authenticatable automatically halts on a bad password 20: @Halted = false if @Result == :failure 21: end [9] pry(#<Devise::Strategies::TwoFactorAuthenticatable>):1> resource.present? => true [10] pry(#<Devise::Strategies::TwoFactorAuthenticatable>):1> resource.login => "miranda" [11] pry(#<Devise::Strategies::TwoFactorAuthenticatable>):1> While the `super` is not called to set up the session, but it does not necessarily *fail* the login. This change ensures that fail() is called when this case is hit.
I ran into this issue in a Rails 4.2 (through Rails 5.2) app we run at Buildout Inc. These changes ensured that our authentication flow did correctly abort when the wrong OTP attempt was provided when required. I also added a set of specs for the strategy itself to verify this. If there's any feedback or requested changes, I'm happy to adjust this PR! Thanks! |
Oof. Saw CI failed. Fixing the faker callers. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
In the event that an OTP attempt is invalid, the super method is never
called. If the authentication_hash did describe a record, it was not
rejecting the authentication. The find_for_database_authentication
method is inherited from Devise::Strategies::DatabaseAuthenticatable
which pays no attention to the otp_attempt, considering this pry
session:
While the
super
is not called to set up the session, but it does notnecessarily fail the login. This change ensures that fail() is called
when this case is hit.