two_factor_spec.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe 'User', current_user_id: 1, performs_jobs: true, type: :request do
  4. let(:agent) { create(:agent) }
  5. let(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user: agent) }
  6. let(:two_factor_enabled) { true }
  7. let(:token_expires_at) { 1.hour.from_now }
  8. let(:token) { create(:token, action: 'PasswordCheck', persistent: false, user: agent, expires_at: token_expires_at) }
  9. let(:token_value) { token&.token }
  10. before do
  11. Setting.set('two_factor_authentication_method_authenticator_app', two_factor_enabled)
  12. two_factor_pref
  13. action_user = agent
  14. permissions = %w[user_preferences.two_factor_authentication]
  15. # users with 2FA can no longer login via basic auth
  16. # thus simulating token login
  17. authenticated_as(action_user, token: create(:token, user: action_user, permissions: permissions))
  18. allow(Token).to receive(:validate!).and_call_original
  19. allow(Token).to receive(:validate!).with(token: :invalid, action: 'PasswordCheck').and_raise(Token::TokenInvalid)
  20. end
  21. shared_examples 'cleaning up used token' do
  22. it 'removes token' do
  23. expect(Token).not_to exist(token: token_value)
  24. end
  25. end
  26. shared_examples 'keeping used token' do
  27. it 'keeps token' do
  28. expect(Token).to exist(token: token_value)
  29. end
  30. end
  31. shared_examples 'ensuring token is valid' do
  32. context 'when token is invalid' do
  33. let(:token_value) { :invalid }
  34. it 'returns an error if token is invalid' do
  35. expect(json_response).to include('invalid_password_token' => true)
  36. end
  37. end
  38. end
  39. describe 'DELETE /users/two_factor/remove_authentication_method' do
  40. before do
  41. delete '/api/v1/users/two_factor/remove_authentication_method',
  42. params: { method: 'authenticator_app', token: token_value }, as: :json
  43. end
  44. it 'gets the result', :aggregate_failures do
  45. expect(response).to have_http_status(:ok)
  46. expect { two_factor_pref.reload }.to raise_error(ActiveRecord::RecordNotFound)
  47. end
  48. it_behaves_like 'cleaning up used token'
  49. it_behaves_like 'ensuring token is valid'
  50. end
  51. describe 'POST /users/two_factor/enabled_authentication_methods' do
  52. before do
  53. post '/api/v1/users/two_factor/enabled_authentication_methods', params: { token: token_value }, as: :json
  54. end
  55. it_behaves_like 'keeping used token'
  56. it_behaves_like 'ensuring token is valid'
  57. context 'with disabled authenticator app method' do
  58. let(:two_factor_enabled) { false }
  59. let(:two_factor_pref) { nil }
  60. it 'returns nothing', :aggregate_failures do
  61. expect(response).to have_http_status(:ok)
  62. expect(json_response).to be_blank
  63. end
  64. end
  65. context 'with not having authenticator app configured' do
  66. let(:two_factor_pref) { nil }
  67. it 'returns the correct result', :aggregate_failures do
  68. expect(response).to have_http_status(:ok)
  69. expect(json_response.first).to eq({
  70. 'method' => 'authenticator_app',
  71. 'configured' => false,
  72. 'default' => false,
  73. })
  74. end
  75. end
  76. context 'with having authenticator app configured' do
  77. it 'returns the correct result', :aggregate_failures do
  78. expect(response).to have_http_status(:ok)
  79. expect(json_response.first).to eq({
  80. 'method' => 'authenticator_app',
  81. 'configured' => true,
  82. 'default' => true,
  83. })
  84. end
  85. end
  86. end
  87. describe 'POST /users/two_factor/verify_configuration' do
  88. let(:recover_codes_enabled) { true }
  89. let(:has_recovery_codes) { false }
  90. let(:two_factor_pref) { nil }
  91. let(:params) { { token: token_value } }
  92. let(:method) { 'authenticator_app' }
  93. let(:verification_code) { ROTP::TOTP.new(configuration[:secret]).now }
  94. let(:configuration) { agent.auth_two_factor.authentication_method_object(method).initiate_configuration }
  95. before do
  96. if has_recovery_codes
  97. create(:user_two_factor_preference, :recovery_codes, user: agent)
  98. end
  99. Setting.set('two_factor_authentication_recovery_codes', recover_codes_enabled)
  100. post '/api/v1/users/two_factor/verify_configuration', params: params, as: :json
  101. end
  102. it 'fails without needed params' do
  103. expect(response).to have_http_status(:unprocessable_entity)
  104. end
  105. it_behaves_like 'ensuring token is valid'
  106. context 'with needed params' do
  107. let(:params) do
  108. {
  109. method: method,
  110. token: token_value,
  111. payload: verification_code,
  112. configuration: configuration,
  113. }
  114. end
  115. context 'with wrong verification code' do
  116. let(:verification_code) { 'wrong' }
  117. it 'verified is false' do
  118. expect(json_response['verified']).to be(false)
  119. end
  120. it_behaves_like 'keeping used token'
  121. end
  122. context 'with correct verification code', :aggregate_failures do
  123. it 'verified is true' do
  124. expect(json_response['verified']).to be(true)
  125. expect(json_response['recovery_codes'].length).to eq(10)
  126. expect(Token).not_to exist(token: token_value)
  127. end
  128. context 'with disabled recovery codes' do
  129. let(:recover_codes_enabled) { false }
  130. it 'verified is true (but without recovery codes)' do
  131. expect(json_response['verified']).to be(true)
  132. expect(json_response['recovery_codes']).to be_nil
  133. end
  134. end
  135. context 'with existing recovery codes' do
  136. let(:has_recovery_codes) { true }
  137. it 'verified is true (but without recovery codes)' do
  138. expect(json_response['verified']).to be(true)
  139. expect(json_response['recovery_codes']).to be_nil
  140. end
  141. end
  142. it_behaves_like 'cleaning up used token'
  143. end
  144. end
  145. end
  146. describe 'POST /users/two_factor/recovery_codes_generate' do
  147. let(:recover_codes_enabled) { true }
  148. let(:current_codes) { [] }
  149. let(:params) { { token: token_value } }
  150. before do
  151. Setting.set('two_factor_authentication_recovery_codes', recover_codes_enabled)
  152. current_codes
  153. post '/api/v1/users/two_factor/recovery_codes_generate', params:, as: :json
  154. end
  155. it_behaves_like 'ensuring token is valid'
  156. it_behaves_like 'cleaning up used token'
  157. context 'with disabled recovery codes' do
  158. let(:recover_codes_enabled) { false }
  159. it 'does not generate codes' do
  160. expect(json_response).to be_nil
  161. end
  162. end
  163. context 'without existing recovery codes' do
  164. it 'does generate codes' do
  165. expect(json_response.length).to eq(10)
  166. end
  167. end
  168. context 'with existing recovery codes' do
  169. let(:current_codes) { Auth::TwoFactor::RecoveryCodes.new(agent).generate }
  170. it 'does not generate codes' do
  171. expect(json_response).not_to eq(current_codes)
  172. end
  173. end
  174. end
  175. describe 'POST /users/two_factor/authentication_method_initiate_configuration/:method' do
  176. let(:two_factor_pref) { nil }
  177. let(:method) { 'authenticator_app' }
  178. let(:params) { { token: token_value } }
  179. before do
  180. post "/api/v1/users/two_factor/authentication_method_initiate_configuration/#{method}", params:, as: :json
  181. end
  182. it_behaves_like 'keeping used token'
  183. it_behaves_like 'ensuring token is valid'
  184. context 'with invalid params' do
  185. context 'with an unknown method' do
  186. let(:method) { 'unknown' }
  187. it 'fails' do
  188. expect(response).to have_http_status(:unprocessable_entity)
  189. end
  190. end
  191. end
  192. context 'with valid params' do
  193. it 'returns configuration', :aggregate_failures do
  194. expect(response).to have_http_status(:ok)
  195. expect(json_response['configuration']).to include('secret').and include('provisioning_uri')
  196. end
  197. end
  198. end
  199. describe 'POST /users/two_factor/authentication_method_configuration/:method' do
  200. let(:method) { 'security_keys' }
  201. let(:two_factor_pref) { create(:user_two_factor_preference, :security_keys, user: agent) }
  202. let(:params) { { token: token_value } }
  203. before do
  204. post "/api/v1/users/two_factor/authentication_method_configuration/#{method}", params:, as: :json
  205. end
  206. it_behaves_like 'keeping used token'
  207. it_behaves_like 'ensuring token is valid'
  208. context 'with invalid params' do
  209. context 'with an unknown method' do
  210. let(:method) { 'unknown' }
  211. it 'fails' do
  212. expect(response).to have_http_status(:unprocessable_entity)
  213. end
  214. end
  215. end
  216. context 'with valid params' do
  217. context 'with no stored two-factor preference' do
  218. let(:two_factor_pref) { nil }
  219. it 'returns nothing', :aggregate_failures do
  220. expect(response).to have_http_status(:ok)
  221. expect(json_response['configuration']).to be_empty
  222. end
  223. end
  224. it 'returns configuration', :aggregate_failures do
  225. expect(response).to have_http_status(:ok)
  226. expect(json_response['configuration']).to include('credentials')
  227. end
  228. context 'with authenticator app method in client context' do
  229. let(:method) { 'authenticator_app' }
  230. let(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user: agent) }
  231. it 'returns nothing', :aggregate_failures do
  232. expect(response).to have_http_status(:ok)
  233. expect(json_response['configuration']).to be_empty
  234. end
  235. end
  236. end
  237. end
  238. describe 'DELETE /users/two_factor/authentication_remove_credentials/:method/:credential_id' do
  239. it 'fails without needed params' do
  240. delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
  241. params: { token: token_value },
  242. as: :json
  243. expect(response).to have_http_status(:unprocessable_entity)
  244. end
  245. context 'with needed params' do
  246. let(:method) { 'security_keys' }
  247. let(:credential_id) { 'credential_id' }
  248. let(:two_factor_pref) do
  249. create(:user_two_factor_preference, :security_keys, credential_public_key: credential_id, user: agent)
  250. end
  251. context 'when removing configuration' do
  252. let(:params) { { credential_id:, token: token_value } }
  253. describe 'tokens behavior' do
  254. before do
  255. delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
  256. params: params,
  257. as: :json
  258. end
  259. it_behaves_like 'keeping used token'
  260. it_behaves_like 'ensuring token is valid'
  261. end
  262. it 'returns ok and updates configuration', :aggregate_failures do
  263. allow(Service::User::TwoFactor::RemoveMethodCredentials)
  264. .to receive(:new)
  265. .and_call_original
  266. expect_any_instance_of(Service::User::TwoFactor::RemoveMethodCredentials)
  267. .to receive(:execute)
  268. .and_call_original
  269. delete '/api/v1/users/two_factor/authentication_remove_credentials/security_keys',
  270. params: params,
  271. as: :json
  272. expect(response).to have_http_status(:ok)
  273. expect(Service::User::TwoFactor::RemoveMethodCredentials)
  274. .to have_received(:new).with(user: agent, method_name: 'security_keys', credential_id:)
  275. end
  276. end
  277. end
  278. end
  279. describe 'POST /users/two_factor/default_authentication_method' do
  280. let(:method) { 'unknown' }
  281. let(:params) { {} }
  282. before do
  283. Setting.set('two_factor_authentication_method_security_keys', two_factor_enabled)
  284. create(:user_two_factor_preference, :security_keys, user: agent)
  285. post '/api/v1/users/two_factor/default_authentication_method', params: params, as: :json
  286. end
  287. it 'fails without needed params' do
  288. expect(response).to have_http_status(:unprocessable_entity)
  289. end
  290. context 'with needed params' do
  291. let(:params) { { method: method } }
  292. let(:method) { 'security_keys' }
  293. it 'returns ok and updates default method', :aggregate_failures do
  294. expect(response).to have_http_status(:ok)
  295. expect(agent.reload.preferences.dig(:two_factor_authentication, :default)).to eq(method)
  296. end
  297. end
  298. end
  299. end