Dmitrii Golub
9 years ago
24 changed files with 718 additions and 275 deletions
@ -1,15 +1,10 @@ |
|||||||
class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration |
class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration |
||||||
def up |
def change |
||||||
change_table :<%= table_name %> do |t| |
add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0 |
||||||
t.string :otp_secret_key |
add_column :<%= table_name %>, :encrypted_otp_secret_key, :string |
||||||
t.integer :second_factor_attempts_count, :default => 0 |
add_column :<%= table_name %>, :encrypted_otp_secret_key_iv, :string |
||||||
end |
add_column :<%= table_name %>, :encrypted_otp_secret_key_salt, :string |
||||||
|
|
||||||
add_index :<%= table_name %>, :otp_secret_key, :unique => true |
add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true |
||||||
end |
|
||||||
|
|
||||||
def down |
|
||||||
remove_column :<%= table_name %>, :otp_secret_key |
|
||||||
remove_column :<%= table_name %>, :second_factor_attempts_count |
|
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,11 +1,19 @@ |
|||||||
module TwoFactorAuthentication |
module TwoFactorAuthentication |
||||||
module Schema |
module Schema |
||||||
def otp_secret_key |
|
||||||
apply_devise_schema :otp_secret_key, String |
|
||||||
end |
|
||||||
|
|
||||||
def second_factor_attempts_count |
def second_factor_attempts_count |
||||||
apply_devise_schema :second_factor_attempts_count, Integer, :default => 0 |
apply_devise_schema :second_factor_attempts_count, Integer, :default => 0 |
||||||
end |
end |
||||||
|
|
||||||
|
def encrypted_otp_secret_key |
||||||
|
apply_devise_schema :encrypted_otp_secret_key, String |
||||||
|
end |
||||||
|
|
||||||
|
def encrypted_otp_secret_key_iv |
||||||
|
apply_devise_schema :encrypted_otp_secret_key_iv, String |
||||||
|
end |
||||||
|
|
||||||
|
def encrypted_otp_secret_key_salt |
||||||
|
apply_devise_schema :encrypted_otp_secret_key_salt, String |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,18 +0,0 @@ |
|||||||
require 'spec_helper' |
|
||||||
|
|
||||||
include Warden::Test::Helpers |
|
||||||
|
|
||||||
describe HomeController, :type => :controller do |
|
||||||
context "passed only 1st factor auth" do |
|
||||||
let(:user) { create_user } |
|
||||||
|
|
||||||
describe "is_fully_authenticated helper" do |
|
||||||
it "should be true" do |
|
||||||
login_as user, scope: :user |
|
||||||
visit user_two_factor_authentication_path |
|
||||||
|
|
||||||
expect(controller.is_fully_authenticated?).to be_truthy |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -0,0 +1,33 @@ |
|||||||
|
require 'spec_helper' |
||||||
|
|
||||||
|
describe Devise::TwoFactorAuthenticationController, type: :controller do |
||||||
|
describe 'is_fully_authenticated? helper' do |
||||||
|
before do |
||||||
|
sign_in |
||||||
|
end |
||||||
|
|
||||||
|
context 'after user enters valid OTP code' do |
||||||
|
it 'returns true' do |
||||||
|
post :update, code: controller.current_user.otp_code |
||||||
|
|
||||||
|
expect(subject.is_fully_authenticated?).to eq true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when user has not entered any OTP yet' do |
||||||
|
it 'returns false' do |
||||||
|
get :show |
||||||
|
|
||||||
|
expect(subject.is_fully_authenticated?).to eq false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when user enters an invalid OTP' do |
||||||
|
it 'returns false' do |
||||||
|
post :update, code: '12345' |
||||||
|
|
||||||
|
expect(subject.is_fully_authenticated?).to eq false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,36 @@ |
|||||||
|
require 'spec_helper' |
||||||
|
|
||||||
|
require 'generators/active_record/two_factor_authentication_generator' |
||||||
|
|
||||||
|
describe ActiveRecord::Generators::TwoFactorAuthenticationGenerator, type: :generator do |
||||||
|
destination File.expand_path('../../../../../tmp', __FILE__) |
||||||
|
|
||||||
|
before do |
||||||
|
prepare_destination |
||||||
|
end |
||||||
|
|
||||||
|
it 'runs all methods in the generator' do |
||||||
|
gen = generator %w(users) |
||||||
|
expect(gen).to receive(:copy_two_factor_authentication_migration) |
||||||
|
gen.invoke_all |
||||||
|
end |
||||||
|
|
||||||
|
describe 'the generated files' do |
||||||
|
before do |
||||||
|
run_generator %w(users) |
||||||
|
end |
||||||
|
|
||||||
|
describe 'the migration' do |
||||||
|
subject { migration_file('db/migrate/two_factor_authentication_add_to_users.rb') } |
||||||
|
|
||||||
|
it { is_expected.to exist } |
||||||
|
it { is_expected.to be_a_migration } |
||||||
|
it { is_expected.to contain /def change/ } |
||||||
|
it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ } |
||||||
|
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key, :string/ } |
||||||
|
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_iv, :string/ } |
||||||
|
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_salt, :string/ } |
||||||
|
it { is_expected.to contain /add_index :users, :encrypted_otp_secret_key, unique: true/ } |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -1,168 +1,264 @@ |
|||||||
require 'spec_helper' |
require 'spec_helper' |
||||||
include AuthenticatedModelHelper |
include AuthenticatedModelHelper |
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#otp_code' do |
describe Devise::Models::TwoFactorAuthenticatable do |
||||||
let(:instance) { build_guest_user } |
describe '#otp_code' do |
||||||
subject { instance.otp_code(time) } |
shared_examples 'otp_code' do |instance| |
||||||
let(:time) { 1392852456 } |
subject { instance.otp_code(time) } |
||||||
|
let(:time) { 1_392_852_456 } |
||||||
it "should return an error if no secret is set" do |
|
||||||
expect { |
it 'returns an error if no secret is set' do |
||||||
subject |
expect { subject }.to raise_error Exception |
||||||
}.to raise_error Exception |
end |
||||||
|
|
||||||
|
context 'secret is set' do |
||||||
|
before :each do |
||||||
|
instance.otp_secret_key = '2z6hxkdwi3uvrnpn' |
||||||
|
end |
||||||
|
|
||||||
|
it 'does not return an error' do |
||||||
|
subject |
||||||
|
end |
||||||
|
|
||||||
|
it 'matches Devise configured length' do |
||||||
|
expect(subject.length).to eq(Devise.otp_length) |
||||||
|
end |
||||||
|
|
||||||
|
context 'with a known time' do |
||||||
|
let(:time) { 1_392_852_756 } |
||||||
|
|
||||||
|
it 'returns a known result' do |
||||||
|
expect(subject). |
||||||
|
to eq('0000000524562202'.split(//).last(Devise.otp_length).join) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'with a known time yielding a result with less than 6 digits' do |
||||||
|
let(:time) { 1_393_065_856 } |
||||||
|
|
||||||
|
it 'returns a known result padded with zeroes' do |
||||||
|
expect(subject). |
||||||
|
to eq('0000001608007672'.split(//).last(Devise.otp_length).join) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it_behaves_like 'otp_code', GuestUser.new |
||||||
|
it_behaves_like 'otp_code', EncryptedUser.new |
||||||
end |
end |
||||||
|
|
||||||
context "secret is set" do |
describe '#authenticate_otp' do |
||||||
before :each do |
shared_examples 'authenticate_otp' do |instance| |
||||||
instance.otp_secret_key = "2z6hxkdwi3uvrnpn" |
before :each do |
||||||
|
instance.otp_secret_key = '2z6hxkdwi3uvrnpn' |
||||||
|
end |
||||||
|
|
||||||
|
def do_invoke(code, user) |
||||||
|
user.authenticate_otp(code) |
||||||
|
end |
||||||
|
|
||||||
|
it 'authenticates a recently created code' do |
||||||
|
code = instance.otp_code |
||||||
|
expect(do_invoke(code, instance)).to eq(true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'does not authenticate an old code' do |
||||||
|
code = instance.otp_code(1.minutes.ago.to_i) |
||||||
|
expect(do_invoke(code, instance)).to eq(false) |
||||||
|
end |
||||||
end |
end |
||||||
|
|
||||||
it "should not return an error" do |
it_behaves_like 'authenticate_otp', GuestUser.new |
||||||
subject |
it_behaves_like 'authenticate_otp', EncryptedUser.new |
||||||
|
end |
||||||
|
|
||||||
|
describe '#send_two_factor_authentication_code' do |
||||||
|
let(:instance) { build_guest_user } |
||||||
|
|
||||||
|
it 'raises an error by default' do |
||||||
|
expect { instance.send_two_factor_authentication_code }. |
||||||
|
to raise_error(NotImplementedError) |
||||||
end |
end |
||||||
|
|
||||||
it "should be configured length" do |
it 'is overrideable' do |
||||||
expect(subject.length).to eq(Devise.otp_length) |
def instance.send_two_factor_authentication_code |
||||||
|
'Code sent' |
||||||
|
end |
||||||
|
expect(instance.send_two_factor_authentication_code).to eq('Code sent') |
||||||
end |
end |
||||||
|
end |
||||||
|
|
||||||
context "with a known time" do |
describe '#provisioning_uri' do |
||||||
let(:time) { 1392852756 } |
shared_examples 'provisioning_uri' do |instance| |
||||||
|
before do |
||||||
|
instance.email = 'houdini@example.com' |
||||||
|
instance.run_callbacks :create |
||||||
|
end |
||||||
|
|
||||||
it "should return a known result" do |
it "returns uri with user's email" do |
||||||
expect(subject).to eq("0000000524562202".split(//).last(Devise.otp_length).join) |
expect(instance.provisioning_uri). |
||||||
|
to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}}) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns uri with issuer option' do |
||||||
|
expect(instance.provisioning_uri('houdini')). |
||||||
|
to match(%r{otpauth://totp/houdini\?secret=\w{16}$}) |
||||||
end |
end |
||||||
end |
|
||||||
|
|
||||||
context "with a known time yielding a result with less than 6 digits" do |
it 'returns uri with issuer option' do |
||||||
let(:time) { 1393065856 } |
require 'cgi' |
||||||
|
|
||||||
it "should return a known result padded with zeroes" do |
uri = URI.parse(instance.provisioning_uri('houdini', issuer: 'Magic')) |
||||||
expect(subject).to eq("0000001608007672".split(//).last(Devise.otp_length).join) |
params = CGI.parse(uri.query) |
||||||
|
|
||||||
|
expect(uri.scheme).to eq('otpauth') |
||||||
|
expect(uri.host).to eq('totp') |
||||||
|
expect(uri.path).to eq('/houdini') |
||||||
|
expect(params['issuer'].shift).to eq('Magic') |
||||||
|
expect(params['secret'].shift).to match(/\w{16}/) |
||||||
end |
end |
||||||
end |
end |
||||||
|
|
||||||
|
it_behaves_like 'provisioning_uri', GuestUser.new |
||||||
|
it_behaves_like 'provisioning_uri', EncryptedUser.new |
||||||
end |
end |
||||||
end |
|
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#authenticate_otp' do |
describe '#populate_otp_column' do |
||||||
let(:instance) { build_guest_user } |
shared_examples 'populate_otp_column' do |klass| |
||||||
|
let(:instance) { klass.new } |
||||||
|
|
||||||
before :each do |
it 'populates otp_column on create' do |
||||||
instance.otp_secret_key = "2z6hxkdwi3uvrnpn" |
expect(instance.otp_secret_key).to be_nil |
||||||
end |
|
||||||
|
|
||||||
def do_invoke code, options = {} |
# populate_otp_column called via before_create |
||||||
instance.authenticate_otp(code, options) |
instance.run_callbacks :create |
||||||
end |
|
||||||
|
|
||||||
it "should be able to authenticate a recently created code" do |
expect(instance.otp_secret_key).to match(/\w{16}/) |
||||||
code = instance.otp_code |
end |
||||||
expect(do_invoke(code)).to eq(true) |
|
||||||
end |
|
||||||
|
|
||||||
it "should not authenticate an old code" do |
it 'repopulates otp_column' do |
||||||
code = instance.otp_code(1.minutes.ago.to_i) |
instance.run_callbacks :create |
||||||
expect(do_invoke(code)).to eq(false) |
original_key = instance.otp_secret_key |
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#send_two_factor_authentication_code' do |
instance.populate_otp_column |
||||||
let(:instance) { build_guest_user } |
|
||||||
|
|
||||||
it "should raise an error by default" do |
expect(instance.otp_secret_key).to match(/\w{16}/) |
||||||
expect { |
expect(instance.otp_secret_key).to_not eq(original_key) |
||||||
instance.send_two_factor_authentication_code |
end |
||||||
}.to raise_error(NotImplementedError) |
end |
||||||
|
|
||||||
|
it_behaves_like 'populate_otp_column', GuestUser |
||||||
|
it_behaves_like 'populate_otp_column', EncryptedUser |
||||||
end |
end |
||||||
|
|
||||||
it "should be overrideable" do |
describe '#max_login_attempts' do |
||||||
def instance.send_two_factor_authentication_code |
let(:instance) { build_guest_user } |
||||||
"Code sent" |
|
||||||
|
before do |
||||||
|
@original_max_login_attempts = GuestUser.max_login_attempts |
||||||
|
GuestUser.max_login_attempts = 3 |
||||||
end |
end |
||||||
expect(instance.send_two_factor_authentication_code).to eq("Code sent") |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#provisioning_uri' do |
after { GuestUser.max_login_attempts = @original_max_login_attempts } |
||||||
let(:instance) { build_guest_user } |
|
||||||
|
|
||||||
before do |
it 'returns class setting' do |
||||||
instance.email = "houdini@example.com" |
expect(instance.max_login_attempts).to eq(3) |
||||||
instance.run_callbacks :create |
end |
||||||
end |
|
||||||
|
|
||||||
it "should return uri with user's email" do |
it 'returns false as boolean' do |
||||||
expect(instance.provisioning_uri).to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}}) |
instance.second_factor_attempts_count = nil |
||||||
end |
expect(instance.max_login_attempts?).to be_falsey |
||||||
|
instance.second_factor_attempts_count = 0 |
||||||
|
expect(instance.max_login_attempts?).to be_falsey |
||||||
|
instance.second_factor_attempts_count = 1 |
||||||
|
expect(instance.max_login_attempts?).to be_falsey |
||||||
|
instance.second_factor_attempts_count = 2 |
||||||
|
expect(instance.max_login_attempts?).to be_falsey |
||||||
|
end |
||||||
|
|
||||||
it "should return uri with issuer option" do |
it 'returns true as boolean after too many attempts' do |
||||||
expect(instance.provisioning_uri("houdini")).to match(%r{otpauth://totp/houdini\?secret=\w{16}$}) |
instance.second_factor_attempts_count = 3 |
||||||
|
expect(instance.max_login_attempts?).to be_truthy |
||||||
|
instance.second_factor_attempts_count = 4 |
||||||
|
expect(instance.max_login_attempts?).to be_truthy |
||||||
|
end |
||||||
end |
end |
||||||
|
|
||||||
it "should return uri with issuer option" do |
describe '.has_one_time_password' do |
||||||
require 'cgi' |
context 'when encrypted: true option is passed' do |
||||||
|
let(:instance) { EncryptedUser.new } |
||||||
|
|
||||||
uri = URI.parse(instance.provisioning_uri("houdini", issuer: 'Magic')) |
it 'encrypts otp_secret_key with iv, salt, and encoding' do |
||||||
params = CGI::parse(uri.query) |
instance.otp_secret_key = '2z6hxkdwi3uvrnpn' |
||||||
|
|
||||||
expect(uri.scheme).to eq("otpauth") |
expect(instance.encrypted_otp_secret_key).to match(/.{44}/) |
||||||
expect(uri.host).to eq("totp") |
|
||||||
expect(uri.path).to eq("/houdini") |
expect(instance.encrypted_otp_secret_key_iv).to match(/.{24}/) |
||||||
expect(params['issuer'].shift).to eq('Magic') |
|
||||||
expect(params['secret'].shift).to match(%r{\w{16}}) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#populate_otp_column' do |
expect(instance.encrypted_otp_secret_key_salt).to match(/.{25}/) |
||||||
let(:instance) { build_guest_user } |
end |
||||||
|
|
||||||
it "populates otp_column on create" do |
it 'does not encrypt a nil otp_secret_key' do |
||||||
expect(instance.otp_secret_key).to be_nil |
instance.otp_secret_key = nil |
||||||
|
|
||||||
instance.run_callbacks :create # populate_otp_column called via before_create |
expect(instance.encrypted_otp_secret_key).to be_nil |
||||||
|
|
||||||
expect(instance.otp_secret_key).to match(%r{\w{16}}) |
expect(instance.encrypted_otp_secret_key_iv).to be_nil |
||||||
end |
|
||||||
|
|
||||||
it "repopulates otp_column" do |
expect(instance.encrypted_otp_secret_key_salt).to be_nil |
||||||
instance.run_callbacks :create |
end |
||||||
original_key = instance.otp_secret_key |
|
||||||
|
|
||||||
instance.populate_otp_column |
it 'does not encrypt an empty otp_secret_key' do |
||||||
|
instance.otp_secret_key = '' |
||||||
|
|
||||||
expect(instance.otp_secret_key).to match(%r{\w{16}}) |
expect(instance.encrypted_otp_secret_key).to eq '' |
||||||
expect(instance.otp_secret_key).to_not eq(original_key) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe Devise::Models::TwoFactorAuthenticatable, '#max_login_attempts' do |
expect(instance.encrypted_otp_secret_key_iv).to be_nil |
||||||
let(:instance) { build_guest_user } |
|
||||||
|
|
||||||
before do |
expect(instance.encrypted_otp_secret_key_salt).to be_nil |
||||||
@original_max_login_attempts = GuestUser.max_login_attempts |
end |
||||||
GuestUser.max_login_attempts = 3 |
|
||||||
end |
|
||||||
|
|
||||||
after { GuestUser.max_login_attempts = @original_max_login_attempts } |
it 'raises an error when Devise.otp_secret_encryption_key is not set' do |
||||||
|
allow(Devise).to receive(:otp_secret_encryption_key).and_return nil |
||||||
|
|
||||||
it "returns class setting" do |
# This error is raised by the encryptor gem |
||||||
expect(instance.max_login_attempts).to eq(3) |
expect { instance.otp_secret_key = '2z6hxkdwi3uvrnpn' }. |
||||||
end |
to raise_error ArgumentError, 'must specify a :key' |
||||||
|
end |
||||||
|
|
||||||
it "returns false as boolean" do |
it 'passes in the correct options to Encryptor' do |
||||||
instance.second_factor_attempts_count = nil |
instance.otp_secret_key = 'testing' |
||||||
expect(instance.max_login_attempts?).to be_falsey |
iv = instance.encrypted_otp_secret_key_iv |
||||||
instance.second_factor_attempts_count = 0 |
salt = instance.encrypted_otp_secret_key_salt |
||||||
expect(instance.max_login_attempts?).to be_falsey |
|
||||||
instance.second_factor_attempts_count = 1 |
encrypted = Encryptor.encrypt( |
||||||
expect(instance.max_login_attempts?).to be_falsey |
value: 'testing', |
||||||
instance.second_factor_attempts_count = 2 |
key: Devise.otp_secret_encryption_key, |
||||||
expect(instance.max_login_attempts?).to be_falsey |
iv: iv.unpack('m').first, |
||||||
end |
salt: salt.unpack('m').first |
||||||
|
) |
||||||
|
|
||||||
it "returns true as boolean after too many attempts" do |
expect(instance.encrypted_otp_secret_key).to eq [encrypted].pack('m') |
||||||
instance.second_factor_attempts_count = 3 |
end |
||||||
expect(instance.max_login_attempts?).to be_truthy |
|
||||||
instance.second_factor_attempts_count = 4 |
it 'varies the iv per instance' do |
||||||
expect(instance.max_login_attempts?).to be_truthy |
instance.otp_secret_key = 'testing' |
||||||
|
user2 = EncryptedUser.new |
||||||
|
user2.otp_secret_key = 'testing' |
||||||
|
|
||||||
|
expect(user2.encrypted_otp_secret_key_iv). |
||||||
|
to_not eq instance.encrypted_otp_secret_key_iv |
||||||
|
end |
||||||
|
|
||||||
|
it 'varies the salt per instance' do |
||||||
|
instance.otp_secret_key = 'testing' |
||||||
|
user2 = EncryptedUser.new |
||||||
|
user2.otp_secret_key = 'testing' |
||||||
|
|
||||||
|
expect(user2.encrypted_otp_secret_key_salt). |
||||||
|
to_not eq instance.encrypted_otp_secret_key_salt |
||||||
|
end |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,14 @@ |
|||||||
|
class EncryptedUser |
||||||
|
extend ActiveModel::Callbacks |
||||||
|
include ActiveModel::Validations |
||||||
|
include Devise::Models::TwoFactorAuthenticatable |
||||||
|
|
||||||
|
define_model_callbacks :create |
||||||
|
attr_accessor :encrypted_otp_secret_key, |
||||||
|
:encrypted_otp_secret_key_iv, |
||||||
|
:encrypted_otp_secret_key_salt, |
||||||
|
:email, |
||||||
|
:second_factor_attempts_count |
||||||
|
|
||||||
|
has_one_time_password(encrypted: true) |
||||||
|
end |
@ -0,0 +1,9 @@ |
|||||||
|
class AddEncryptedColumnsToUser < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
add_column :users, :encrypted_otp_secret_key, :string |
||||||
|
add_column :users, :encrypted_otp_secret_key_iv, :string |
||||||
|
add_column :users, :encrypted_otp_secret_key_salt, :string |
||||||
|
|
||||||
|
add_index :users, :encrypted_otp_secret_key, unique: true |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,19 @@ |
|||||||
|
class PopulateOtpColumn < 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 |
@ -0,0 +1,5 @@ |
|||||||
|
class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
remove_column :users, :otp_secret_key, :string |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,16 @@ |
|||||||
|
module ControllerHelper |
||||||
|
def sign_in(user = create_user('not_encrypted')) |
||||||
|
allow(warden).to receive(:authenticated?).with(:user).and_return(true) |
||||||
|
allow(controller).to receive(:current_user).and_return(user) |
||||||
|
warden.session(:user)[TwoFactorAuthentication::NEED_AUTHENTICATION] = true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
RSpec.configure do |config| |
||||||
|
config.include Devise::TestHelpers, type: :controller |
||||||
|
config.include ControllerHelper, type: :controller |
||||||
|
|
||||||
|
config.before(:example, type: :controller) do |
||||||
|
@request.env['devise.mapping'] = Devise.mappings[:user] |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue