sessions_controller_spec.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. require 'webauthn/fake_client'
  4. RSpec.describe Auth::SessionsController do
  5. render_views
  6. before do
  7. request.env['devise.mapping'] = Devise.mappings[:user]
  8. end
  9. describe 'GET #new' do
  10. it 'returns http success' do
  11. get :new
  12. expect(response).to have_http_status(200)
  13. end
  14. end
  15. describe 'DELETE #destroy' do
  16. let(:user) { Fabricate(:user) }
  17. context 'with a regular user' do
  18. it 'redirects to home after sign out' do
  19. sign_in(user, scope: :user)
  20. delete :destroy
  21. expect(response).to redirect_to(new_user_session_path)
  22. end
  23. it 'does not delete redirect location with continue=true' do
  24. sign_in(user, scope: :user)
  25. controller.store_location_for(:user, '/authorize')
  26. delete :destroy, params: { continue: 'true' }
  27. expect(controller.stored_location_for(:user)).to eq '/authorize'
  28. end
  29. end
  30. context 'with a suspended user' do
  31. before do
  32. user.account.suspend!
  33. end
  34. it 'redirects to home after sign out' do
  35. sign_in(user, scope: :user)
  36. delete :destroy
  37. expect(response).to redirect_to(new_user_session_path)
  38. end
  39. end
  40. end
  41. describe 'POST #create' do
  42. context 'when using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do
  43. context 'when using a valid password' do
  44. before do
  45. post :create, params: { user: { email: 'pam_user1', password: '123456' } }
  46. end
  47. it 'redirects to home' do
  48. expect(response).to redirect_to(root_path)
  49. end
  50. it 'logs the user in' do
  51. expect(controller.current_user).to be_instance_of(User)
  52. end
  53. end
  54. context 'when using an invalid password' do
  55. before do
  56. post :create, params: { user: { email: 'pam_user1', password: 'WRONGPW' } }
  57. end
  58. it 'shows a login error' do
  59. expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
  60. end
  61. it "doesn't log the user in" do
  62. expect(controller.current_user).to be_nil
  63. end
  64. end
  65. context 'when using a valid email and existing user' do
  66. let!(:user) do
  67. account = Fabricate.build(:account, username: 'pam_user1', user: nil)
  68. account.save!(validate: false)
  69. user = Fabricate(:user, email: 'pam@example.com', password: nil, account: account, external: true)
  70. user
  71. end
  72. before do
  73. post :create, params: { user: { email: user.email, password: '123456' } }
  74. end
  75. it 'redirects to home' do
  76. expect(response).to redirect_to(root_path)
  77. end
  78. it 'logs the user in' do
  79. expect(controller.current_user).to eq user
  80. end
  81. end
  82. end
  83. context 'when using password authentication' do
  84. let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') }
  85. context 'when using a valid password' do
  86. before do
  87. post :create, params: { user: { email: user.email, password: user.password } }
  88. end
  89. it 'redirects to home' do
  90. expect(response).to redirect_to(root_path)
  91. end
  92. it 'logs the user in' do
  93. expect(controller.current_user).to eq user
  94. end
  95. end
  96. context 'when using a valid password on a previously-used account with a new IP address' do
  97. let(:previous_ip) { '1.2.3.4' }
  98. let(:current_ip) { '4.3.2.1' }
  99. let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) }
  100. before do
  101. allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip)
  102. allow(UserMailer).to receive(:suspicious_sign_in)
  103. .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later!: nil))
  104. user.update(current_sign_in_at: 1.month.ago)
  105. post :create, params: { user: { email: user.email, password: user.password } }
  106. end
  107. it 'redirects to home' do
  108. expect(response).to redirect_to(root_path)
  109. end
  110. it 'logs the user in' do
  111. expect(controller.current_user).to eq user
  112. end
  113. it 'sends a suspicious sign-in mail' do
  114. expect(UserMailer).to have_received(:suspicious_sign_in).with(user, current_ip, anything, anything)
  115. end
  116. end
  117. context 'when using email with uppercase letters' do
  118. before do
  119. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  120. end
  121. it 'redirects to home' do
  122. expect(response).to redirect_to(root_path)
  123. end
  124. it 'logs the user in' do
  125. expect(controller.current_user).to eq user
  126. end
  127. end
  128. context 'when using an invalid password' do
  129. before do
  130. post :create, params: { user: { email: user.email, password: 'wrongpw' } }
  131. end
  132. it 'shows a login error' do
  133. expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
  134. end
  135. it "doesn't log the user in" do
  136. expect(controller.current_user).to be_nil
  137. end
  138. end
  139. context 'when using an unconfirmed password' do
  140. before do
  141. request.headers['Accept-Language'] = accept_language
  142. post :create, params: { user: { email: unconfirmed_user.email, password: unconfirmed_user.password } }
  143. end
  144. let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } }
  145. let(:accept_language) { 'fr' }
  146. it 'redirects to home' do
  147. expect(response).to redirect_to(root_path)
  148. end
  149. end
  150. context "when logging in from the user's page" do
  151. before do
  152. allow(controller).to receive(:single_user_mode?).and_return(single_user_mode)
  153. allow(controller).to receive(:stored_location_for).with(:user).and_return("/@#{user.account.username}")
  154. post :create, params: { user: { email: user.email, password: user.password } }
  155. end
  156. context 'with single user mode' do
  157. let(:single_user_mode) { true }
  158. it 'redirects to home' do
  159. expect(response).to redirect_to(root_path)
  160. end
  161. end
  162. context 'with non-single user mode' do
  163. let(:single_user_mode) { false }
  164. it "redirects back to the user's page" do
  165. expect(response).to redirect_to(short_account_path(username: user.account))
  166. end
  167. end
  168. end
  169. end
  170. context 'when using two-factor authentication' do
  171. context 'with OTP enabled as second factor' do
  172. let!(:user) do
  173. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  174. end
  175. let!(:recovery_codes) do
  176. codes = user.generate_otp_backup_codes!
  177. user.save
  178. return codes
  179. end
  180. context 'when using email and password' do
  181. before do
  182. post :create, params: { user: { email: user.email, password: user.password } }
  183. end
  184. it 'renders two factor authentication page' do
  185. expect(controller).to render_template('two_factor')
  186. expect(controller).to render_template(partial: '_otp_authentication_form')
  187. end
  188. end
  189. context 'when using email and password after an unfinished log-in attempt to a 2FA-protected account' do
  190. let!(:other_user) do
  191. Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  192. end
  193. before do
  194. post :create, params: { user: { email: other_user.email, password: other_user.password } }
  195. post :create, params: { user: { email: user.email, password: user.password } }
  196. end
  197. it 'renders two factor authentication page' do
  198. expect(controller).to render_template('two_factor')
  199. expect(controller).to render_template(partial: '_otp_authentication_form')
  200. end
  201. end
  202. context 'when using upcase email and password' do
  203. before do
  204. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  205. end
  206. it 'renders two factor authentication page' do
  207. expect(controller).to render_template('two_factor')
  208. expect(controller).to render_template(partial: '_otp_authentication_form')
  209. end
  210. end
  211. context 'when using a valid OTP' do
  212. before do
  213. post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  214. end
  215. it 'redirects to home' do
  216. expect(response).to redirect_to(root_path)
  217. end
  218. it 'logs the user in' do
  219. expect(controller.current_user).to eq user
  220. end
  221. end
  222. context 'when the server has an decryption error' do
  223. before do
  224. allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
  225. post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  226. end
  227. it 'shows a login error' do
  228. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  229. end
  230. it "doesn't log the user in" do
  231. expect(controller.current_user).to be_nil
  232. end
  233. end
  234. context 'when using a valid recovery code' do
  235. before do
  236. post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  237. end
  238. it 'redirects to home' do
  239. expect(response).to redirect_to(root_path)
  240. end
  241. it 'logs the user in' do
  242. expect(controller.current_user).to eq user
  243. end
  244. end
  245. context 'when using an invalid OTP' do
  246. before do
  247. post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  248. end
  249. it 'shows a login error' do
  250. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  251. end
  252. it "doesn't log the user in" do
  253. expect(controller.current_user).to be_nil
  254. end
  255. end
  256. end
  257. context 'with WebAuthn and OTP enabled as second factor' do
  258. let!(:user) do
  259. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  260. end
  261. let!(:recovery_codes) do
  262. codes = user.generate_otp_backup_codes!
  263. user.save
  264. return codes
  265. end
  266. let!(:webauthn_credential) do
  267. user.update(webauthn_id: WebAuthn.generate_user_id)
  268. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  269. user.webauthn_credentials.create(
  270. nickname: 'SecurityKeyNickname',
  271. external_id: public_key_credential.id,
  272. public_key: public_key_credential.public_key,
  273. sign_count: '1000'
  274. )
  275. user.webauthn_credentials.take
  276. end
  277. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" }
  278. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  279. let(:challenge) { WebAuthn::Credential.options_for_get.challenge }
  280. let(:sign_count) { 1234 }
  281. let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
  282. context 'when using email and password' do
  283. before do
  284. post :create, params: { user: { email: user.email, password: user.password } }
  285. end
  286. it 'renders webauthn authentication page' do
  287. expect(controller).to render_template('two_factor')
  288. expect(controller).to render_template(partial: '_webauthn_form')
  289. end
  290. end
  291. context 'when using upcase email and password' do
  292. before do
  293. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  294. end
  295. it 'renders webauthn authentication page' do
  296. expect(controller).to render_template('two_factor')
  297. expect(controller).to render_template(partial: '_webauthn_form')
  298. end
  299. end
  300. context 'when using a valid webauthn credential' do
  301. before do
  302. @controller.session[:webauthn_challenge] = challenge
  303. post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  304. end
  305. it 'instructs the browser to redirect to home' do
  306. expect(body_as_json[:redirect_path]).to eq(root_path)
  307. end
  308. it 'logs the user in' do
  309. expect(controller.current_user).to eq user
  310. end
  311. it 'updates the sign count' do
  312. expect(webauthn_credential.reload.sign_count).to eq(sign_count)
  313. end
  314. end
  315. end
  316. end
  317. end
  318. describe 'GET #webauthn_options' do
  319. context 'with WebAuthn and OTP enabled as second factor' do
  320. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" }
  321. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  322. let!(:user) do
  323. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  324. end
  325. before do
  326. user.update(webauthn_id: WebAuthn.generate_user_id)
  327. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  328. user.webauthn_credentials.create(
  329. nickname: 'SecurityKeyNickname',
  330. external_id: public_key_credential.id,
  331. public_key: public_key_credential.public_key,
  332. sign_count: '1000'
  333. )
  334. post :create, params: { user: { email: user.email, password: user.password } }
  335. end
  336. it 'returns http success' do
  337. get :webauthn_options
  338. expect(response).to have_http_status 200
  339. end
  340. end
  341. end
  342. end