Skip to content
Merged
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
173 changes: 155 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,24 +225,24 @@ steps:
Open the generated file, and replace its contents with the following:
```ruby
class PopulateEncryptedOtpFields < ActiveRecord::Migration
def up
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = user.read_attribute('otp_secret_key')
user.save!
end
end

def down
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = ROTP::Base32.random_base32
user.save!
end
end
end
def up
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = user.read_attribute('otp_secret_key')
user.save!
end
end

def down
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = ROTP::Base32.random_base32
user.save!
end
end
end
```

5. Generate a migration to remove the `:otp_secret_key` column:
Expand All @@ -263,6 +263,143 @@ after them):
bundle exec rake db:rollback STEP=3
```

#### Critical Security Note! Add before_action to your user registration controllers

You should have a file registrations_controller.rb in your controllers folder
to overwrite/customize user registrations. It should include the lines below, for 2FA protection of user model updates, meaning that users can only access the users/edit page after confirming 2FA fully, not simply by logging in. Otherwise the entire 2FA system can be bypassed!

```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?

flash[:error] = t('devise.errors.messages.user_not_authenticated')
redirect_to user_two_factor_authentication_url
end
end
```

#### Critical Security Note! Add 2FA validation to your custom user actions

Make sure you are passing the 2FA secret codes securely and checking for them upon critical user actions, such as API key updates, user email or pgp pubkey updates, or any other changess to private/secure account-related details. Validate the secret during the initial 2FA key/secret verification by the user also, of course.

For example, a simple account_controller.rb may look something like this:

```
require 'json'

class AccountController < ApplicationController
before_action :require_signed_in!
before_action :authenticate_user!
respond_to :html, :json

def account_API
resp = {}
begin
if(account_params["twoFAKey"] && account_params["twoFASecret"])
current_user.otp_secret_key = account_params["twoFAKey"]
if(current_user.authenticate_totp(account_params["twoFASecret"]))
# user has validated their temporary 2FA code, save it to their account, enable 2FA on this account
current_user.save!
resp['success'] = "passed 2FA validation!"
else
resp['error'] = "failed 2FA validation!"
end
elsif(param[:userAccountStuff] && param[:userAccountWidget])
#before updating important user account stuff and widgets,
#check to see that the 2FA secret has also been passed in, and verify it...
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!
resp[error] = 'You failed 2FA validation!'
end

...
end
else
resp['error'] = 'unknown format error, not saved!'
end
rescue Exception => e
puts "WARNING: account api threw error : '#{e}' for user #{current_user.username}"
#print "error trace: #{e.backtrace}\n"
resp['error'] = "unanticipated server response"
end
render json: resp.to_json
end

def account_params
params.require(:twoFA).permit(:userAccountStuff, :userAcountWidget, :twoFAKey, :twoFASecret)
end
end
```


### Example App

[TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample)


### Example user actions

to use an ENV VAR for the 2FA encryption key:

config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']

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 )

to check if user has input the correct code (from the QR display page)
before saving the user model:

```
current_user.authenticate_totp('123456')
```

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:

otpauth://totp/LABEL?secret=p6wwetjnkjnrcmpd (example secret used here)

where LABEL should be something like "example.com (Username)", which shows up in their GA app to remind them the code is for example.com

this returns true or false with an allowed_otp_drift_seconds 'grace period'

to set TOTP to DISABLED for a user account:

```
current_user.second_factor_attempts_count=nil
current_user.encrypted_otp_secret_key=nil
current_user.encrypted_otp_secret_key_iv=nil
current_user.encrypted_otp_secret_key_salt=nil
current_user.direct_otp=nil
current_user.direct_otp_sent_at=nil
current_user.totp_timestamp=nil
current_user.direct_otp=nil
current_user.otp_secret_key=nil
current_user.otp_confirmed=nil
current_user.save! (if in ruby code instead of console)
current_user.direct_otp? => false
current_user.totp_enabled? => false
```