diff --git a/README.md b/README.md index f4f91d1a..2cac0a9e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Where MODEL is your model name (e.g. User or Admin). This generator will add migration in `db/migrate/`, which will add the following columns to your table: - `:second_factor_attempts_count` +- `:lockout_reset_timeout` - `:encrypted_otp_secret_key` - `:encrypted_otp_secret_key_iv` - `:encrypted_otp_secret_key_salt` @@ -64,7 +65,7 @@ devise :database_authenticatable, :registerable, :recoverable, :rememberable, Then create your migration file using the Rails generator, such as: ``` -rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp +rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer lockout_reset_timeout:datetime encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp ``` Open your migration file (it will be in the `db/migrate` directory and will be @@ -271,9 +272,9 @@ to overwrite/customize user registrations. It should include the lines below, fo ```ruby class RegistrationsController < Devise::RegistrationsController before_action :confirm_two_factor_authenticated, except: [:new, :create, :cancel] - + protected - + def confirm_two_factor_authenticated return if is_fully_authenticated? @@ -296,10 +297,10 @@ Make sure you are passing the 2FA secret codes securely and checking for them up before_action :require_signed_in! before_action :authenticate_user! respond_to :html, :json - + def account_API resp = {} - begin + begin if(account_params["twoFAKey"] && account_params["twoFASecret"]) current_user.otp_secret_key = account_params["twoFAKey"] if(current_user.authenticate_totp(account_params["twoFASecret"])) @@ -315,15 +316,15 @@ Make sure you are passing the 2FA secret codes securely and checking for them up if(account_params["twoFASecret"] && current_user.totp_enabled? && current_user.authenticate_totp(account_params["twoFASecret"])) # user has passed 2FA checks, do cool user account stuff here ... - else - # user failed 2FA check! No cool user stuff happens! + else + # user failed 2FA check! No cool user stuff happens! resp[error] = 'You failed 2FA validation!' end - + ... end else - resp['error'] = 'unknown format error, not saved!' + resp['error'] = 'unknown format error, not saved!' end rescue Exception => e puts "WARNING: account api threw error : '#{e}' for user #{current_user.username}" @@ -332,11 +333,11 @@ Make sure you are passing the 2FA secret codes securely and checking for them up end render json: resp.to_json end - + def account_params params.require(:twoFA).permit(:userAccountStuff, :userAcountWidget, :twoFAKey, :twoFASecret) end - end + end ``` @@ -357,7 +358,7 @@ to set up TOTP for Google Authenticator for user: current_user.otp_secret_key = current_user.generate_totp_secret current_user.save! ``` - + ( encrypted db fields are set upon user model save action, rails c access relies on setting env var: OTP_SECRET_ENCRYPTION_KEY ) @@ -369,11 +370,11 @@ before saving the user model: ``` additional note: - + ``` current_user.otp_secret_key ``` - + This returns the OTP secret key in plaintext for the user (if you have set the env var) in the console the string used for generating the QR given to the user for their Google Auth is something like: @@ -399,6 +400,6 @@ to set TOTP to DISABLED for a user account: current_user.direct_otp? => false current_user.totp_enabled? => false ``` - + diff --git a/app/controllers/devise/two_factor_authentication_controller.rb b/app/controllers/devise/two_factor_authentication_controller.rb index 0e7aed84..dc4447e7 100644 --- a/app/controllers/devise/two_factor_authentication_controller.rb +++ b/app/controllers/devise/two_factor_authentication_controller.rb @@ -62,6 +62,7 @@ def after_two_factor_fail_for(resource) set_flash_message :alert, :attempt_failed, now: true if resource.max_login_attempts? + resource.update(second_factor_attempts_locked_at: Time.now) sign_out(resource) render :max_login_attempts_reached else diff --git a/lib/generators/active_record/templates/migration.rb b/lib/generators/active_record/templates/migration.rb index 251ef402..0e4bada5 100644 --- a/lib/generators/active_record/templates/migration.rb +++ b/lib/generators/active_record/templates/migration.rb @@ -7,6 +7,7 @@ def change add_column :<%= table_name %>, :direct_otp, :string add_column :<%= table_name %>, :direct_otp_sent_at, :datetime add_column :<%= table_name %>, :totp_timestamp, :timestamp + add_column :<%= table_name %>, :second_factor_attempts_locked_at, :datetime add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true end diff --git a/lib/two_factor_authentication.rb b/lib/two_factor_authentication.rb index 7b1bbbc1..d8d14a3d 100644 --- a/lib/two_factor_authentication.rb +++ b/lib/two_factor_authentication.rb @@ -9,6 +9,9 @@ module Devise mattr_accessor :max_login_attempts @@max_login_attempts = 3 + mattr_accessor :lockout_reset_timeout + @@lockout_reset_timeout = 5.minutes + mattr_accessor :allowed_otp_drift_seconds @@allowed_otp_drift_seconds = 30 diff --git a/lib/two_factor_authentication/models/two_factor_authenticatable.rb b/lib/two_factor_authentication/models/two_factor_authenticatable.rb index 6d73a0fb..3b014682 100644 --- a/lib/two_factor_authentication/models/two_factor_authenticatable.rb +++ b/lib/two_factor_authentication/models/two_factor_authenticatable.rb @@ -16,7 +16,8 @@ def has_one_time_password(options = {}) ::Devise::Models.config( self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, :remember_otp_session_for_seconds, :otp_secret_encryption_key, - :direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout + :direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout, + :lockout_reset_timeout ) end @@ -40,7 +41,7 @@ def authenticate_totp(code, options = {}) raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil? totp = ROTP::TOTP.new(totp_secret, digits: digits) new_timestamp = totp.verify( - without_spaces(code), + without_spaces(code), drift_ahead: drift, drift_behind: drift, after: totp_timestamp ) return false unless new_timestamp @@ -74,6 +75,7 @@ def send_two_factor_authentication_code(code) end def max_login_attempts? + reset_lockout second_factor_attempts_count.to_i >= max_login_attempts.to_i end @@ -81,6 +83,7 @@ def max_login_attempts self.class.max_login_attempts end + def totp_enabled? respond_to?(:otp_secret_key) && !otp_secret_key.nil? end @@ -109,6 +112,19 @@ def create_direct_otp(options = {}) private + def reset_lockout + return unless self.class.lockout_reset_timeout.present? + + if should_reset_lockout? + update_attributes(second_factor_attempts_count: 0, second_factor_attempts_locked_at: nil) + end + end + + def should_reset_lockout? + return unless second_factor_attempts_locked_at.present? + Time.now - self.class.lockout_reset_timeout > second_factor_attempts_locked_at + end + def without_spaces(code) code.gsub(/\s/, '') end diff --git a/lib/two_factor_authentication/schema.rb b/lib/two_factor_authentication/schema.rb index 9f938b7c..99375964 100644 --- a/lib/two_factor_authentication/schema.rb +++ b/lib/two_factor_authentication/schema.rb @@ -4,6 +4,10 @@ def second_factor_attempts_count apply_devise_schema :second_factor_attempts_count, Integer, :default => 0 end + def second_factor_attempts_locked_at + apply_devise_schema :second_factor_attempts_locked_at, DateTime + end + def encrypted_otp_secret_key apply_devise_schema :encrypted_otp_secret_key, String end