diff --git a/.travis.yml b/.travis.yml index cafa2230..c3fc35f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,28 @@ language: ruby env: - - "RAILS_VERSION=4.0" - - "RAILS_VERSION=4.1" - "RAILS_VERSION=4.2" + - "RAILS_VERSION=5.2" - "RAILS_VERSION=master" rvm: - - 2.1 - - 2.2 - - 2.3.1 + - 2.3.8 + - 2.4.5 + - 2.5.3 matrix: + fast_finish: true allow_failures: - env: "RAILS_VERSION=master" - exclude: - - rvm: 2.1 - env: RAILS_VERSION=master + include: - rvm: 2.2 - env: RAILS_VERSION=master + env: RAILS_VERSION=4.2 before_install: - - gem update bundler + - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true + - gem install bundler -v '< 2' before_script: - - bundle exec rake app:db:migrate + - bundle exec rake app:db:setup script: bundle exec rake spec diff --git a/Gemfile b/Gemfile index 70ab496c..810ce296 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ rails = case rails_version when "master" {github: "rails/rails"} when "default" - "~> 4.1" + "~> 5.2" else "~> #{rails_version}" end @@ -27,5 +27,4 @@ end group :test do gem 'rack_session_access' gem 'ammeter' - gem 'pry' end diff --git a/README.md b/README.md index dabdea41..f4f91d1a 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ [](https://gitter.im/Houdini/two_factor_authentication?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/Houdini/two_factor_authentication) -[](https://codeclimate.com/github/Houdini/two_factor_authentication) +[](https://codeclimate.com/github/Houdini/two_factor_authentication) ## Features * Support for 2 types of OTP codes - 1. Codes delivered directly to the user - 2. TOTP (Google Authenticator) codes based on a shared secret (HMAC) + 1. Codes delivered directly to the user + 2. TOTP (Google Authenticator) codes based on a shared secret (HMAC) * Configurable OTP code digit length * Configurable max login attempts * Customizable logic to determine if a user needs two factor authentication @@ -54,7 +54,7 @@ migration in `db/migrate/`, which will add the following columns to your table: #### Manual initial setup If you prefer to set up the model and migration manually, add the -`:two_factor_authentication` option to your existing devise options, such as: +`:two_factor_authenticatable` option to your existing devise options, such as: ```ruby devise :database_authenticatable, :registerable, :recoverable, :rememberable, @@ -97,6 +97,7 @@ config.direct_otp_length = 6 # Direct OTP code length config.remember_otp_session_for_seconds = 30.days # Time before browser has to perform 2fA again. Default is 0. config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY'] config.second_factor_resource_id = 'id' # Field or method name used to set value for 2fA remember cookie +config.delete_cookie_on_logout = false # Delete cookie when user signs out, to force 2fA again on login ``` The `otp_secret_encryption_key` must be a random key that is not stored in the DB, and is not checked in to your repo. It is recommended to store it in an @@ -161,25 +162,32 @@ Below is an example using ERB: <% end %> <%= link_to "Sign out", destroy_user_session_path, :method => :delete %> - ``` -#### Enable TOTP support for existing users +#### Upgrading from version 1.X to 2.X + +The following database fields are new in version 2. + +- `direct_otp` +- `direct_otp_sent_at` +- `totp_timestamp` -If you have existing users that need to be provided with a OTP secret key, so -they can use TOTP, create a rake task. It could look like this one below: +To add them, generate a migration such as: + + $ rails g migration AddTwoFactorFieldsToUsers direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp + +The `otp_secret_key` is only required for users who use TOTP (Google Authenticator) codes, +so unless it has been shared with the user it should be set to `nil`. The +following pseudo-code is an example of how this might be done: ```ruby -desc 'rake task to update users with otp secret key' -task :update_users_with_otp_secret_key => :environment do - User.find_each do |user| - user.generate_totp_secret +User.find_each do |user| do + if !uses_authenticator_app(user) + user.otp_secret_key = nil user.save! - puts "Rake[:update_users_with_otp_secret_key] => OTP secret key set to '#{key}' for User '#{user.email}'" end end ``` -Then run the task with `bundle exec rake update_users_with_otp_secret_key` #### Adding the TOTP encryption option to an existing app @@ -217,25 +225,25 @@ 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: ``` @@ -255,6 +263,142 @@ 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.save! (if in ruby code instead of console) + 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 6d2b4873..0e7aed84 100644 --- a/app/controllers/devise/two_factor_authentication_controller.rb +++ b/app/controllers/devise/two_factor_authentication_controller.rb @@ -1,3 +1,5 @@ +require 'devise/version' + class Devise::TwoFactorAuthenticationController < DeviseController prepend_before_action :authenticate_scope! before_action :prepare_and_validate, :handle_two_factor_authentication @@ -26,7 +28,13 @@ def after_two_factor_success_for(resource) set_remember_two_factor_cookie(resource) warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false - bypass_sign_in(resource, scope: resource_name) + # For compatability with devise versions below v4.2.0 + # https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb + if respond_to?(:bypass_sign_in) + bypass_sign_in(resource, scope: resource_name) + else + sign_in(resource_name, resource, bypass: true) + end set_flash_message :notice, :success resource.update_attribute(:second_factor_attempts_count, 0) @@ -39,7 +47,7 @@ def set_remember_two_factor_cookie(resource) if expires_seconds && expires_seconds > 0 cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = { value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}", - expires: expires_seconds.from_now + expires: expires_seconds.seconds.from_now } end end diff --git a/app/views/devise/two_factor_authentication/show.html.erb b/app/views/devise/two_factor_authentication/show.html.erb index 552fd7d8..9a3ae060 100644 --- a/app/views/devise/two_factor_authentication/show.html.erb +++ b/app/views/devise/two_factor_authentication/show.html.erb @@ -7,13 +7,13 @@
<%= flash[:notice] %>
<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %> - <%= text_field_tag :code %> + <%= text_field_tag :code, '', autofocus: true %> <%= submit_tag "Submit" %> <% end %> <% if resource.direct_otp %> -<%= link_to "Resend Code", resend_code_user_two_factor_authentication_path, action: :get %> + <%= link_to "Resend Code", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %> <% else %> -<%= link_to "Send me a code instead", resend_code_user_two_factor_authentication_path, action: :get %> +<%= link_to "Send me a code instead", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %> <% end %> -<%= link_to "Sign out", destroy_user_session_path, :method => :delete %> +<%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %> diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 00000000..253e6058 --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,8 @@ +de: + devise: + two_factor_authentication: + success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich." + attempt_failed: "Authentifizierungsversuch fehlgeschlagen." + max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben." + contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren." + code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt." diff --git a/lib/generators/active_record/templates/migration.rb b/lib/generators/active_record/templates/migration.rb index 251ef402..f14a733d 100644 --- a/lib/generators/active_record/templates/migration.rb +++ b/lib/generators/active_record/templates/migration.rb @@ -1,4 +1,6 @@ class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration + disable_ddl_transaction! + def change add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0 add_column :<%= table_name %>, :encrypted_otp_secret_key, :string @@ -8,6 +10,6 @@ def change add_column :<%= table_name %>, :direct_otp_sent_at, :datetime add_column :<%= table_name %>, :totp_timestamp, :timestamp - add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true + add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true, algorithm: :concurrently end end diff --git a/lib/two_factor_authentication.rb b/lib/two_factor_authentication.rb index 59ffa039..7b1bbbc1 100644 --- a/lib/two_factor_authentication.rb +++ b/lib/two_factor_authentication.rb @@ -2,7 +2,6 @@ require 'devise' require 'active_support/concern' require "active_model" -require "active_record" require "active_support/core_ext/class/attribute_accessors" require "cgi" @@ -30,6 +29,9 @@ module Devise mattr_accessor :second_factor_resource_id @@second_factor_resource_id = 'id' + + mattr_accessor :delete_cookie_on_logout + @@delete_cookie_on_logout = false end module TwoFactorAuthentication @@ -44,7 +46,7 @@ module Controllers Devise.add_module :two_factor_authenticatable, :model => 'two_factor_authentication/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication -require 'two_factor_authentication/orm/active_record' +require 'two_factor_authentication/orm/active_record' if defined?(ActiveRecord::Base) require 'two_factor_authentication/routes' require 'two_factor_authentication/models/two_factor_authenticatable' require 'two_factor_authentication/rails' diff --git a/lib/two_factor_authentication/controllers/helpers.rb b/lib/two_factor_authentication/controllers/helpers.rb index f8a084d4..64e8377c 100644 --- a/lib/two_factor_authentication/controllers/helpers.rb +++ b/lib/two_factor_authentication/controllers/helpers.rb @@ -20,11 +20,16 @@ def handle_two_factor_authentication end def handle_failed_second_factor(scope) - if request.format.present? and request.format.html? - session["#{scope}_return_to"] = request.original_fullpath if request.get? - redirect_to two_factor_authentication_path_for(scope) + if request.format.present? + if request.format.html? + session["#{scope}_return_to"] = request.original_fullpath if request.get? + redirect_to two_factor_authentication_path_for(scope) + elsif request.format.json? + session["#{scope}_return_to"] = root_path(format: :html) + render json: { redirect_to: two_factor_authentication_path_for(scope) }, status: :unauthorized + end else - render nothing: true, status: :unauthorized + head :unauthorized end end diff --git a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb index 159ae144..3ff03415 100644 --- a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +++ b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb @@ -7,7 +7,11 @@ if user.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request) - user.send_new_otp unless user.totp_enabled? + user.send_new_otp if user.send_new_otp_after_login? end end end + +Warden::Manager.before_logout do |user, auth, _options| + auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout +end diff --git a/lib/two_factor_authentication/models/two_factor_authenticatable.rb b/lib/two_factor_authentication/models/two_factor_authenticatable.rb index 7d4a3306..6d73a0fb 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) + :direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout + ) end module InstanceMethodsOnActivation @@ -38,7 +39,10 @@ def authenticate_totp(code, options = {}) drift = options[:drift] || self.class.allowed_otp_drift_seconds 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_with_drift_and_prior(code, drift, totp_timestamp) + new_timestamp = totp.verify( + without_spaces(code), + drift_ahead: drift, drift_behind: drift, after: totp_timestamp + ) return false unless new_timestamp self.totp_timestamp = new_timestamp true @@ -61,6 +65,10 @@ def send_new_otp(options = {}) send_two_factor_authentication_code(direct_otp) end + def send_new_otp_after_login? + !totp_enabled? + end + def send_two_factor_authentication_code(code) raise NotImplementedError.new("No default implementation - please define in your class.") end @@ -84,7 +92,10 @@ def confirm_totp_secret(secret, code, options = {}) end def generate_totp_secret - ROTP::Base32.random_base32 + # ROTP gem since version 5 to version 5.1 + # at version 5.1 ROTP gem reinstates. + # Details: https://github.com/mdp/rotp/blob/master/CHANGELOG.md#510 + ROTP::Base32.try(:random) || ROTP::Base32.random_base32 end def create_direct_otp(options = {}) @@ -98,6 +109,10 @@ def create_direct_otp(options = {}) private + def without_spaces(code) + code.gsub(/\s/, '') + end + def random_base10(digits) SecureRandom.random_number(10**digits).to_s.rjust(digits, '0') end @@ -113,16 +128,16 @@ def clear_direct_otp module EncryptionInstanceMethods def otp_secret_key - decrypt(encrypted_otp_secret_key) + otp_decrypt(encrypted_otp_secret_key) end def otp_secret_key=(value) - self.encrypted_otp_secret_key = encrypt(value) + self.encrypted_otp_secret_key = otp_encrypt(value) end private - def decrypt(encrypted_value) + def otp_decrypt(encrypted_value) return encrypted_value if encrypted_value.blank? encrypted_value = encrypted_value.unpack('m').first @@ -137,7 +152,7 @@ def decrypt(encrypted_value) value end - def encrypt(value) + def otp_encrypt(value) return value if value.blank? value = value.to_s @@ -153,7 +168,8 @@ def encryption_options_for(value) value: value, key: Devise.otp_secret_encryption_key, iv: iv_for_attribute, - salt: salt_for_attribute + salt: salt_for_attribute, + algorithm: 'aes-256-cbc' } end @@ -161,7 +177,7 @@ def iv_for_attribute(algorithm = 'aes-256-cbc') iv = encrypted_otp_secret_key_iv if iv.nil? - algo = OpenSSL::Cipher::Cipher.new(algorithm) + algo = OpenSSL::Cipher.new(algorithm) iv = [algo.random_iv].pack('m') self.encrypted_otp_secret_key_iv = iv end diff --git a/lib/two_factor_authentication/orm/active_record.rb b/lib/two_factor_authentication/orm/active_record.rb index 616862ee..8053ee30 100644 --- a/lib/two_factor_authentication/orm/active_record.rb +++ b/lib/two_factor_authentication/orm/active_record.rb @@ -1,3 +1,5 @@ +require "active_record" + module TwoFactorAuthentication module Orm module ActiveRecord diff --git a/lib/two_factor_authentication/version.rb b/lib/two_factor_authentication/version.rb index f75a7a6f..239fae10 100644 --- a/lib/two_factor_authentication/version.rb +++ b/lib/two_factor_authentication/version.rb @@ -1,3 +1,3 @@ module TwoFactorAuthentication - VERSION = "1.1.5".freeze + VERSION = "2.2.0".freeze end diff --git a/spec/controllers/two_factor_authentication_controller_spec.rb b/spec/controllers/two_factor_authentication_controller_spec.rb index 100876ad..d578d0bb 100644 --- a/spec/controllers/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/two_factor_authentication_controller_spec.rb @@ -2,6 +2,14 @@ describe Devise::TwoFactorAuthenticationController, type: :controller do describe 'is_fully_authenticated? helper' do + def post_code(code) + if Rails::VERSION::MAJOR >= 5 + post :update, params: { code: code } + else + post :update, code: code + end + end + before do sign_in end @@ -9,7 +17,7 @@ context 'after user enters valid OTP code' do it 'returns true' do controller.current_user.send_new_otp - post :update, code: controller.current_user.direct_otp + post_code controller.current_user.direct_otp expect(subject.is_fully_authenticated?).to eq true end end @@ -24,7 +32,7 @@ context 'when user enters an invalid OTP' do it 'returns false' do - post :update, code: '12345' + post_code '12345' expect(subject.is_fully_authenticated?).to eq false end diff --git a/spec/features/two_factor_authenticatable_spec.rb b/spec/features/two_factor_authenticatable_spec.rb index ebde2d80..d433d636 100644 --- a/spec/features/two_factor_authenticatable_spec.rb +++ b/spec/features/two_factor_authenticatable_spec.rb @@ -174,6 +174,18 @@ def sms_sign_in visit dashboard_path expect(page).to have_content("Enter the code that was sent to you") end + + scenario 'Delete cookie when user logs out if enabled' do + user.class.delete_cookie_on_logout = true + + login_as user + logout + + login_as user + + visit dashboard_path + expect(page).to have_content("Enter the code that was sent to you") + end end it 'sets the warden session need_two_factor_authentication key to true' do diff --git a/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb b/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb index 272d0cfd..6fb4f505 100644 --- a/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +++ b/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb @@ -86,6 +86,11 @@ def do_invoke(code, user) expect(do_invoke(code, instance)).to eq(true) end + it 'authenticates a code entered with a space' do + code = @totp_helper.totp_code.insert(3, ' ') + expect(do_invoke(code, instance)).to eq(true) + end + it 'does not authenticate an old code' do code = @totp_helper.totp_code(1.minutes.ago.to_i) expect(do_invoke(code, instance)).to eq(false) @@ -133,12 +138,12 @@ def instance.send_two_factor_authentication_code(code) it "returns uri with user's email" do expect(instance.provisioning_uri). - to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}}) + to match(%r{otpauth://totp/houdini@example.com\?secret=\w{32}}) end it 'returns uri with issuer option' do expect(instance.provisioning_uri('houdini')). - to match(%r{otpauth://totp/houdini\?secret=\w{16}$}) + to match(%r{otpauth://totp/houdini\?secret=\w{32}$}) end it 'returns uri with issuer option' do @@ -150,7 +155,7 @@ def instance.send_two_factor_authentication_code(code) expect(uri.host).to eq('totp') expect(uri.path).to eq('/Magic:houdini') expect(params['issuer'].shift).to eq('Magic') - expect(params['secret'].shift).to match(/\w{16}/) + expect(params['secret'].shift).to match(/\w{32}/) end end end @@ -163,10 +168,10 @@ def instance.send_two_factor_authentication_code(code) shared_examples 'generate_totp_secret' do |klass| let(:instance) { klass.new } - it 'returns a 16 character string' do + it 'returns a 32 character string' do secret = instance.generate_totp_secret - expect(secret).to match(/\w{16}/) + expect(secret).to match(/\w{32}/) end end @@ -280,16 +285,20 @@ def instance.send_two_factor_authentication_code(code) to raise_error ArgumentError end - it 'passes in the correct options to Encryptor' do + it 'passes in the correct options to Encryptor. + We test here output of + Devise::Models::TwoFactorAuthenticatable::EncryptionInstanceMethods.encryption_options_for' do instance.otp_secret_key = 'testing' iv = instance.encrypted_otp_secret_key_iv salt = instance.encrypted_otp_secret_key_salt + # it's important here to put the same crypto algorithm from that method encrypted = Encryptor.encrypt( value: 'testing', key: Devise.otp_secret_encryption_key, iv: iv.unpack('m').first, - salt: salt.unpack('m').first + salt: salt.unpack('m').first, + algorithm: 'aes-256-cbc' ) expect(instance.encrypted_otp_secret_key).to eq [encrypted].pack('m') diff --git a/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb b/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb index 74adf30c..83d51c94 100644 --- a/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +++ b/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb @@ -1,4 +1,4 @@ -class DeviseCreateUsers < ActiveRecord::Migration +class DeviseCreateUsers < ActiveRecord::Migration[4.2] def change create_table(:users) do |t| ## Database authenticatable diff --git a/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb b/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb index 5720bb88..e3c2bf61 100644 --- a/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb +++ b/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb @@ -1,4 +1,4 @@ -class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration +class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration[4.2] def up change_table :users do |t| t.string :otp_secret_key diff --git a/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb b/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb index ee3fa8f6..87f8217f 100644 --- a/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +++ b/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb @@ -1,4 +1,4 @@ -class AddNickanmeToUsers < ActiveRecord::Migration +class AddNickanmeToUsers < ActiveRecord::Migration[4.2] def change change_table :users do |t| t.column :nickname, :string, limit: 64 diff --git a/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb b/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb index 67f80a6b..ba6b5281 100644 --- a/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +++ b/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb @@ -1,4 +1,4 @@ -class AddEncryptedColumnsToUser < ActiveRecord::Migration +class AddEncryptedColumnsToUser < ActiveRecord::Migration[4.2] def change add_column :users, :encrypted_otp_secret_key, :string add_column :users, :encrypted_otp_secret_key_iv, :string diff --git a/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb b/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb index a51222fe..ab34526d 100644 --- a/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +++ b/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb @@ -1,4 +1,4 @@ -class PopulateOtpColumn < ActiveRecord::Migration +class PopulateOtpColumn < ActiveRecord::Migration[4.2] def up User.reset_column_information diff --git a/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb b/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb index 9ddd608e..40ebc073 100644 --- a/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +++ b/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb @@ -1,4 +1,4 @@ -class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration +class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration[4.2] def change remove_column :users, :otp_secret_key, :string end diff --git a/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb b/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb index e598f423..8ec40e48 100644 --- a/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +++ b/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb @@ -1,4 +1,4 @@ -class DeviseCreateAdmins < ActiveRecord::Migration +class DeviseCreateAdmins < ActiveRecord::Migration[4.2] def change create_table(:admins) do |t| ## Database authenticatable diff --git a/spec/rails_app/db/schema.rb b/spec/rails_app/db/schema.rb index 5f011618..9c0d0f18 100644 --- a/spec/rails_app/db/schema.rb +++ b/spec/rails_app/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -11,48 +10,46 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160209032439) do +ActiveRecord::Schema.define(version: 2016_02_09_032439) do create_table "admins", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false + t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_admins_on_email", unique: true + t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true end - add_index "admins", ["email"], name: "index_admins_on_email", unique: true - add_index "admins", ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true - create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false + t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "second_factor_attempts_count", default: 0 - t.string "nickname", limit: 64 - t.string "encrypted_otp_secret_key" - t.string "encrypted_otp_secret_key_iv" - t.string "encrypted_otp_secret_key_salt" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "second_factor_attempts_count", default: 0 + t.string "nickname", limit: 64 + t.string "encrypted_otp_secret_key" + t.string "encrypted_otp_secret_key_iv" + t.string "encrypted_otp_secret_key_salt" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end - add_index "users", ["email"], name: "index_users_on_email", unique: true - add_index "users", ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true - end diff --git a/spec/support/authenticated_model_helper.rb b/spec/support/authenticated_model_helper.rb index 42696e68..8138dc58 100644 --- a/spec/support/authenticated_model_helper.rb +++ b/spec/support/authenticated_model_helper.rb @@ -29,7 +29,7 @@ def generate_unique_email end def create_table_for_nonencrypted_user - silence_stream(STDOUT) do + ActiveRecord::Migration.suppress_messages do ActiveRecord::Schema.define(version: 1) do create_table 'users', force: :cascade do |t| t.string 'email', default: '', null: false diff --git a/spec/support/totp_helper.rb b/spec/support/totp_helper.rb index 1a49c811..060489dc 100644 --- a/spec/support/totp_helper.rb +++ b/spec/support/totp_helper.rb @@ -6,6 +6,6 @@ def initialize(secret_key, otp_length) end def totp_code(time = Time.now) - ROTP::TOTP.new(@secret_key, digits: @otp_length).at(time, true) + ROTP::TOTP.new(@secret_key, digits: @otp_length).at(time) end end diff --git a/two_factor_authentication.gemspec b/two_factor_authentication.gemspec index d606d6ed..9580f117 100644 --- a/two_factor_authentication.gemspec +++ b/two_factor_authentication.gemspec @@ -9,6 +9,7 @@ Gem::Specification.new do |s| s.email = ["dmitrii.golub@gmail.com"] s.homepage = "https://github.com/Houdini/two_factor_authentication" s.summary = %q{Two factor authentication plugin for devise} + s.license = "MIT" s.description = <<-EOF ### Features ### * control sms code pattern @@ -27,13 +28,13 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rails', '>= 3.1.1' s.add_runtime_dependency 'devise' s.add_runtime_dependency 'randexp' - s.add_runtime_dependency 'rotp', '>= 3.2.0' + s.add_runtime_dependency 'rotp', '>= 4.0.0' s.add_runtime_dependency 'encryptor' s.add_development_dependency 'bundler' s.add_development_dependency 'rake' s.add_development_dependency 'rspec-rails', '>= 3.0.1' - s.add_development_dependency 'capybara', '2.4.1' + s.add_development_dependency 'capybara', '~> 2.5' s.add_development_dependency 'pry' s.add_development_dependency 'timecop' end