sessions_controller_spec.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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(controller.request).to receive(:remote_ip).and_return(current_ip)
  102. user.update(current_sign_in_at: 1.month.ago)
  103. post :create, params: { user: { email: user.email, password: user.password } }
  104. end
  105. it 'redirects to home' do
  106. expect(response).to redirect_to(root_path)
  107. end
  108. it 'logs the user in' do
  109. expect(controller.current_user).to eq user
  110. end
  111. it 'sends a suspicious sign-in mail' do
  112. expect(UserMailer.deliveries.size).to eq(1)
  113. expect(UserMailer.deliveries.first.to.first).to eq(user.email)
  114. expect(UserMailer.deliveries.first.subject).to eq(I18n.t('user_mailer.suspicious_sign_in.subject'))
  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(user).to receive(:validate_and_consume_otp!).with(user.current_otp).and_raise(OpenSSL::Cipher::CipherError)
  225. allow(User).to receive(:find_by).with(id: user.id).and_return(user)
  226. post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  227. end
  228. it 'shows a login error' do
  229. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  230. end
  231. it "doesn't log the user in" do
  232. expect(controller.current_user).to be_nil
  233. end
  234. end
  235. context 'when using a valid recovery code' do
  236. before do
  237. post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  238. end
  239. it 'redirects to home' do
  240. expect(response).to redirect_to(root_path)
  241. end
  242. it 'logs the user in' do
  243. expect(controller.current_user).to eq user
  244. end
  245. end
  246. context 'when using an invalid OTP' do
  247. before do
  248. post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  249. end
  250. it 'shows a login error' do
  251. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  252. end
  253. it "doesn't log the user in" do
  254. expect(controller.current_user).to be_nil
  255. end
  256. end
  257. end
  258. context 'with WebAuthn and OTP enabled as second factor' do
  259. let!(:user) do
  260. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  261. end
  262. let!(:recovery_codes) do
  263. codes = user.generate_otp_backup_codes!
  264. user.save
  265. return codes
  266. end
  267. let!(:webauthn_credential) do
  268. user.update(webauthn_id: WebAuthn.generate_user_id)
  269. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  270. user.webauthn_credentials.create(
  271. nickname: 'SecurityKeyNickname',
  272. external_id: public_key_credential.id,
  273. public_key: public_key_credential.public_key,
  274. sign_count: '1000'
  275. )
  276. user.webauthn_credentials.take
  277. end
  278. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" }
  279. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  280. let(:challenge) { WebAuthn::Credential.options_for_get.challenge }
  281. let(:sign_count) { 1234 }
  282. let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
  283. context 'when using email and password' do
  284. before do
  285. post :create, params: { user: { email: user.email, password: user.password } }
  286. end
  287. it 'renders webauthn authentication page' do
  288. expect(controller).to render_template('two_factor')
  289. expect(controller).to render_template(partial: '_webauthn_form')
  290. end
  291. end
  292. context 'when using upcase email and password' do
  293. before do
  294. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  295. end
  296. it 'renders webauthn authentication page' do
  297. expect(controller).to render_template('two_factor')
  298. expect(controller).to render_template(partial: '_webauthn_form')
  299. end
  300. end
  301. context 'when using a valid webauthn credential' do
  302. before do
  303. controller.session[:webauthn_challenge] = challenge
  304. post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  305. end
  306. it 'instructs the browser to redirect to home' do
  307. expect(body_as_json[:redirect_path]).to eq(root_path)
  308. end
  309. it 'logs the user in' do
  310. expect(controller.current_user).to eq user
  311. end
  312. it 'updates the sign count' do
  313. expect(webauthn_credential.reload.sign_count).to eq(sign_count)
  314. end
  315. end
  316. end
  317. end
  318. end
  319. describe 'GET #webauthn_options' do
  320. context 'with WebAuthn and OTP enabled as second factor' do
  321. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" }
  322. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  323. let!(:user) do
  324. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  325. end
  326. before do
  327. user.update(webauthn_id: WebAuthn.generate_user_id)
  328. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  329. user.webauthn_credentials.create(
  330. nickname: 'SecurityKeyNickname',
  331. external_id: public_key_credential.id,
  332. public_key: public_key_credential.public_key,
  333. sign_count: '1000'
  334. )
  335. post :create, params: { user: { email: user.email, password: user.password } }
  336. end
  337. it 'returns http success' do
  338. get :webauthn_options
  339. expect(response).to have_http_status 200
  340. end
  341. end
  342. end
  343. end