zammad/spec/requests/user/two_factor_spec.rb

371 lines
12 KiB
Ruby

# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe 'User', current_user_id: 1, performs_jobs: true, type: :request do
let(:agent) { create(:agent) }
let(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user: agent) }
let(:two_factor_enabled) { true }
let(:token_expires_at) { 1.hour.from_now }
let(:token) { create(:token, action: 'PasswordCheck', persistent: false, user: agent, expires_at: token_expires_at) }
let(:token_value) { token&.token }
before do
Setting.set('two_factor_authentication_method_authenticator_app', two_factor_enabled)
two_factor_pref
action_user = agent
permissions = %w[user_preferences.two_factor_authentication]
# users with 2FA can no longer login via basic auth
# thus simulating token login
authenticated_as(action_user, token: create(:token, user: action_user, permissions: permissions))
allow(Token).to receive(:validate!).and_call_original
allow(Token).to receive(:validate!).with(token: :invalid, action: 'PasswordCheck').and_raise(Token::TokenInvalid)
end
shared_examples 'cleaning up used token' do
it 'removes token' do
expect(Token).not_to exist(token: token_value)
end
end
shared_examples 'keeping used token' do
it 'keeps token' do
expect(Token).to exist(token: token_value)
end
end
shared_examples 'ensuring token is valid' do
context 'when token is invalid' do
let(:token_value) { :invalid }
it 'returns an error if token is invalid' do
expect(json_response).to include('invalid_password_token' => true)
end
end
end
describe 'DELETE /users/two_factor/remove_authentication_method' do
before do
delete '/api/v1/users/two_factor/remove_authentication_method',
params: { method: 'authenticator_app', token: token_value }, as: :json
end
it 'gets the result', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect { two_factor_pref.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it_behaves_like 'cleaning up used token'
it_behaves_like 'ensuring token is valid'
end
describe 'POST /users/two_factor/enabled_authentication_methods' do
before do
post '/api/v1/users/two_factor/enabled_authentication_methods', params: { token: token_value }, as: :json
end
it_behaves_like 'keeping used token'
it_behaves_like 'ensuring token is valid'
context 'with disabled authenticator app method' do
let(:two_factor_enabled) { false }
let(:two_factor_pref) { nil }
it 'returns nothing', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response).to be_blank
end
end
context 'with not having authenticator app configured' do
let(:two_factor_pref) { nil }
it 'returns the correct result', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response.first).to eq({
'method' => 'authenticator_app',
'configured' => false,
'default' => false,
})
end
end
context 'with having authenticator app configured' do
it 'returns the correct result', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response.first).to eq({
'method' => 'authenticator_app',
'configured' => true,
'default' => true,
})
end
end
end
describe 'POST /users/two_factor/verify_configuration' do
let(:recover_codes_enabled) { true }
let(:has_recovery_codes) { false }
let(:two_factor_pref) { nil }
let(:params) { { token: token_value } }
let(:method) { 'authenticator_app' }
let(:verification_code) { ROTP::TOTP.new(configuration[:secret]).now }
let(:configuration) { agent.auth_two_factor.authentication_method_object(method).initiate_configuration }
before do
if has_recovery_codes
create(:user_two_factor_preference, :recovery_codes, user: agent)
end
Setting.set('two_factor_authentication_recovery_codes', recover_codes_enabled)
post '/api/v1/users/two_factor/verify_configuration', params: params, as: :json
end
it 'fails without needed params' do
expect(response).to have_http_status(:unprocessable_content)
end
it_behaves_like 'ensuring token is valid'
context 'with needed params' do
let(:params) do
{
method: method,
token: token_value,
payload: verification_code,
configuration: configuration,
}
end
context 'with wrong verification code' do
let(:verification_code) { 'wrong' }
it 'verified is false' do
expect(json_response['verified']).to be(false)
end
it_behaves_like 'keeping used token'
end
context 'with correct verification code', :aggregate_failures do
it 'verified is true' do
expect(json_response['verified']).to be(true)
expect(json_response['recovery_codes'].length).to eq(10)
expect(Token).not_to exist(token: token_value)
end
context 'with disabled recovery codes' do
let(:recover_codes_enabled) { false }
it 'verified is true (but without recovery codes)' do
expect(json_response['verified']).to be(true)
expect(json_response['recovery_codes']).to be_nil
end
end
context 'with existing recovery codes' do
let(:has_recovery_codes) { true }
it 'verified is true (but without recovery codes)' do
expect(json_response['verified']).to be(true)
expect(json_response['recovery_codes']).to be_nil
end
end
it_behaves_like 'cleaning up used token'
end
end
end
describe 'POST /users/two_factor/recovery_codes_generate' do
let(:recover_codes_enabled) { true }
let(:current_codes) { [] }
let(:params) { { token: token_value } }
before do
Setting.set('two_factor_authentication_recovery_codes', recover_codes_enabled)
current_codes
post '/api/v1/users/two_factor/recovery_codes_generate', params:, as: :json
end
it_behaves_like 'ensuring token is valid'
it_behaves_like 'cleaning up used token'
context 'with disabled recovery codes' do
let(:recover_codes_enabled) { false }
it 'does not generate codes' do
expect(json_response).to be_nil
end
end
context 'without existing recovery codes' do
it 'does generate codes' do
expect(json_response.length).to eq(10)
end
end
context 'with existing recovery codes' do
let(:current_codes) { Auth::TwoFactor::RecoveryCodes.new(agent).generate }
it 'does not generate codes' do
expect(json_response).not_to eq(current_codes)
end
end
end
describe 'POST /users/two_factor/authentication_method_initiate_configuration/:method' do
let(:two_factor_pref) { nil }
let(:method) { 'authenticator_app' }
let(:params) { { token: token_value } }
before do
post "/api/v1/users/two_factor/authentication_method_initiate_configuration/#{method}", params:, as: :json
end
it_behaves_like 'keeping used token'
it_behaves_like 'ensuring token is valid'
context 'with invalid params' do
context 'with an unknown method' do
let(:method) { 'unknown' }
it 'fails' do
expect(response).to have_http_status(:unprocessable_content)
end
end
end
context 'with valid params' do
it 'returns configuration', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response['configuration']).to include('secret').and include('provisioning_uri')
end
end
end
describe 'POST /users/two_factor/authentication_method_configuration/:method' do
let(:method) { 'security_keys' }
let(:two_factor_pref) { create(:user_two_factor_preference, :security_keys, user: agent) }
let(:params) { { token: token_value } }
before do
post "/api/v1/users/two_factor/authentication_method_configuration/#{method}", params:, as: :json
end
it_behaves_like 'keeping used token'
it_behaves_like 'ensuring token is valid'
context 'with invalid params' do
context 'with an unknown method' do
let(:method) { 'unknown' }
it 'fails' do
expect(response).to have_http_status(:unprocessable_content)
end
end
end
context 'with valid params' do
context 'with no stored two-factor preference' do
let(:two_factor_pref) { nil }
it 'returns nothing', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response['configuration']).to be_empty
end
end
it 'returns configuration', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response['configuration']).to include('credentials')
end
context 'with authenticator app method in client context' do
let(:method) { 'authenticator_app' }
let(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user: agent) }
it 'returns nothing', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response['configuration']).to be_empty
end
end
end
end
describe 'DELETE /users/two_factor/authentication_remove_credentials/:method/:credential_id' do
it 'fails without needed params' do
delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
params: { token: token_value },
as: :json
expect(response).to have_http_status(:unprocessable_content)
end
context 'with needed params' do
let(:method) { 'security_keys' }
let(:credential_id) { 'credential_id' }
let(:two_factor_pref) do
create(:user_two_factor_preference, :security_keys, credential_public_key: credential_id, user: agent)
end
context 'when removing configuration' do
let(:params) { { credential_id:, token: token_value } }
describe 'tokens behavior' do
before do
delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
params: params,
as: :json
end
it_behaves_like 'keeping used token'
it_behaves_like 'ensuring token is valid'
end
it 'returns ok and updates configuration', :aggregate_failures do
allow(Service::User::TwoFactor::RemoveMethodCredentials)
.to receive(:execute)
.and_call_original
delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
params: params,
as: :json
expect(response).to have_http_status(:ok)
expect(Service::User::TwoFactor::RemoveMethodCredentials)
.to have_received(:execute).with(method_name: 'security_keys', credential_id:, current_user: agent)
end
end
end
end
describe 'POST /users/two_factor/default_authentication_method' do
let(:method) { 'unknown' }
let(:params) { {} }
before do
Setting.set('two_factor_authentication_method_security_keys', two_factor_enabled)
create(:user_two_factor_preference, :security_keys, user: agent)
post '/api/v1/users/two_factor/default_authentication_method', params: params, as: :json
end
it 'fails without needed params' do
expect(response).to have_http_status(:unprocessable_content)
end
context 'with needed params' do
let(:params) { { method: method } }
let(:method) { 'security_keys' }
it 'returns ok and updates default method', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(agent.reload.preferences.dig(:two_factor_authentication, :default)).to eq(method)
end
end
end
end