Skip to content
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

Add lockout_reset_timeout #191

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down Expand Up @@ -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?

Expand All @@ -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"]))
Expand All @@ -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}"
Expand All @@ -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
```


Expand All @@ -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 )

Expand All @@ -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:

Expand All @@ -399,6 +400,6 @@ to set TOTP to DISABLED for a user account:
current_user.direct_otp? => false
current_user.totp_enabled? => false
```



Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/generators/active_record/templates/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/two_factor_authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 18 additions & 2 deletions lib/two_factor_authentication/models/two_factor_authenticatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -74,13 +75,15 @@ 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

def max_login_attempts
self.class.max_login_attempts
end


def totp_enabled?
respond_to?(:otp_secret_key) && !otp_secret_key.nil?
end
Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_attributes has been deprecated so think this would need to be update

https://apidock.com/rails/ActiveRecord/Base/update_attributes

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
Expand Down
4 changes: 4 additions & 0 deletions lib/two_factor_authentication/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down