Fix 2FA challenge and password challenge for non-database users (#11831)
* Fix 2FA challenge not appearing for non-database users Fix #11685 * Fix account deletion not working when using external login Fix #11691
This commit is contained in:
		| @@ -8,8 +8,6 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   skip_before_action :require_no_authentication, only: [:create] | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] | ||||
|  | ||||
|   before_action :set_instance_presenter, only: [:new] | ||||
|   before_action :set_body_classes | ||||
|  | ||||
| @@ -22,9 +20,22 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     super do |resource| | ||||
|       remember_me(resource) | ||||
|       flash.delete(:notice) | ||||
|     self.resource = begin | ||||
|       if user_params[:email].blank? && session[:otp_user_id].present? | ||||
|         User.find(session[:otp_user_id]) | ||||
|       else | ||||
|         warden.authenticate!(auth_options) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     if resource.otp_required_for_login? | ||||
|       if user_params[:otp_attempt].present? && session[:otp_user_id].present? | ||||
|         authenticate_with_two_factor_via_otp(resource) | ||||
|       else | ||||
|         prompt_for_two_factor(resource) | ||||
|       end | ||||
|     else | ||||
|       authenticate_and_respond(resource) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -37,18 +48,6 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|  | ||||
|   protected | ||||
|  | ||||
|   def find_user | ||||
|     if session[:otp_user_id] | ||||
|       User.find(session[:otp_user_id]) | ||||
|     elsif user_params[:email] | ||||
|       if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? | ||||
|         User.joins(:account).find_by(accounts: { username: user_params[:email] }) | ||||
|       else | ||||
|         User.find_for_authentication(email: user_params[:email]) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def user_params | ||||
|     params.require(:user).permit(:email, :password, :otp_attempt) | ||||
|   end | ||||
| @@ -71,32 +70,17 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|     super | ||||
|   end | ||||
|  | ||||
|   def two_factor_enabled? | ||||
|     find_user.try(:otp_required_for_login?) | ||||
|   end | ||||
|  | ||||
|   def valid_otp_attempt?(user) | ||||
|     user.validate_and_consume_otp!(user_params[:otp_attempt]) || | ||||
|       user.invalidate_otp_backup_code!(user_params[:otp_attempt]) | ||||
|   rescue OpenSSL::Cipher::CipherError => _error | ||||
|   rescue OpenSSL::Cipher::CipherError | ||||
|     false | ||||
|   end | ||||
|  | ||||
|   def authenticate_with_two_factor | ||||
|     user = self.resource = find_user | ||||
|  | ||||
|     if user_params[:otp_attempt].present? && session[:otp_user_id] | ||||
|       authenticate_with_two_factor_via_otp(user) | ||||
|     elsif user&.valid_password?(user_params[:password]) | ||||
|       prompt_for_two_factor(user) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def authenticate_with_two_factor_via_otp(user) | ||||
|     if valid_otp_attempt?(user) | ||||
|       session.delete(:otp_user_id) | ||||
|       remember_me(user) | ||||
|       sign_in(user) | ||||
|       authenticate_and_respond(user) | ||||
|     else | ||||
|       flash.now[:alert] = I18n.t('users.invalid_otp_token') | ||||
|       prompt_for_two_factor(user) | ||||
| @@ -108,6 +92,13 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|     render :two_factor | ||||
|   end | ||||
|  | ||||
|   def authenticate_and_respond(user) | ||||
|     sign_in(user) | ||||
|     remember_me(user) | ||||
|  | ||||
|     respond_with user, location: after_sign_in_path_for(user) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_instance_presenter | ||||
| @@ -120,9 +111,11 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|  | ||||
|   def home_paths(resource) | ||||
|     paths = [about_path] | ||||
|  | ||||
|     if single_user_mode? && resource.is_a?(User) | ||||
|       paths << short_account_path(username: resource.account) | ||||
|     end | ||||
|  | ||||
|     paths | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -14,12 +14,11 @@ class Settings::DeletesController < Settings::BaseController | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     if current_user.valid_password?(delete_params[:password]) | ||||
|       Admin::SuspensionWorker.perform_async(current_user.account_id, true) | ||||
|       sign_out | ||||
|     if challenge_passed? | ||||
|       destroy_account! | ||||
|       redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg') | ||||
|     else | ||||
|       redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg') | ||||
|       redirect_to settings_delete_path, alert: I18n.t('deletes.challenge_not_passed') | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -29,11 +28,25 @@ class Settings::DeletesController < Settings::BaseController | ||||
|     redirect_to root_path unless Setting.open_deletion | ||||
|   end | ||||
|  | ||||
|   def delete_params | ||||
|     params.require(:form_delete_confirmation).permit(:password) | ||||
|   def resource_params | ||||
|     params.require(:form_delete_confirmation).permit(:password, :username) | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
|  | ||||
|   def challenge_passed? | ||||
|     if current_user.encrypted_password.blank? | ||||
|       current_account.username == resource_params[:username] | ||||
|     else | ||||
|       current_user.valid_password?(resource_params[:password]) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy_account! | ||||
|     current_account.suspend! | ||||
|     Admin::SuspensionWorker.perform_async(current_user.account_id, true) | ||||
|     sign_out | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,5 +3,5 @@ | ||||
| class Form::DeleteConfirmation | ||||
|   include ActiveModel::Model | ||||
|  | ||||
|   attr_accessor :password | ||||
|   attr_accessor :password, :username | ||||
| end | ||||
|   | ||||
| @@ -20,7 +20,10 @@ | ||||
|  | ||||
|   %hr.spacer/ | ||||
|  | ||||
|   = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password') | ||||
|   - if current_user.encrypted_password.present? | ||||
|     = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password') | ||||
|   - else | ||||
|     = f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username') | ||||
|  | ||||
|   .actions | ||||
|     = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative' | ||||
|   | ||||
| @@ -71,10 +71,13 @@ end | ||||
|  | ||||
| Devise.setup do |config| | ||||
|   config.warden do |manager| | ||||
|     manager.default_strategies(scope: :user).unshift :database_authenticatable | ||||
|     manager.default_strategies(scope: :user).unshift :ldap_authenticatable if Devise.ldap_authentication | ||||
|     manager.default_strategies(scope: :user).unshift :pam_authenticatable  if Devise.pam_authentication | ||||
|     manager.default_strategies(scope: :user).unshift :two_factor_authenticatable | ||||
|     manager.default_strategies(scope: :user).unshift :two_factor_backupable | ||||
|  | ||||
|     # We handle 2FA in our own sessions controller so this gets in the way | ||||
|     manager.default_strategies(scope: :user).delete :two_factor_backupable | ||||
|     manager.default_strategies(scope: :user).delete :two_factor_authenticatable | ||||
|   end | ||||
|  | ||||
|   # The secret key used by Devise. Devise uses this key to generate | ||||
|   | ||||
| @@ -632,8 +632,9 @@ en: | ||||
|       x_months: "%{count}mo" | ||||
|       x_seconds: "%{count}s" | ||||
|   deletes: | ||||
|     bad_password_msg: The password you entered was incorrect | ||||
|     challenge_not_passed: The information you entered was not correct | ||||
|     confirm_password: Enter your current password to verify your identity | ||||
|     confirm_username: Enter your username to confirm the procedure | ||||
|     proceed: Delete account | ||||
|     success_msg: Your account was successfully deleted | ||||
|     warning: | ||||
|   | ||||
| @@ -5,11 +5,11 @@ require 'rails_helper' | ||||
| RSpec.describe Auth::SessionsController, type: :controller do | ||||
|   render_views | ||||
|  | ||||
|   describe 'GET #new' do | ||||
|     before do | ||||
|       request.env['devise.mapping'] = Devise.mappings[:user] | ||||
|     end | ||||
|   before do | ||||
|     request.env['devise.mapping'] = Devise.mappings[:user] | ||||
|   end | ||||
|  | ||||
|   describe 'GET #new' do | ||||
|     it 'returns http success' do | ||||
|       get :new | ||||
|       expect(response).to have_http_status(200) | ||||
| @@ -19,10 +19,6 @@ RSpec.describe Auth::SessionsController, type: :controller do | ||||
|   describe 'DELETE #destroy' do | ||||
|     let(:user) { Fabricate(:user) } | ||||
|  | ||||
|     before do | ||||
|       request.env['devise.mapping'] = Devise.mappings[:user] | ||||
|     end | ||||
|  | ||||
|     context 'with a regular user' do | ||||
|       it 'redirects to home after sign out' do | ||||
|         sign_in(user, scope: :user) | ||||
| @@ -51,10 +47,6 @@ RSpec.describe Auth::SessionsController, type: :controller do | ||||
|   end | ||||
|  | ||||
|   describe 'POST #create' do | ||||
|     before do | ||||
|       request.env['devise.mapping'] = Devise.mappings[:user] | ||||
|     end | ||||
|  | ||||
|     context 'using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do | ||||
|       context 'using a valid password' do | ||||
|         before do | ||||
| @@ -191,11 +183,11 @@ RSpec.describe Auth::SessionsController, type: :controller do | ||||
|     end | ||||
|  | ||||
|     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)) | ||||
|       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 | ||||
|  | ||||
|       let!(:recovery_codes) do | ||||
|         codes = user.generate_otp_backup_codes! | ||||
|         user.save | ||||
|         return codes | ||||
|   | ||||
		Reference in New Issue
	
	Block a user