123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe Auth::TwoFactor, current_user_id: 1 do
- let(:user) { create(:user) }
- let(:instance) { described_class.new(user) }
- before do
- Setting.set('two_factor_authentication_method_authenticator_app', true)
- end
- describe '#all_authentication_methods' do
- it 'returns all methods, including disabled and not setup for user' do
- expect(instance.all_authentication_methods.map { |elem| elem.class.name })
- .to eq([
- Auth::TwoFactor::AuthenticationMethod::SecurityKeys.name,
- Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp.name,
- ])
- end
- it 'returns instance for current user' do
- expect(instance.all_authentication_methods.first).to have_attributes(user: user)
- end
- end
- describe '.authentication_method_classes' do
- it 'returns sorted methods' do
- expect(described_class.authentication_method_classes)
- .to eq([
- Auth::TwoFactor::AuthenticationMethod::SecurityKeys,
- Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp,
- ])
- end
- end
- describe '#enabled_authentication_methods' do
- it 'returns only enabled method' do
- expect(instance.enabled_authentication_methods)
- .to contain_exactly(be_a(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp))
- end
- end
- describe '#available_authentication_methods' do
- it 'returns available methods' do
- expect(instance.available_authentication_methods)
- .to contain_exactly(be_a(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp))
- end
- context 'when enabled method is not available' do
- before do
- allow_any_instance_of(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- .to receive(:available?)
- .and_return(false)
- end
- it 'returns available methods' do
- expect(instance.available_authentication_methods).to be_empty
- end
- end
- end
- describe '#enabled?' do
- it 'returns true' do
- expect(instance).to be_enabled
- end
- context 'without enabled methods' do
- before do
- Setting.set('two_factor_authentication_method_authenticator_app', false)
- end
- it 'returns false' do
- expect(instance).not_to be_enabled
- end
- end
- end
- describe '#verify_configuration?' do
- it 'returns false if invalid method name given' do
- expect(instance)
- .not_to be_verify_configuration('nonexistantmethod', {}, {})
- end
- it 'returns false if invalid payload or configuration given' do
- expect(instance)
- .not_to be_verify_configuration('authenticator_app', {}, {})
- end
- context 'when payload and configuration are valid' do
- before do
- allow_any_instance_of(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- .to receive(:verify)
- .and_return({ config: :yes, verified: true })
- end
- it 'returns true' do
- expect(instance)
- .to be_verify_configuration('authenticator_app', {}, {})
- end
- it 'creates uer configuration' do
- expect_any_instance_of(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- .to receive(:create_user_config)
- .with({ config: :yes })
- instance.verify_configuration?('authenticator_app', {}, {})
- end
- end
- end
- describe '#authentication_method_object' do
- before { create(:user_two_factor_preference, :authenticator_app, user: user) }
- it 'returns expected value' do
- expect(instance.authentication_method_object('authenticator_app')).to be_a(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- end
- end
- describe '#user_authentication_methods' do
- before { create(:user_two_factor_preference, :authenticator_app, user: user) }
- it 'returns expected value' do
- expect(instance.user_authentication_methods).to include(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- end
- end
- describe '#user_default_method' do
- before { create(:user_two_factor_preference, :authenticator_app, user: user) }
- it 'returns expected value' do
- # 'user' variable is cached + was created before the preference was set.
- user.reload
- expect(instance.user_default_authentication_method).to be_a(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- end
- context 'when two methods exist' do
- let(:another_method) { create(:user_two_factor_preference, :security_keys, user: user) }
- before do
- Setting.set('two_factor_authentication_method_security_keys', true)
- another_method
- user.reload
- user.preferences[:two_factor_authentication][:default] = 'security_keys'
- user.save!
- end
- it 'returns selected method' do
- user.reload
- expect(instance.user_default_authentication_method)
- .to be_a(Auth::TwoFactor::AuthenticationMethod::SecurityKeys)
- end
- context 'when default method disabled' do
- before do
- Setting.set('two_factor_authentication_method_security_keys', false)
- end
- it 'returns another method' do
- user.reload
- expect(instance.user_default_authentication_method)
- .to be_a(Auth::TwoFactor::AuthenticationMethod::AuthenticatorApp)
- end
- end
- end
- end
- describe '#user_setup_required' do
- let(:user_role) { create(:role, :agent) }
- let(:non_user_role) { create(:role, :agent) }
- let(:user) { create(:user, roles: [user_role]) }
- context 'when the setup is required' do
- before do
- Setting.set('two_factor_authentication_enforce_role_ids', [user_role.id])
- end
- it 'returns expected value' do
- expect(instance.user_setup_required?).to be(true)
- end
- end
- context 'when the setup is not required' do
- before do
- Setting.set('two_factor_authentication_enforce_role_ids', [non_user_role.id])
- end
- it 'returns expected value' do
- expect(instance.user_setup_required?).to be(false)
- end
- end
- end
- describe '#user_configured' do
- before do
- Setting.set('two_factor_authentication_method_authenticator_app', true)
- end
- shared_examples 'recovery codes present' do |configured|
- before do
- Auth::TwoFactor::RecoveryCodes.new(user).generate
- end
- it 'returns expected value' do
- expect(instance.user_configured?).to be(configured)
- end
- end
- context 'when a method is configured' do
- before do
- create(:user_two_factor_preference, :authenticator_app, user: user)
- # 'user' variable is cached + was created before the preference was set.
- user.reload
- end
- it 'returns expected value' do
- expect(instance.user_configured?).to be(true)
- end
- it_behaves_like 'recovery codes present', true
- end
- context 'when a method is not configured' do
- before { user.reload }
- it 'returns expected value' do
- expect(instance.user_configured?).to be(false)
- end
- it_behaves_like 'recovery codes present', false
- end
- end
- describe '#verify?' do
- let(:secret) { ROTP::Base32.random_base32 }
- let(:last_otp_at) { 1_256_953_732 } # 2009-10-31T01:48:52Z
- let(:two_factor_pref) do
- create(:user_two_factor_preference, :authenticator_app,
- user: user,
- method: method,
- configuration: configuration)
- end
- let(:configuration) do
- {
- last_otp_at: last_otp_at,
- secret: secret,
- }
- end
- before { two_factor_pref }
- context 'with authenticator app as method' do
- let(:method) { 'authenticator_app' }
- let(:code) { ROTP::TOTP.new(secret).now }
- shared_examples 'returning true result' do
- it 'returns true result' do
- result = instance.verify?(method, code)
- expect(result).to be true
- end
- it 'updates last otp at timestamp' do
- instance.verify?(method, code)
- expect(user.two_factor_preferences.find_by(method: method).configuration[:last_otp_at]).to be > last_otp_at
- end
- end
- shared_examples 'returning false result' do
- it 'returns false result' do
- result = instance.verify?(method, code)
- expect(result).to be false
- end
- end
- context 'with valid code provided' do
- let(:code) { ROTP::TOTP.new(secret).now }
- it_behaves_like 'returning true result'
- end
- context 'with invalid code provided' do
- let(:code) { 'FOOBAR' }
- it_behaves_like 'returning false result'
- end
- context 'with no configured secret' do
- let(:code) { ROTP::TOTP.new(secret).now }
- let(:configuration) do
- {
- foo: 'bar',
- }
- end
- it_behaves_like 'returning false result'
- end
- context 'with no configured method' do
- let(:code) { ROTP::TOTP.new(secret).now }
- let(:configuration) { nil }
- it_behaves_like 'returning false result'
- end
- context 'with used recovery code' do
- let(:method) { 'recovery_codes' }
- let(:current_codes) { Auth::TwoFactor::RecoveryCodes.new(user).generate }
- let(:code) { current_codes.first }
- let(:two_factor_pref) { nil }
- before do
- current_codes
- end
- it 'returns true result' do
- expect(instance.verify?(method, code)).to be true
- end
- context 'with invalid code provided' do
- let(:code) { 'wrong' }
- it 'returns false result' do
- expect(instance.verify?(method, code)).to be false
- end
- end
- end
- end
- end
- end
|