mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
336 lines
9.7 KiB
Ruby
336 lines
9.7 KiB
Ruby
# Copyright (C) 2012-2026 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
|