sessions_controller_spec.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. require 'webauthn/fake_client'
  4. RSpec.describe Auth::SessionsController, type: :controller 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 'using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do
  43. context '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 '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 '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 'using password authentication' do
  84. let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') }
  85. context '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 '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).and_return(double('email', 'deliver_later!': nil))
  103. user.update(current_sign_in_at: 1.month.ago)
  104. post :create, params: { user: { email: user.email, password: user.password } }
  105. end
  106. it 'redirects to home' do
  107. expect(response).to redirect_to(root_path)
  108. end
  109. it 'logs the user in' do
  110. expect(controller.current_user).to eq user
  111. end
  112. it 'sends a suspicious sign-in mail' do
  113. expect(UserMailer).to have_received(:suspicious_sign_in).with(user, current_ip, anything, anything)
  114. end
  115. end
  116. context 'using email with uppercase letters' do
  117. before do
  118. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  119. end
  120. it 'redirects to home' do
  121. expect(response).to redirect_to(root_path)
  122. end
  123. it 'logs the user in' do
  124. expect(controller.current_user).to eq user
  125. end
  126. end
  127. context 'using an invalid password' do
  128. before do
  129. post :create, params: { user: { email: user.email, password: 'wrongpw' } }
  130. end
  131. it 'shows a login error' do
  132. expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
  133. end
  134. it "doesn't log the user in" do
  135. expect(controller.current_user).to be_nil
  136. end
  137. end
  138. context 'using an unconfirmed password' do
  139. before do
  140. request.headers['Accept-Language'] = accept_language
  141. post :create, params: { user: { email: unconfirmed_user.email, password: unconfirmed_user.password } }
  142. end
  143. let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } }
  144. let(:accept_language) { 'fr' }
  145. it 'redirects to home' do
  146. expect(response).to redirect_to(root_path)
  147. end
  148. end
  149. context "logging in from the user's page" do
  150. before do
  151. allow(controller).to receive(:single_user_mode?).and_return(single_user_mode)
  152. allow(controller).to receive(:stored_location_for).with(:user).and_return("/@#{user.account.username}")
  153. post :create, params: { user: { email: user.email, password: user.password } }
  154. end
  155. context "in single user mode" do
  156. let(:single_user_mode) { true }
  157. it 'redirects to home' do
  158. expect(response).to redirect_to(root_path)
  159. end
  160. end
  161. context "in non-single user mode" do
  162. let(:single_user_mode) { false }
  163. it "redirects back to the user's page" do
  164. expect(response).to redirect_to(short_account_path(username: user.account))
  165. end
  166. end
  167. end
  168. end
  169. context 'using two-factor authentication' do
  170. context 'with OTP enabled as second factor' do
  171. let!(:user) do
  172. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  173. end
  174. let!(:recovery_codes) do
  175. codes = user.generate_otp_backup_codes!
  176. user.save
  177. return codes
  178. end
  179. context 'using email and password' do
  180. before do
  181. post :create, params: { user: { email: user.email, password: user.password } }
  182. end
  183. it 'renders two factor authentication page' do
  184. expect(controller).to render_template("two_factor")
  185. expect(controller).to render_template(partial: "_otp_authentication_form")
  186. end
  187. end
  188. context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do
  189. let!(:other_user) do
  190. Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  191. end
  192. before do
  193. post :create, params: { user: { email: other_user.email, password: other_user.password } }
  194. post :create, params: { user: { email: user.email, password: user.password } }
  195. end
  196. it 'renders two factor authentication page' do
  197. expect(controller).to render_template("two_factor")
  198. expect(controller).to render_template(partial: "_otp_authentication_form")
  199. end
  200. end
  201. context 'using upcase email and password' do
  202. before do
  203. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  204. end
  205. it 'renders two factor authentication page' do
  206. expect(controller).to render_template("two_factor")
  207. expect(controller).to render_template(partial: "_otp_authentication_form")
  208. end
  209. end
  210. context 'using a valid OTP' do
  211. before do
  212. post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  213. end
  214. it 'redirects to home' do
  215. expect(response).to redirect_to(root_path)
  216. end
  217. it 'logs the user in' do
  218. expect(controller.current_user).to eq user
  219. end
  220. end
  221. context 'when the server has an decryption error' do
  222. before do
  223. allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
  224. post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  225. end
  226. it 'shows a login error' do
  227. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  228. end
  229. it "doesn't log the user in" do
  230. expect(controller.current_user).to be_nil
  231. end
  232. end
  233. context 'using a valid recovery code' do
  234. before do
  235. post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  236. end
  237. it 'redirects to home' do
  238. expect(response).to redirect_to(root_path)
  239. end
  240. it 'logs the user in' do
  241. expect(controller.current_user).to eq user
  242. end
  243. end
  244. context 'using an invalid OTP' do
  245. before do
  246. post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  247. end
  248. it 'shows a login error' do
  249. expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
  250. end
  251. it "doesn't log the user in" do
  252. expect(controller.current_user).to be_nil
  253. end
  254. end
  255. end
  256. context 'with WebAuthn and OTP enabled as second factor' do
  257. let!(:user) do
  258. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  259. end
  260. let!(:recovery_codes) do
  261. codes = user.generate_otp_backup_codes!
  262. user.save
  263. return codes
  264. end
  265. let!(:webauthn_credential) do
  266. user.update(webauthn_id: WebAuthn.generate_user_id)
  267. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  268. user.webauthn_credentials.create(
  269. nickname: 'SecurityKeyNickname',
  270. external_id: public_key_credential.id,
  271. public_key: public_key_credential.public_key,
  272. sign_count: '1000'
  273. )
  274. user.webauthn_credentials.take
  275. end
  276. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
  277. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  278. let(:challenge) { WebAuthn::Credential.options_for_get.challenge }
  279. let(:sign_count) { 1234 }
  280. let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
  281. context 'using email and password' do
  282. before do
  283. post :create, params: { user: { email: user.email, password: user.password } }
  284. end
  285. it 'renders webauthn authentication page' do
  286. expect(controller).to render_template("two_factor")
  287. expect(controller).to render_template(partial: "_webauthn_form")
  288. end
  289. end
  290. context 'using upcase email and password' do
  291. before do
  292. post :create, params: { user: { email: user.email.upcase, password: user.password } }
  293. end
  294. it 'renders webauthn authentication page' do
  295. expect(controller).to render_template("two_factor")
  296. expect(controller).to render_template(partial: "_webauthn_form")
  297. end
  298. end
  299. context 'using a valid webauthn credential' do
  300. before do
  301. @controller.session[:webauthn_challenge] = challenge
  302. post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
  303. end
  304. it 'instructs the browser to redirect to home' do
  305. expect(body_as_json[:redirect_path]).to eq(root_path)
  306. end
  307. it 'logs the user in' do
  308. expect(controller.current_user).to eq user
  309. end
  310. it 'updates the sign count' do
  311. expect(webauthn_credential.reload.sign_count).to eq(sign_count)
  312. end
  313. end
  314. end
  315. end
  316. end
  317. describe 'GET #webauthn_options' do
  318. context 'with WebAuthn and OTP enabled as second factor' do
  319. let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
  320. let(:fake_client) { WebAuthn::FakeClient.new(domain) }
  321. let!(:user) do
  322. Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
  323. end
  324. before do
  325. user.update(webauthn_id: WebAuthn.generate_user_id)
  326. public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
  327. user.webauthn_credentials.create(
  328. nickname: 'SecurityKeyNickname',
  329. external_id: public_key_credential.id,
  330. public_key: public_key_credential.public_key,
  331. sign_count: '1000'
  332. )
  333. post :create, params: { user: { email: user.email, password: user.password } }
  334. end
  335. it 'returns http success' do
  336. get :webauthn_options
  337. expect(response).to have_http_status :ok
  338. end
  339. end
  340. end
  341. end