From df4ff9a8e13d776e1670c232655db0275a353a0f Mon Sep 17 00:00:00 2001 From: Patrick Figel Date: Sat, 15 Apr 2017 13:26:03 +0200 Subject: Add recovery code support for two-factor auth (#1773) * Add recovery code support for two-factor auth When users enable two-factor auth, the app now generates ten single-use recovery codes. Users are encouraged to print the codes and store them in a safe place. The two-factor prompt during login now accepts both OTP codes and recovery codes. The two-factor settings UI allows users to regenerated lost recovery codes. Users who have set up two-factor auth prior to this feature being added can use it to generate recovery codes for the first time. Fixes #563 and fixes #987 * Set OTP_SECRET in test enviroment * add missing .html to view file names --- spec/controllers/auth/sessions_controller_spec.rb | 93 ++++++++++++++++++++--- 1 file changed, 84 insertions(+), 9 deletions(-) (limited to 'spec/controllers/auth') diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index c2a1fbe91..0ec0b8f2f 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Auth::SessionsController, type: :controller do describe 'GET #new' do before do - request.env["devise.mapping"] = Devise.mappings[:user] + request.env['devise.mapping'] = Devise.mappings[:user] end it 'returns http success' do @@ -15,19 +15,94 @@ RSpec.describe Auth::SessionsController, type: :controller do end describe 'POST #create' do - let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') } - before do - request.env["devise.mapping"] = Devise.mappings[:user] - post :create, params: { user: { email: user.email, password: user.password } } + request.env['devise.mapping'] = Devise.mappings[:user] end - it 'redirects to home' do - expect(response).to redirect_to(root_path) + context 'using password authentication' do + let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') } + + context 'using a valid password' do + before do + post :create, params: { user: { email: user.email, password: user.password } } + end + + it 'redirects to home' do + expect(response).to redirect_to(root_path) + end + + it 'logs the user in' do + expect(controller.current_user).to eq user + end + end + + context 'using an invalid password' do + before do + post :create, params: { user: { email: user.email, password: 'wrongpw' } } + end + + it 'shows a login error' do + expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: 'Email') + end + + it "doesn't log the user in" do + expect(controller.current_user).to be_nil + end + end end - it 'logs the user in' do - expect(controller.current_user).to eq user + context 'using two-factor authentication' do + let(:user) do + Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', + otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + end + let(:recovery_codes) do + codes = user.generate_otp_backup_codes! + user.save + return codes + end + + context 'using a valid OTP' do + before do + post :create, params: { user: { otp_attempt: user.current_otp } }, session: { otp_user_id: user.id } + end + + it 'redirects to home' do + expect(response).to redirect_to(root_path) + end + + it 'logs the user in' do + expect(controller.current_user).to eq user + end + end + + context 'using a valid recovery code' do + before do + post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { otp_user_id: user.id } + end + + it 'redirects to home' do + expect(response).to redirect_to(root_path) + end + + it 'logs the user in' do + expect(controller.current_user).to eq user + end + end + + context 'using an invalid OTP' do + before do + post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { otp_user_id: user.id } + end + + it 'shows a login error' do + expect(flash[:alert]).to match I18n.t('users.invalid_otp_token') + end + + it "doesn't log the user in" do + expect(controller.current_user).to be_nil + end + end end end end -- cgit