GithubHelp home page GithubHelp logo

devise-two-factor's Introduction

Devise-Two-Factor Authentication

Build Status

Devise-Two-Factor is a minimalist extension to Devise which offers support for two-factor authentication, through the TOTP scheme. It:

  • Allows you to incorporate two-factor authentication into your existing models
  • Is opinionated about security, so you don't have to be
  • Integrates easily with two-factor applications like Google Authenticator and Authy
  • Is extensible, and includes two-factor backup codes as an example of how plugins can be structured

Contributing

We welcome pull requests, bug reports, and other contributions. We're especially looking for help getting this gem fully compatible with Rails 5+ and squashing any deprecation messages.

Example App

An example Rails 4 application is provided in the demo directory. It showcases a minimal example of Devise-Two-Factor in action, and can act as a reference for integrating the gem into your own application.

For the demo app to work, create an encryption key and store it as an environment variable. One way to do this is to create a file named local_env.yml in the application root. Set the value of ENCRYPTION_KEY in the YML file. That value will be loaded into the application environment by application.rb.

Getting Started

Devise-Two-Factor doesn't require much to get started, but there are two prerequisites before you can start using it in your application:

  1. A Rails application with devise installed
  2. Secrets configured for ActiveRecord encrypted attributes

First, you'll need a Rails application setup with Devise. Visit the Devise homepage for instructions.

Devise-Two-Factor uses ActiveRecord encrypted attributes. If you haven't already set up ActiveRecord encryption you must generate a key set and configure your application to use them either with Rails' encrypted credentials or from another source such as environment variables.

# Generates a random key set and outputs it to stdout
./bin/rails db:encryption:init

You can load the key set using Rails' credentials.

# Copy the generated key set into your encrypted credentials file
# Setting the EDITOR environment variable is optional, but without it your default editor will open
EDITOR="code --wait" ./bin/rails credentials:edit

To learn more about credentials run ./bin/rails credentials:help.

Alternatively, you can configure your application with environment variables rather than Rails' credentials.

# Copy the generate key set and set them as environment variables

config.active_record.encryption.primary_key = ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY']
config.active_record.encryption.deterministic_key = ENV['ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY']
config.active_record.encryption.key_derivation_salt = ENV['ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT']

Add Devise-Two-Factor to your Gemfile with:

# Gemfile

gem 'devise-two-factor'

There is a generator which automates most of the setup:

# MODEL is the name of the model you wish to configure devise_two_factor e.g. User or Admin
./bin/rails generate devise_two_factor MODEL

Where MODEL is the name of the model you wish to add two-factor functionality to (for example user)

This generator will:

  1. Create a new migration which adds a few columns to the specified model:

    add_column :users, :otp_secret, :string
    add_column :users, :consumed_timestep, :integer
    add_column :users, :otp_required_for_login, :boolean
  2. Edit app/models/MODEL.rb (where MODEL is your model name):

    • add the :two_factor_authenticatable devise module
    • remove the :database_authenticatable if present because it is incompatible with :two_factor_authenticatable
  3. Add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authentication.

Remember to apply the new migration after you run the generator:

./bin/rails db:migrate

Next you need to whitelist :otp_attempt as a permitted parameter in Devise :sign_in controller. You can do this by adding the following to your application_controller.rb:

# app/controllers/application_controller.rb

  before_action :configure_permitted_parameters, if: :devise_controller?

  # ...

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
  end

Finally you should verify that :database_authenticatable is not being loaded by your model. The generator will try to remove it, but if you have a non-standard Devise setup, this step may fail.

Loading both :database_authenticatable and :two_factor_authenticatable in a model is a security issue It will allow users to bypass two-factor authenticatable due to the way Warden handles cascading strategies!

Designing Your Workflow

Devise-Two-Factor only worries about the backend, leaving the details of the integration up to you. This means that you're responsible for building the UI that drives the gem. While there is an example Rails application included in the gem, it is important to remember that this gem is intentionally very open-ended, and you should build a user experience which fits your individual application.

There are two key workflows you'll have to think about:

  1. Logging in with two-factor authentication
  2. Enabling two-factor authentication for a given user

We chose to keep things as simple as possible, and our implementation can be found by registering at Tinfoil Security, and enabling two-factor authentication from the security settings page.

Logging In

Logging in with two-factor authentication works extremely similarly to regular database authentication in Devise. The TwoFactorAuthenticatable strategy accepts three parameters:

  1. email
  2. password
  3. otp_attempt (Their one-time password for this session)

These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you.

Disabling Automatic Login After Password Resets

If you use the Devise recoverable strategy, the default behavior after a password reset is to automatically authenticate the user and log them in. This is obviously a problem if a user has two-factor authentication enabled, as resetting the password would get around the two-factor requirement.

Because of this, you need to set sign_in_after_reset_password to false (either globally in your Devise initializer or via devise_for).

Enabling Two-Factor Authentication

Enabling two-factor authentication for a user is easy. For example, if my user model were named User, I could do the following:

current_user.otp_required_for_login = true
current_user.otp_secret = User.generate_otp_secret
current_user.save!

Before you can do this however, you need to decide how you're going to transmit two-factor tokens to a user. Common strategies include sending an SMS, or using a mobile application such as Google Authenticator.

At Tinfoil Security, we opted to use the excellent rqrcode-rails3 gem to generate a QR-code representing the user's secret key, which can then be scanned by any mobile two-factor authentication client.

If you decide to do this you'll need to generate a URI to act as the source for the QR code. This can be done using the User#otp_provisioning_uri method.

issuer = 'Your App'
label = "#{issuer}:#{current_user.email}"

current_user.otp_provisioning_uri(label, issuer: issuer)

# > "otpauth://totp/Your%20App:[email protected]?secret=[otp_secret]&issuer=Your+App"

If you instead to decide to send the one-time password to the user directly, such as via SMS, you'll need a mechanism for generating the one-time password on the server:

current_user.current_otp

The generated code will be valid for the duration specified by otp_allowed_drift. This value can be modified by adding a config in config/initializers/devise.rb.

Devise.otp_allowed_drift = 240 # value in seconds
Devise.setup do |config|
...
end

However you decide to handle enrollment, there are a few important considerations to be made:

  • Whether you'll force the use of two-factor authentication, and if so, how you'll migrate existing users to system, and what your on-boarding experience will look like
  • If you authenticate using SMS, you'll want to verify the user's ownership of the phone, in much the same way you're probably verifying their email address
  • How you'll handle device revocation in the event that a user loses access to their device, or that device is rendered temporarily unavailable (This gem includes TwoFactorBackupable as an example extension meant to solve this problem)

It sounds like a lot of work, but most of these problems have been very elegantly solved by other people. We recommend taking a look at the excellent workflows used by Heroku and Google for inspiration.

Filtering sensitive parameters from the logs

To prevent two-factor authentication codes from leaking if your application logs get breached, you'll want to filter sensitive parameters from the Rails logs. Add the following to config/initializers/filter_parameter_logging.rb:

Rails.application.config.filter_parameters += [:otp_attempt]

Preventing Brute-Force Attacks

With any authentication solution it is also important to protect your users from brute-force attacks. For Devise-Two-Factor specifically if a user's username and password have already been compromised an attacker would be able to try possible TOTP codes and see if they can hit a lucky collision to log in. While Devise-Two-Factor is open-ended by design and cannot solve this for all applications natively there are some possible mitigations to consider. A non-exhaustive list follows:

  1. Use the lockable strategy from Devise to lock a user after a certain number of failed login attempts. See https://www.rubydoc.info/github/heartcombo/devise/main/Devise/Models/Lockable for more information.
  2. Configure a rate limit for your application, especially on the endpoints used to log in. One such library to accomplish this is rack-attack.
  3. When displaying authentication errors hide whether validating a username/password combination failed or a two-factor code failed behind a more generic error message.

Acknowledgements

Thank you to Christian Reitter (Radically Open Security) and Chris MacNaughton (Centauri Solutions) for reporting the issue.

Backup Codes

Devise-Two-Factor is designed with extensibility in mind. One such extension, TwoFactorBackupable, is included and serves as a good example of how to extend this gem. This plugin allows you to add the ability to generate single-use backup codes for a user, which they may use to bypass two-factor authentication, in the event that they lose access to their device.

To install it, you need to add the :two_factor_backupable directive to your model.

devise :two_factor_backupable

You'll also be required to enable the :two_factor_backupable strategy, by adding the following line to your Warden config in your Devise initializer, substituting :user for the name of your Devise scope.

manager.default_strategies(:scope => :user).unshift :two_factor_backupable

The final installation step is dependent on your version of Rails. If you're not running Rails 4, skip to the next section. Otherwise, create the following migration:

class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
  def change
    # Change type from :string to :text if using MySQL database
    add_column :users, :otp_backup_codes, :string, array: true
  end
end

You can then generate backup codes for a user:

codes = current_user.generate_otp_backup_codes!
current_user.save!
# Display codes to the user somehow!

The backup codes are stored in the database as bcrypt hashes, so be sure to display them to the user at this point. If all went well, the user should be able to login using each of the generated codes in place of their two-factor token. Each code is single-use, and generating a new set of backup codes for that user will invalidate all of the old ones.

You can customize the length of each code, and the number of codes generated by passing the options into :two_factor_backupable in the Devise directive:

devise :two_factor_backupable, otp_backup_code_length:     32,
                               otp_number_of_backup_codes: 10

Testing

Devise-Two-Factor includes shared-examples for both TwoFactorAuthenticatable and TwoFactorBackupable. Adding the following two lines to the specs for your two-factor enabled models will allow you to test your models for two-factor functionality:

require 'devise_two_factor/spec_helpers'

it_behaves_like "two_factor_authenticatable"
it_behaves_like "two_factor_backupable"

Troubleshooting

If you are using Rails 4.x and Ruby >= 2.7, you may get an error like

An error occurred while loading ./spec/devise/models/two_factor_authenticatable_spec.rb.
Failure/Error: require 'devise'

NoMethodError:
  undefined method `new' for BigDecimal:Class

see https://github.com/ruby/bigdecimal#which-version-should-you-select and ruby/bigdecimal#127 for more details, but you should be able to solve this by explicitly requiring an older version of bigdecimal in your gemfile like

gem "bigdecimal", "~> 1.4"

devise-two-factor's People

Contributors

alexsoble avatar atroutt avatar baseballlover723 avatar borski avatar bsedat avatar connorshea avatar dependabot[bot] avatar ebeeson avatar eoinkelly avatar ethagnawl avatar f3ndot avatar gogainda avatar inkstak avatar johnathanludwig avatar m-nakamura145 avatar mattyb avatar mediafinger avatar mikwat avatar neymarsabin avatar petergoldstein avatar pic avatar pnicholls avatar quinnwilton avatar r7kamura avatar rolftimmermans avatar sgomez17 avatar voran avatar willnet avatar ykzts avatar yskkin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

devise-two-factor's Issues

Rails 5/Devise 4 deprecations

These occur when we run our test suite with Rails 5.0.0.beta2, Devise 4.0.0.rc1, and a fork of devise-two-factor that allows Devise 4 in the gemspec:

DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from <top (required)> at /home/ubuntu/ImageHex/vendor/bundle/ruby/2.3.0/bundler/gems/devise-two-factor-0fa0a6914739/lib/devise_two_factor/models/two_factor_authenticatable.rb:1)
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from <top (required)> at /home/ubuntu/ImageHex/vendor/bundle/ruby/2.3.0/bundler/gems/devise-two-factor-0fa0a6914739/lib/devise_two_factor/models/two_factor_authenticatable.rb:1)
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from <top (required)> at /home/ubuntu/ImageHex/vendor/bundle/ruby/2.3.0/bundler/gems/devise-two-factor-0fa0a6914739/lib/devise_two_factor/models/two_factor_authenticatable.rb:1)
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from <top (required)> at /home/ubuntu/ImageHex/vendor/bundle/ruby/2.3.0/bundler/gems/devise-two-factor-0fa0a6914739/lib/devise_two_factor/models/two_factor_authenticatable.rb:1)

Command used is bundle exec rspec --color --require spec_helper --require rails_helper --format documentation spec on Circle CI. Can provide more information if needed.

Previously consumed OTP is considered valid after drift period has passed

RFC 6238 says that once an OTP is used, it cannot ever be used again.

The tests here are not properly testing the scenario where an OTP is consumed, then the user waits an amount between the drift period and twice the drift period, then tries entering the OTP again. The expected behavior is that the OTP should not be valid, but it is.

The problem with the tests is that they are freezing Time.current, but because current_otp_timestep depends on the current time, it must not be frozen. Instead Timecop.travel must be used to advance in time.

Here is a failing spec:

describe '#validate_and_consume_otp!' do
  let(:otp_secret) { '2z6hxkdwi3uvrnpn' }

  before :each do
    subject.otp_secret = otp_secret
  end

  after :each do
    Timecop.return
  end

  context 'with a stored consumed_timestep' do
    context 'given a previously correct OTP' do
      it 'fails to validate' do
        consumed_otp = ROTP::TOTP.new(otp_secret).at(Time.now)
        subject.validate_and_consume_otp!(consumed_otp)

        amount_to_travel = (subject.class.otp_allowed_drift).seconds
        Timecop.travel(amount_to_travel)

        expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
      end
    end
  end
end

Demo throws error

Fresh install of the demo app.

Clicking "Enable 2FA" here...
enable-2fa-page

...throws an error page:
error-2fa-page

README mentions generating an encryption key but doesn't mention where to put it or what to call it.

How to enable using backup codes

Should it be a separate page from the Authentication Code input page, or should the Authentication Code input page support backup codes? If the latter is what's intended, it isn't working for me. The README doesn't specify how we're supposed to do that.

TwoFactorBackupable not working?

I'm getting the following error when I try to generate the 2FA backup codes: undefined method 'generate_otp_backup_codes' for #<Class:0x007fe485bbbf90>

The code looks like this:

def enable_twofactor
    self.otp_secret = User.generate_otp_secret
    self.otp_backup_codes = User.generate_otp_backup_codes
    self.save
end

Generating the otp_secret works, but not the backup codes. I've added the db migration and run rake db:migrate, added the strategy to the devise initializer, followed the instructions and re-read them over and over.

Any ideas as to why this might be happening?

Backup codes + mysql

Just a note that in it's current form, using backup codes with the mysql db driver won't work, as mysql doesn't support the array data type. Rails will save the codes w/o issue, but the code fails when trying to read the field as it is a serialized YAML string. I had to use this in my User model to make it work:

def otp_backup_codes
codes = self.read_attribute(:otp_backup_codes)
codes = YAML::load(codes) unless codes.kind_of?(Array) || codes.nil?
codes
end

Additionally, Devise has moved their Devise.bcrypt definition into the database_authenticatable module, so I had to use a monkey-patch to define it myself.

ROTP 3.x support

Hello! Gem will be support ROTP 3.x ?

Reason for this issue: provisioning uri have new format in 3.x versions and clients which use Microsoft authenticator - don't see issuer.
Link: [https://github.com/mdp/rotp/blob/master/lib/rotp/totp.rb#L55-L68]

Symmetrically encrypt backup codes with user password & attr_encrypted

I've seen a lot of vendors use the pattern of re-displaying the users' backup codes many times (usually after user re-enters their password for verification purposes). And even showing which codes have been burned. Even Google has adopted this pattern.

Unless I'm mistaken, because TwoFactorBackupable uses Devise::Encryptor#digest the codes can only be displayed once and no more. While this is wonderfully secure, I feel like the hit to the user is such that it warrants re-evaluation.

I propose that attr_encrypted is leveraged to safely keep the backup codes encrypted at rest but can be, when combined with a supplied password, be displayed the user on subsequent requests in case they need to reprint or rewrite them down.

attr_encrypted supports methods as a key so associating it with a user's password is doable.

What do you think?

undefined method `bcrypt' for Devise:Module

Help!

I use this gem in my project. but I face the error when generate backup codes.

My devise is 3.1.0 and this gem is locking 1.0.0

I find the Device.bcrypt method is defined in module database_authenticate which is conflict with two_factor_authenticatable.

Since some reason, I cannot upgrade my devise.
_So how can I use two_factor_authenticatable and two_factor_backupable with devise 3.1.0?_

Disallow authentication code once verified

This is a suggestion for small improvement. RFC 6238 requires the implementation to disallow (MUST NOT) multiple submissions of the same authentication code. This is apparently to detect MITM and over the shoulder attacks.

When using lockable, failed_attempts is incremented by two after failed login attempt

When using devise-two-factor with devise lockable, there are somehow two calls being made to validate/valid_for_authentication? - the latter of which is doing the failed_attempts increment.

The first failed_attempts increment happens within this block, and the latter seems to be happening after the authenticate! method is finished executing.

if validate(resource) { !resource.otp_required_for_login ||
  resource.valid_otp?(params[scope]['otp_attempt']) }
  super
end

My devise initializer is configuring the warden strategy as follows:

config.warden do |manager|
  manager.default_strategies(:scope => :user).unshift :two_factor_authenticatable
end

My user model is set up with devise as follows:

devise :two_factor_authenticatable, :otp_secret_encryption_key => ENV['RSA_KEY_PW'], otp_secret_length: 64
devise :registerable, :recoverable, :invitable, :trackable, :timeoutable, :lockable

Change value of otp_allowed_drift

Hi! I'm trying to change the value of otp_allowed_drift, but have no success with that. Could you please help me here?

What I've tried already:

# config/initializers/devise.rb
Devise.setup do |config|
  config.otp_allowed_drift = 1800
end

and

# config/initializers/devise.rb
Devise.otp_allowed_drift = 1800
Devise.setup do |config|
  ...
end

With the second attempt the value of otp_allowed_drift had been changed, but my otp codes still changes like once in 30 seconds (default value), not once in 30 minutes as I expect to.

NoMethodError: undefined method `otp_backup_codes'

I've just installed this gem and this is the error I get on log in:

NoMethodError in Devise::SessionsController#create
undefined method `otp_backup_codes' for #<User:0xd55b920>

I added Backup Codes.
I also have not called any specific methods yet.

NoMethodError: undefined method `validate_and_consume_otp!'

class User < ActiveRecord::Base
  devise :rememberable, :trackable, :lockable,
         :session_limitable, :two_factor_authenticatable,
         :otp_secret_encryption_key => ENV['TWO_FACTOR_SECRET']
  # ...
end

and then

2.1.1 :001 >user = user.first
2.1.1 :002 > user.validate_and_consume_otp!('x','x')
NoMethodError: undefined method `validate_and_consume_otp!' for #<User:0x00000006402378>

Support NoSQL databases in generator

It would be awesome if, in the generator, you could specify an optional arg that let you generate for a NoSQL database (like Mongoid). So instead of creating a database migration, it would just add the appropriate fields to the model file.

Add a CHANGELOG

Can we get a CHANGELOG file for the gem? Would be nice to have for seeing what's changed between releases.

Allow attr_encrypted 2.x

attr_encrypted 2.0 introduced more secure default settings (aes-256-gcm) and a new mode per_attribute_iv

Authentication code error message

If user got a authentication code error, the error message is still 'Invalid email or password.', how to specify the message to some sentence like 'Authentication code is wrong'?

several tests in shared example group fail

uri's have parameters in a reverse order. spec should be more accepting of that.

  1) User behaves like two_factor_authenticatable #otp_provisioning_uri should return uri with issuer option
     Failure/Error: expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}&issuer=#{issuer}$})
       expected "otpauth://totp/[email protected]?issuer=Tinfoil&secret=ydvy642tugtmdw6tn4p3e7lkacmut6chqy6u4o3a6co55rhu4eip2ls2kd6cw2tcll6espnxtabak6xyjypn6b7zg5a7huplbq4vpgobyhi5gcnc5oyrpwxv6k73is4n" to match /otpauth:\/\/totp\/[email protected]\?secret=\w{128}&issuer=Tinfoil$/
       Diff:
       @@ -1,2 +1,2 @@
       -/otpauth:\/\/totp\/[email protected]\?secret=\w{128}&issuer=Tinfoil$/
       +"otpauth://totp/[email protected]?issuer=Tinfoil&secret=ydvy642tugtmdw6tn4p3e7lkacmut6chqy6u4o3a6co55rhu4eip2ls2kd6cw2tcll6espnxtabak6xyjypn6b7zg5a7huplbq4vpgobyhi5gcnc5oyrpwxv6k73is4n"

  2) User behaves like two_factor_authenticatable #validate_and_consume_otp! validates an OTP within the allowed drift
     Failure/Error: expect(subject.validate_and_consume_otp!(otp)).to be true
     NoMethodError:
       undefined method `default_otp_required_for_login' for nil:NilClass

Update to latest version of rotp

Hi,
I am packaging devise-two-factor for Debian, as part of GitLab packaging. Debian has rotp version 2.1.1 in the archives and it gives the following test failure with devise-two-factor. Can you please update the dependency of rotp to the latest version? It would be a great help. Thanks.

.F...................

Failures:

  1. Devise::Models::TwoFactorAuthenticatable When included in a class behaves like two_factor_authenticatable #otp_provisioning_uri should return uri with issuer option
    Failure/Error: subject.otp_provisioning_uri(account, issuer: issuer).should match(%r{otpauth://totp/#{account}?issuer=#{issuer}&secret=\w{#{otp_secret_length}}$})
    expected "otpauth://totp/[email protected]?secret=5q5cxn5m5xsg7qdkhi3ga6zfmfdngeah7muhnz2sx2bcpgfxp2aqm5cp4ieeakn5ja3xzlu35n5r7rtbum3zy2bsenlpynzeryw5ecktzwbsxt5ba74yy6og6umv5f55&issuer=Tinfoil" to match /otpauth://totp/[email protected]?issuer=Tinfoil&secret=\w{128}$/
    Diff:
    @@ -1,2 +1,2 @@
    -/otpauth://totp/[email protected]?issuer=Tinfoil&secret=\w{128}$/
    +"otpauth://totp/[email protected]?secret=5q5cxn5m5xsg7qdkhi3ga6zfmfdngeah7muhnz2sx2bcpgfxp2aqm5cp4ieeakn5ja3xzlu35n5r7rtbum3zy2bsenlpynzeryw5ecktzwbsxt5ba74yy6og6umv5f55&issuer=Tinfoil"

Shared Example Group: "two_factor_authenticatable" called from ./spec/devise/models/two_factor_authenticatable_spec.rb:14

./lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb:73:in `block (3 levels) in <top (required)>'

Demo not working

Getting undefined method 'for' for #<Devise::ParameterSanitizer:0x007fbfb5a59ed0>
in
app/controllers/application_controller.rb:11:in 'configure_permitted_parameters'

Login using backup codes

Hello,

While I've got the regular two-factor authentication flow using FreeOTP/Google Authenticator working in my app (which was a breeze, thanks to this awesome gem & documentation!), I'm not able to get the login flow using two-factor backup codes working.

I can see from the app logs that the backup code entered in the otp_attempt form field has been consumed... its removed from the otp_backup_codes model attribute through an SQL update. But unfortunately, the login doesn't succeed. The user is returned back to the login screen with a 401 Unauthorized response code from the app.

I couldn't find any resources through a google search that could've provided some insight on such a problem existing, so I'm assuming this has to be a problem with my app... I'd much appreciate it if someone can give me some pointers/directions on how I can debug this problem.

Thanks in advance!
Dhwanit

Should probably mention that it's using TOTP

I was curious and tried finding out what kind of OTP scheme you were using. Dug through the dependencies (rotp) and noticed it supported both HOTP & TOTP, which you use TOTP though.

Makes sense, and is much easier conceptually. I figure it should be mentioned in the README.

Can't generate Otp_secret (ArgumentError: must specify a :key)

Hello and first of all thanks for this awesome gem!

I want to add it to my admin system, but faced with some troubles. My :two_factor_authenticatable named AdminUser, so I followed all prescriptions you have in the readme: run generator, migration etc and now trying to make 2 factor actually work with.

u = AdminUser.first
u.otp_secret = AdminUser.generate_otp_secret

With this I have an error:

ArgumentError: must specify a :key
from /home/vagrant/.rvm/gems/ruby-2.2.2/gems/encryptor-1.3.0/lib/encryptor.rb:51:in `crypt'

I'm on Devise (3.5.2) and devise-two-factor (1.1.0)

assets:precompile failing when there is no DB connection

When devise-two-factor is enabled, rake assets:precompile tries to connect to the DB. The problem is related to devise that causes the model to be loaded and attr_encrypted that require DB connection on class load.

rake aborted!
ActiveRecord::NoDatabaseError: FATAL:  database "example_test" does not exist
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:661:in `rescue in connect'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:651:in `connect'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:242:in `initialize'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:44:in `new'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:44:in `postgresql_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:438:in `new_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:448:in `checkout_new_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:422:in `acquire_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:349:in `block in checkout'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:348:in `checkout'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:263:in `block in connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:262:in `connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:567:in `retrieve_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_handling.rb:113:in `retrieve_connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/connection_handling.rb:87:in `connection'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.2/lib/active_record/model_schema.rb:230:in `table_exists?'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/attr_encrypted-1.3.4/lib/attr_encrypted/adapters/active_record.rb:59:in `attribute_instance_methods_as_symbols'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/attr_encrypted-1.3.4/lib/attr_encrypted.rb:130:in `block in attr_encrypted'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/attr_encrypted-1.3.4/lib/attr_encrypted.rb:125:in `each'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/attr_encrypted-1.3.4/lib/attr_encrypted.rb:125:in `attr_encrypted'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/attr_encrypted-1.3.4/lib/attr_encrypted/adapters/active_record.rb:51:in `attr_encrypted'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-two-factor-2.0.0/lib/devise_two_factor/models/two_factor_authenticatable.rb:11:in `block in <module:TwoFactorAuthenticatable>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/concern.rb:120:in `class_eval'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/concern.rb:120:in `append_features'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:103:in `include'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:103:in `block (2 levels) in devise'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:87:in `each'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:87:in `block in devise'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:114:in `devise_modules_hook!'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/models.rb:84:in `devise'
/Users/gawaine/dev/example_app/app/models/admin_user.rb:10:in `<class:AdminUser>'
/Users/gawaine/dev/example_app/app/models/admin_user.rb:9:in `<top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `block in require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:240:in `load_dependency'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:360:in `require_or_load'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:494:in `load_missing_constant'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:184:in `const_missing'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/inflector/methods.rb:261:in `const_get'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/inflector/methods.rb:261:in `block in constantize'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/inflector/methods.rb:259:in `each'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/inflector/methods.rb:259:in `inject'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/inflector/methods.rb:259:in `constantize'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:566:in `get'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:597:in `constantize'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise.rb:287:in `get'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/mapping.rb:81:in `to'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/mapping.rb:76:in `modules'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/mapping.rb:93:in `routes'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/mapping.rb:160:in `default_used_route'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/mapping.rb:70:in `initialize'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise.rb:321:in `new'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise.rb:321:in `add_mapping'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/rails/routes.rb:224:in `block in devise_for'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/rails/routes.rb:223:in `each'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/devise-3.5.2/lib/devise/rails/routes.rb:223:in `devise_for'
/Users/gawaine/dev/example_app/config/routes.rb:23:in `block (3 levels) in <top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:940:in `block in constraints'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:816:in `scope'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:940:in `constraints'
/Users/gawaine/dev/example_app/config/routes.rb:22:in `block (2 levels) in <top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:940:in `block in constraints'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:816:in `scope'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/mapper.rb:940:in `constraints'
/Users/gawaine/dev/example_app/config/routes.rb:21:in `block in <top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/route_set.rb:432:in `instance_exec'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/route_set.rb:432:in `eval_block'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/actionpack-4.2.2/lib/action_dispatch/routing/route_set.rb:410:in `draw'
/Users/gawaine/dev/example_app/config/routes.rb:5:in `<top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `load'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `block in load'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:240:in `load_dependency'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `load'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:40:in `block in load_paths'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:40:in `each'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:40:in `load_paths'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:16:in `reload!'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:26:in `block in updater'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/file_update_checker.rb:75:in `call'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/file_update_checker.rb:75:in `execute'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:27:in `updater'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/routes_reloader.rb:7:in `execute_if_updated'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application/finisher.rb:69:in `block in <module:Finisher>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/initializable.rb:30:in `instance_exec'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/initializable.rb:30:in `run'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/initializable.rb:55:in `block in run_initializers'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/initializable.rb:54:in `run_initializers'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application.rb:352:in `initialize!'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/railtie.rb:194:in `public_send'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/railtie.rb:194:in `method_missing'
/Users/gawaine/dev/example_app/config/environment.rb:5:in `<top (required)>'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `block in require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:240:in `load_dependency'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application.rb:328:in `require_environment!'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/railties-4.2.2/lib/rails/application.rb:457:in `block in run_tasks_blocks'
/Users/gawaine/.rvm/gems/ruby-2.2.2/gems/sprockets-rails-2.3.1/lib/sprockets/rails/task.rb:64:in `block (2 levels) in define'
/Users/gawaine/.rvm/gems/ruby-2.2.2/bin/ruby_executable_hooks:15:in `eval'
/Users/gawaine/.rvm/gems/ruby-2.2.2/bin/ruby_executable_hooks:15:in `<main>'
Tasks: TOP => environment
(See full trace by running task with --trace)

How to check user is two-factor-authenticated or not ?

I need to to authenticate a user is two-step authenticated or not.
As in simple devise we had a method like
authenticate_user!

I need to set this in before_action filter.
Can we have similar method for two-factor-authentication?

NoMethodError: undefined method `validate_and_consume_otp!'

I'm trying to setup this but I think it isnt being initializated.

I followed everything in this tutorial, what am I missing?
https://github.com/mikamai/rails-2fact-auth-example

I have:

  • Bundled gem 'devise-two-factor' and gem 'rqrcode_png'.
  • Updated the database User structure with the 5 new fields.
  • Defined the ENV['VARIABLE']
  • Updated User model accordingly
class User < ActiveRecord::Base
  devise :rememberable, :trackable, :lockable,
         :session_limitable, :two_factor_authenticatable,
         :otp_secret_encryption_key => ENV['VARIABLE']
  # ...
end
  • Created the views
  • Updated devise views
  • Updated routes
  • Whitelisted application params
def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_in) << :otp_attempt
end
  • Updated again User.rb model

    def activate_two_factor params
      otp_params = { otp_secret: unconfirmed_otp_secret }
      if !valid_password?(params[:password])
        errors.add :password, :invalid
        false
      elsif !validate_and_consume_otp!(params[:otp_attempt], otp_params)
        errors.add :otp_attempt, :invalid
        false
      else
        activate_two_factor!
      end
    end
    
    def deactivate_two_factor params
      if !valid_password?(params[:password])
        errors.add :password, :invalid
        false
      else
        self.otp_required_for_login = false
        self.otp_secret = nil
        save
      end
    end
    
    private
    
    def activate_two_factor!
      self.otp_required_for_login = true
      self.otp_secret = current_user.unconfirmed_otp_secret
      self.unconfirmed_otp_secret = nil
      save
    end
  • Created the two_factors_controller.rb

class TwoFactorsController < ApplicationController
  before_filter :authenticate_user!

  def new
  end

  # If user has already enabled the two-factor auth, we generate a
  #   temp. otp_secret and show the 'new' template.
  # Otherwise we show the 'show' template which will allow the user to disable
  #   the two-factor auth
  def show
    unless current_user.otp_required_for_login?
      current_user.unconfirmed_otp_secret = User.generate_otp_secret
      current_user.save!
      @qr = RQRCode::QRCode.new(two_factor_otp_url).to_img.resize(240, 240).to_data_url
      render 'new'
    end
  end

  # User#activate_two_factor will return a boolean. When false is returned
  #   we presume the model has some errors.
  def create
    permitted_params = params.require(:user).permit :password, :otp_attempt
    if current_user.activate_two_factor permitted_params
      redirect_to root_path, notice: "You have enabled Two Factor Auth"
    else
      render 'new'
    end
  end

  # If the provided password is correct, two-factor is disabled
  def destroy
    permitted_params = params.require(:user).permit :password
    if current_user.deactivate_two_factor permitted_params
      redirect_to root_path, notice: "You have disabled Two Factor Auth"
    else
      render 'show'
    end
  end

  private

  # The url needed to generate the QRCode so it can be acquired by
  #   Google Authenticator
  def two_factor_otp_url
    "otpauth://totp/%{app_id}?secret=%{secret}&issuer=%{app}" % {
      :secret => current_user.unconfirmed_otp_secret,
      :app    => "",
      :app_id => "#{current_user.username}"
    }
  end
end

Everything seems to be working, but when I go to /two_factor and click the button I get error from User model.

How should I integrate Devise two factor authentication with custom sessions controller?

I integrated the Houdini two factor authentication with existing rails application.
First I doesn't have any custom sessions_controller then the two_factor_authentication is working fine.
But whereas I written custom create action in Sessions Controller then it doesn't authenticated by two_factor_authentication.

Here is the custom code for create action of Sessions Controller.

  if status_response.nil?
    render :file => 'public/api_not_found.html', :status => :not_found, :layout => false
  else
    if status_response['code'].to_i == 1
      signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
      flash[:alert] = 'Invalid Email ID or password.'
      yield if block_given?
      respond_to_on_destroy
    else
      self.resource = warden.authenticate!(auth_options)
      set_flash_message(:notice, :signed_in) if is_flashing_format?
      sign_in(resource_name, resource)
      yield resource if block_given?
      respond_with resource, location: after_sign_in_path_for(resource)
    end
  end

Model code of user.rb:

  devise :two_factor_authenticatable, :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable,
     :confirmable, password_length: 8..30
  has_one_time_password


  def send_two_factor_authentication_code
    puts ">>>>>>>>>>>>>>> otp_secret_key: #{otp_secret_key}, otp_code: #{otp_code}"
  end

I know if we are customizing the create action of Sessions Controller then we should call the two_factor_authentication. I tried to invoke this in controller but it throws an error.

So my question how should i integrate the two_factor_authentication with custom create action of Session Controller?

OTP attempt param blocked by Devise

Ran into this issue while implementing my app's login flow.

Devise rejects legitimate log-in attempts because otp_attempt is not whitelisted:

Processing by Devise::SessionsController#new as HTML
  Parameters: {"utf8"=>"โœ“", "authenticity_token"=>"he0idQUk10utMP56rfV/NKDujXoQywecAUpurtX3so4=", "user"=>{"email"=>"[email protected]", "password"=>"[FILTERED]", "otp_attempt"=>"1234"}, "commit"=>"Log in"}
Unpermitted parameters: otp_attempt

Solved by adding a little code to application_controller.rb (h/t Stack Overflow):

  before_filter :update_sanitized_params, if: :devise_controller?

  def update_sanitized_params
    devise_parameter_sanitizer.for(:sign_in) do |u| 
      u.permit(:otp_attempt, :email, :password)
    end
  end

Can this gem update the default whitelisted attributes to allow otp_attempt? Thoughts on the best ways to implement?

In the meantime, can add this problem/solution into the README.

3 errors that looks like an issue with attr_encrypted version

I get these 3 test failures with devise 3.5 and 4.0. attr_encrypted is 1.3.4.

Failures:

  1) Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the mode
     Failure/Error: expect(subject.encrypted_attributes[:otp_secret][:algorithm]).to eq('aes-256-cbc')

     NoMethodError:
       undefined method `encrypted_attributes' for #<TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble:0x00000003ab8760>
       Did you mean?  encrypted_otp_secret_salt=
     # ./spec/devise/models/two_factor_authenticatable_spec.rb:75:in `block (4 levels) in <top (required)>'

  2) Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the key
     Failure/Error: expect(subject.encrypted_attributes[:otp_secret][:key]).to eq('test-key'*8)

     NoMethodError:
       undefined method `encrypted_attributes' for #<TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble:0x00000004122178>
       Did you mean?  encrypted_otp_secret_salt=
     # ./spec/devise/models/two_factor_authenticatable_spec.rb:67:in `block (4 levels) in <top (required)>'

  3) Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the mode
     Failure/Error: expect(subject.encrypted_attributes[:otp_secret][:mode]).to eq(:per_attribute_iv_and_salt)

     NoMethodError:
       undefined method `encrypted_attributes' for #<TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble:0x00000004120148>
       Did you mean?  encrypted_otp_secret_salt=
     # ./spec/devise/models/two_factor_authenticatable_spec.rb:71:in `block (4 levels) in <top (required)>'

Finished in 19.4 seconds (files took 12.06 seconds to load)
41 examples, 3 failures

Failed examples:

rspec ./spec/devise/models/two_factor_authenticatable_spec.rb:74 # Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the mode
rspec ./spec/devise/models/two_factor_authenticatable_spec.rb:66 # Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the key
rspec ./spec/devise/models/two_factor_authenticatable_spec.rb:70 # Devise::Models::TwoFactorAuthenticatable When included in a class otp_secret options should be of the mode

Randomized with seed 39654

/usr/bin/ruby2.3 /usr/bin/rspec --pattern ./spec/\*\*/\*_spec.rb --format documentation failed
ERROR: Test "ruby2.3" failed. Exiting.

@connorshea do I need to update attr_encrypted?

Upgrade guide for devise-two-factor with attr_encrypted 3.x doesn't work

Assuming the user has followed the README.md instructions for past versions of devise-two-factor, they should have something like this in their User model:

devise :two_factor_authenticatable,
  :otp_secret_encryption_key => ENV['YOUR_ENCRYPTION_KEY_HERE']

The suggestion in the UPGRADING.md is to replace the above with the following:

attr_encrypted :otp_secret,
  :key       => self.otp_secret_encryption_key,
  :mode      => :per_attribute_iv_and_salt,
  :algorithm => 'aes-256-cbc'

devise :two_factor_authenticatable,
  :otp_secret_encryption_key => ENV['DEVISE_TWO_FACTOR_ENCRYPTION_KEY']

The problem with this is that otp_secret_encryption_key isn't an attribute of the model, so it throws an error.

I had to change the code to the following for it to work with devise-two-factor (current devise-4 branch), Devise 4, and attr_encrypted/encryptor 3.0:

  attr_encrypted :otp_secret,
    :key       => ENV['DEVISE_TWO_FACTOR_ENCRYPTION_KEY'],
    :mode      => :per_attribute_iv_and_salt,
    :algorithm => 'aes-256-cbc'

  devise :two_factor_authenticatable,
         :otp_secret_encryption_key => ENV['DEVISE_TWO_FACTOR_ENCRYPTION_KEY']

The error was initially:

NoMethodError - undefined method `otp_secret_encryption_key' for #<Class:0x007fd612c5d880>:

The error I got after changing self.otp_secret_encryption_key to :otp_secret_encryption_key was:

NoMethodError - undefined method `bytesize' for :otp_secret_encryption_key:Symbol:

After adding :insecure_mode => true it became:

TypeError - no implicit conversion of Symbol into String:

Only when I used the environment variable directly did it work. I should also note that we don't actually use an environment variable, but instead use File.read(Rails.root.join('.secret')).chomp. I don't think that changes anything, but I thought it was worth mentioning just in case.

Two factor as second login step?

In most cases where I've used Two-Factor Auth the auth code is a second login step.

Are they any good examples anywhere on how to make this a two step process?

Doesn't work with Devise >= 3.5

Is it actually incompatible or just the dependencies are set that way?

Bundler could not find compatible versions for gem "devise":
  In snapshot (Gemfile.lock):
    devise (= 3.5.1)

  In Gemfile:
    devise-two-factor (~> 1.0.2) ruby depends on
      devise (< 3.5, >= 3.2.4) ruby

Release 3.0.0 on RubyGems

Not to be a bother, but I'd like to upgrade GitLab to Devise 4, and this is the only thing blocking that upgrade.

Is there anything left to do or is it ready for release?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.