Add password challenge to 2FA settings, e-mail notifications (#11878)
Fix #3961
This commit is contained in:
		
							parent
							
								
									d0c2c52783
								
							
						
					
					
						commit
						e1066cd431
					
				
					 32 changed files with 567 additions and 50 deletions
				
			
		| 
						 | 
					@ -8,6 +8,7 @@ module Admin
 | 
				
			||||||
      authorize @user, :disable_2fa?
 | 
					      authorize @user, :disable_2fa?
 | 
				
			||||||
      @user.disable_two_factor!
 | 
					      @user.disable_two_factor!
 | 
				
			||||||
      log_action :disable_2fa, @user
 | 
					      log_action :disable_2fa, @user
 | 
				
			||||||
 | 
					      UserMailer.two_factor_disabled(@user).deliver_later!
 | 
				
			||||||
      redirect_to admin_accounts_path
 | 
					      redirect_to admin_accounts_path
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										22
									
								
								app/controllers/auth/challenges_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/controllers/auth/challenges_controller.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Auth::ChallengesController < ApplicationController
 | 
				
			||||||
 | 
					  include ChallengableConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  layout 'auth'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  skip_before_action :require_functional!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    if challenge_passed?
 | 
				
			||||||
 | 
					      session[:challenge_passed_at] = Time.now.utc
 | 
				
			||||||
 | 
					      redirect_to challenge_params[:return_to]
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      @challenge = Form::Challenge.new(return_to: challenge_params[:return_to])
 | 
				
			||||||
 | 
					      flash.now[:alert] = I18n.t('challenge.invalid_password')
 | 
				
			||||||
 | 
					      render_challenge
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,7 @@ class Auth::SessionsController < Devise::SessionsController
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
    tmp_stored_location = stored_location_for(:user)
 | 
					    tmp_stored_location = stored_location_for(:user)
 | 
				
			||||||
    super
 | 
					    super
 | 
				
			||||||
 | 
					    session.delete(:challenge_passed_at)
 | 
				
			||||||
    flash.delete(:notice)
 | 
					    flash.delete(:notice)
 | 
				
			||||||
    store_location_for(:user, tmp_stored_location) if continue_after?
 | 
					    store_location_for(:user, tmp_stored_location) if continue_after?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										65
									
								
								app/controllers/concerns/challengable_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/controllers/concerns/challengable_concern.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This concern is inspired by "sudo mode" on GitHub. It
 | 
				
			||||||
 | 
					# is a way to re-authenticate a user before allowing them
 | 
				
			||||||
 | 
					# to see or perform an action.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Add `before_action :require_challenge!` to actions you
 | 
				
			||||||
 | 
					# want to protect.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# The user will be shown a page to enter the challenge (which
 | 
				
			||||||
 | 
					# is either the password, or just the username when no
 | 
				
			||||||
 | 
					# password exists). Upon passing, there is a grace period
 | 
				
			||||||
 | 
					# during which no challenge will be asked from the user.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Accessing challenge-protected resources during the grace
 | 
				
			||||||
 | 
					# period will refresh the grace period.
 | 
				
			||||||
 | 
					module ChallengableConcern
 | 
				
			||||||
 | 
					  extend ActiveSupport::Concern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CHALLENGE_TIMEOUT = 1.hour.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def require_challenge!
 | 
				
			||||||
 | 
					    return if skip_challenge?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if challenge_passed_recently?
 | 
				
			||||||
 | 
					      session[:challenge_passed_at] = Time.now.utc
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @challenge = Form::Challenge.new(return_to: request.url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if params.key?(:form_challenge)
 | 
				
			||||||
 | 
					      if challenge_passed?
 | 
				
			||||||
 | 
					        session[:challenge_passed_at] = Time.now.utc
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        flash.now[:alert] = I18n.t('challenge.invalid_password')
 | 
				
			||||||
 | 
					        render_challenge
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render_challenge
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def render_challenge
 | 
				
			||||||
 | 
					    @body_classes = 'lighter'
 | 
				
			||||||
 | 
					    render template: 'auth/challenges/new', layout: 'auth'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def challenge_passed?
 | 
				
			||||||
 | 
					    current_user.valid_password?(challenge_params[:current_password])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def skip_challenge?
 | 
				
			||||||
 | 
					    current_user.encrypted_password.blank?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def challenge_passed_recently?
 | 
				
			||||||
 | 
					    session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def challenge_params
 | 
				
			||||||
 | 
					    params.require(:form_challenge).permit(:current_password, :return_to)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -3,9 +3,12 @@
 | 
				
			||||||
module Settings
 | 
					module Settings
 | 
				
			||||||
  module TwoFactorAuthentication
 | 
					  module TwoFactorAuthentication
 | 
				
			||||||
    class ConfirmationsController < BaseController
 | 
					    class ConfirmationsController < BaseController
 | 
				
			||||||
 | 
					      include ChallengableConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      layout 'admin'
 | 
					      layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      before_action :authenticate_user!
 | 
					      before_action :authenticate_user!
 | 
				
			||||||
 | 
					      before_action :require_challenge!
 | 
				
			||||||
      before_action :ensure_otp_secret
 | 
					      before_action :ensure_otp_secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      skip_before_action :require_functional!
 | 
					      skip_before_action :require_functional!
 | 
				
			||||||
| 
						 | 
					@ -22,6 +25,8 @@ module Settings
 | 
				
			||||||
          @recovery_codes = current_user.generate_otp_backup_codes!
 | 
					          @recovery_codes = current_user.generate_otp_backup_codes!
 | 
				
			||||||
          current_user.save!
 | 
					          current_user.save!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          UserMailer.two_factor_enabled(current_user).deliver_later!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          render 'settings/two_factor_authentication/recovery_codes/index'
 | 
					          render 'settings/two_factor_authentication/recovery_codes/index'
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
          flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
 | 
					          flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,16 +3,22 @@
 | 
				
			||||||
module Settings
 | 
					module Settings
 | 
				
			||||||
  module TwoFactorAuthentication
 | 
					  module TwoFactorAuthentication
 | 
				
			||||||
    class RecoveryCodesController < BaseController
 | 
					    class RecoveryCodesController < BaseController
 | 
				
			||||||
 | 
					      include ChallengableConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      layout 'admin'
 | 
					      layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      before_action :authenticate_user!
 | 
					      before_action :authenticate_user!
 | 
				
			||||||
 | 
					      before_action :require_challenge!, on: :create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      skip_before_action :require_functional!
 | 
					      skip_before_action :require_functional!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      def create
 | 
					      def create
 | 
				
			||||||
        @recovery_codes = current_user.generate_otp_backup_codes!
 | 
					        @recovery_codes = current_user.generate_otp_backup_codes!
 | 
				
			||||||
        current_user.save!
 | 
					        current_user.save!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
 | 
				
			||||||
        flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
 | 
					        flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        render :index
 | 
					        render :index
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,13 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Settings
 | 
					module Settings
 | 
				
			||||||
  class TwoFactorAuthenticationsController < BaseController
 | 
					  class TwoFactorAuthenticationsController < BaseController
 | 
				
			||||||
 | 
					    include ChallengableConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    layout 'admin'
 | 
					    layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    before_action :authenticate_user!
 | 
					    before_action :authenticate_user!
 | 
				
			||||||
    before_action :verify_otp_required, only: [:create]
 | 
					    before_action :verify_otp_required, only: [:create]
 | 
				
			||||||
 | 
					    before_action :require_challenge!, only: [:create]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    skip_before_action :require_functional!
 | 
					    skip_before_action :require_functional!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +26,7 @@ module Settings
 | 
				
			||||||
      if acceptable_code?
 | 
					      if acceptable_code?
 | 
				
			||||||
        current_user.otp_required_for_login = false
 | 
					        current_user.otp_required_for_login = false
 | 
				
			||||||
        current_user.save!
 | 
					        current_user.save!
 | 
				
			||||||
 | 
					        UserMailer.two_factor_disabled(current_user).deliver_later!
 | 
				
			||||||
        redirect_to settings_two_factor_authentication_path
 | 
					        redirect_to settings_two_factor_authentication_path
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
 | 
					        flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,32 +233,35 @@ hr.spacer {
 | 
				
			||||||
  height: 1px;
 | 
					  height: 1px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.muted-hint {
 | 
					body,
 | 
				
			||||||
 | 
					.admin-wrapper .content {
 | 
				
			||||||
 | 
					  .muted-hint {
 | 
				
			||||||
    color: $darker-text-color;
 | 
					    color: $darker-text-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    a {
 | 
					    a {
 | 
				
			||||||
      color: $highlight-text-color;
 | 
					      color: $highlight-text-color;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.positive-hint {
 | 
					  .positive-hint {
 | 
				
			||||||
    color: $valid-value-color;
 | 
					    color: $valid-value-color;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.negative-hint {
 | 
					  .negative-hint {
 | 
				
			||||||
    color: $error-value-color;
 | 
					    color: $error-value-color;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.neutral-hint {
 | 
					  .neutral-hint {
 | 
				
			||||||
    color: $dark-text-color;
 | 
					    color: $dark-text-color;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.warning-hint {
 | 
					  .warning-hint {
 | 
				
			||||||
    color: $gold-star;
 | 
					    color: $gold-star;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.filters {
 | 
					.filters {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -254,6 +254,10 @@ code {
 | 
				
			||||||
      &-6 {
 | 
					      &-6 {
 | 
				
			||||||
        max-width: 50%;
 | 
					        max-width: 50%;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .actions {
 | 
				
			||||||
 | 
					        margin-top: 27px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .fields-group:last-child,
 | 
					    .fields-group:last-child,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,39 @@ class UserMailer < Devise::Mailer
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def two_factor_enabled(user, **)
 | 
				
			||||||
 | 
					    @resource = user
 | 
				
			||||||
 | 
					    @instance = Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return if @resource.disabled?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    I18n.with_locale(@resource.locale || I18n.default_locale) do
 | 
				
			||||||
 | 
					      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def two_factor_disabled(user, **)
 | 
				
			||||||
 | 
					    @resource = user
 | 
				
			||||||
 | 
					    @instance = Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return if @resource.disabled?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    I18n.with_locale(@resource.locale || I18n.default_locale) do
 | 
				
			||||||
 | 
					      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def two_factor_recovery_codes_changed(user, **)
 | 
				
			||||||
 | 
					    @resource = user
 | 
				
			||||||
 | 
					    @instance = Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return if @resource.disabled?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    I18n.with_locale(@resource.locale || I18n.default_locale) do
 | 
				
			||||||
 | 
					      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def welcome(user)
 | 
					  def welcome(user)
 | 
				
			||||||
    @resource = user
 | 
					    @resource = user
 | 
				
			||||||
    @instance = Rails.configuration.x.local_domain
 | 
					    @instance = Rails.configuration.x.local_domain
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/models/form/challenge.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/models/form/challenge.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Form::Challenge
 | 
				
			||||||
 | 
					  include ActiveModel::Model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr_accessor :current_password, :current_username,
 | 
				
			||||||
 | 
					                :return_to
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -264,17 +264,20 @@ class User < ApplicationRecord
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def password_required?
 | 
					  def password_required?
 | 
				
			||||||
    return false if Devise.pam_authentication || Devise.ldap_authentication
 | 
					    return false if external?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super
 | 
					    super
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def send_reset_password_instructions
 | 
					  def send_reset_password_instructions
 | 
				
			||||||
    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
 | 
					    return false if encrypted_password.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super
 | 
					    super
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def reset_password!(new_password, new_password_confirmation)
 | 
					  def reset_password!(new_password, new_password_confirmation)
 | 
				
			||||||
    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
 | 
					    return false if encrypted_password.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super
 | 
					    super
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								app/views/auth/challenges/new.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/views/auth/challenges/new.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('challenge.prompt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= simple_form_for @challenge, url: request.get? ? auth_challenge_path : '' do |f|
 | 
				
			||||||
 | 
					  = f.input :return_to, as: :hidden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .field-group
 | 
				
			||||||
 | 
					    = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off', :autofocus => true }, label: t('challenge.prompt'), required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .actions
 | 
				
			||||||
 | 
					    = f.button :button, t('challenge.confirm'), type: :submit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %p.hint.subtle-hint= t('challenge.hint_html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-footer= render 'auth/shared/links'
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@
 | 
				
			||||||
    - if controller_name != 'passwords' && controller_name != 'registrations'
 | 
					    - if controller_name != 'passwords' && controller_name != 'registrations'
 | 
				
			||||||
      %li= link_to t('auth.forgot_password'), new_user_password_path
 | 
					      %li= link_to t('auth.forgot_password'), new_user_password_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - if controller_name != 'confirmations'
 | 
					  - if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
 | 
				
			||||||
    %li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
 | 
					    %li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - if user_signed_in? && controller_name != 'setup'
 | 
					  - if user_signed_in? && controller_name != 'setup'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,33 +2,35 @@
 | 
				
			||||||
  = t('settings.two_factor_authentication')
 | 
					  = t('settings.two_factor_authentication')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- if current_user.otp_required_for_login
 | 
					- if current_user.otp_required_for_login
 | 
				
			||||||
  %p.positive-hint
 | 
					  %p.hint
 | 
				
			||||||
 | 
					    %span.positive-hint
 | 
				
			||||||
      = fa_icon 'check'
 | 
					      = fa_icon 'check'
 | 
				
			||||||
      = ' '
 | 
					      = ' '
 | 
				
			||||||
      = t 'two_factor_authentication.enabled'
 | 
					      = t 'two_factor_authentication.enabled'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %hr/
 | 
					  %hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  = simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
 | 
					  = simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
 | 
				
			||||||
    = f.input :otp_attempt, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
 | 
					    .fields-group
 | 
				
			||||||
 | 
					      = f.input :otp_attempt, wrapper: :with_block_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .actions
 | 
					    .actions
 | 
				
			||||||
      = f.button :button, t('two_factor_authentication.disable'), type: :submit
 | 
					      = f.button :button, t('two_factor_authentication.disable'), type: :submit, class: 'negative'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %hr/
 | 
					  %hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %h6= t('two_factor_authentication.recovery_codes')
 | 
					  %h3= t('two_factor_authentication.recovery_codes')
 | 
				
			||||||
  %p.muted-hint
 | 
					  %p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
 | 
				
			||||||
    = t('two_factor_authentication.lost_recovery_codes')
 | 
					
 | 
				
			||||||
    = link_to t('two_factor_authentication.generate_recovery_codes'),
 | 
					  %hr.spacer/
 | 
				
			||||||
      settings_two_factor_authentication_recovery_codes_path,
 | 
					
 | 
				
			||||||
      data: { method: :post }
 | 
					  .simple_form
 | 
				
			||||||
 | 
					    = link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- else
 | 
					- else
 | 
				
			||||||
  .simple_form
 | 
					  .simple_form
 | 
				
			||||||
    %p.hint= t('two_factor_authentication.description_html')
 | 
					    %p.hint= t('two_factor_authentication.description_html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    = link_to t('two_factor_authentication.setup'),
 | 
					    %hr.spacer/
 | 
				
			||||||
      settings_two_factor_authentication_path,
 | 
					
 | 
				
			||||||
      data: { method: :post },
 | 
					    = link_to t('two_factor_authentication.setup'), settings_two_factor_authentication_path, data: { method: :post }, class: 'block-button'
 | 
				
			||||||
      class: 'block-button'
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										43
									
								
								app/views/user_mailer/two_factor_disabled.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/views/user_mailer/two_factor_disabled.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.hero
 | 
				
			||||||
 | 
					                  .email-row
 | 
				
			||||||
 | 
					                    .col-6
 | 
				
			||||||
 | 
					                      %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                        %tbody
 | 
				
			||||||
 | 
					                          %tr
 | 
				
			||||||
 | 
					                            %td.column-cell.text-center.padded
 | 
				
			||||||
 | 
					                              %table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                                %tbody
 | 
				
			||||||
 | 
					                                  %tr
 | 
				
			||||||
 | 
					                                    %td
 | 
				
			||||||
 | 
					                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                              %h1= t 'devise.mailer.two_factor_disabled.title'
 | 
				
			||||||
 | 
					                              %p.lead= t 'devise.mailer.two_factor_disabled.explanation'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.content-start
 | 
				
			||||||
 | 
					                  %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                    %tbody
 | 
				
			||||||
 | 
					                      %tr
 | 
				
			||||||
 | 
					                        %td.column-cell.button-cell
 | 
				
			||||||
 | 
					                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                            %tbody
 | 
				
			||||||
 | 
					                              %tr
 | 
				
			||||||
 | 
					                                %td.button-primary
 | 
				
			||||||
 | 
					                                  = link_to edit_user_registration_url do
 | 
				
			||||||
 | 
					                                    %span= t('settings.account_settings')
 | 
				
			||||||
							
								
								
									
										7
									
								
								app/views/user_mailer/two_factor_disabled.text.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/views/user_mailer/two_factor_disabled.text.erb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_disabled.title' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_disabled.explanation' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=> <%= edit_user_registration_url %>
 | 
				
			||||||
							
								
								
									
										43
									
								
								app/views/user_mailer/two_factor_enabled.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/views/user_mailer/two_factor_enabled.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.hero
 | 
				
			||||||
 | 
					                  .email-row
 | 
				
			||||||
 | 
					                    .col-6
 | 
				
			||||||
 | 
					                      %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                        %tbody
 | 
				
			||||||
 | 
					                          %tr
 | 
				
			||||||
 | 
					                            %td.column-cell.text-center.padded
 | 
				
			||||||
 | 
					                              %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                                %tbody
 | 
				
			||||||
 | 
					                                  %tr
 | 
				
			||||||
 | 
					                                    %td
 | 
				
			||||||
 | 
					                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                              %h1= t 'devise.mailer.two_factor_enabled.title'
 | 
				
			||||||
 | 
					                              %p.lead= t 'devise.mailer.two_factor_enabled.explanation'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.content-start
 | 
				
			||||||
 | 
					                  %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                    %tbody
 | 
				
			||||||
 | 
					                      %tr
 | 
				
			||||||
 | 
					                        %td.column-cell.button-cell
 | 
				
			||||||
 | 
					                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                            %tbody
 | 
				
			||||||
 | 
					                              %tr
 | 
				
			||||||
 | 
					                                %td.button-primary
 | 
				
			||||||
 | 
					                                  = link_to edit_user_registration_url do
 | 
				
			||||||
 | 
					                                    %span= t('settings.account_settings')
 | 
				
			||||||
							
								
								
									
										7
									
								
								app/views/user_mailer/two_factor_enabled.text.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/views/user_mailer/two_factor_enabled.text.erb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_enabled.title' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_enabled.explanation' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=> <%= edit_user_registration_url %>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.hero
 | 
				
			||||||
 | 
					                  .email-row
 | 
				
			||||||
 | 
					                    .col-6
 | 
				
			||||||
 | 
					                      %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                        %tbody
 | 
				
			||||||
 | 
					                          %tr
 | 
				
			||||||
 | 
					                            %td.column-cell.text-center.padded
 | 
				
			||||||
 | 
					                              %table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                                %tbody
 | 
				
			||||||
 | 
					                                  %tr
 | 
				
			||||||
 | 
					                                    %td
 | 
				
			||||||
 | 
					                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                              %h1= t 'devise.mailer.two_factor_recovery_codes_changed.title'
 | 
				
			||||||
 | 
					                              %p.lead= t 'devise.mailer.two_factor_recovery_codes_changed.explanation'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					  %tbody
 | 
				
			||||||
 | 
					    %tr
 | 
				
			||||||
 | 
					      %td.email-body
 | 
				
			||||||
 | 
					        .email-container
 | 
				
			||||||
 | 
					          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					            %tbody
 | 
				
			||||||
 | 
					              %tr
 | 
				
			||||||
 | 
					                %td.content-cell.content-start
 | 
				
			||||||
 | 
					                  %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                    %tbody
 | 
				
			||||||
 | 
					                      %tr
 | 
				
			||||||
 | 
					                        %td.column-cell.button-cell
 | 
				
			||||||
 | 
					                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
				
			||||||
 | 
					                            %tbody
 | 
				
			||||||
 | 
					                              %tr
 | 
				
			||||||
 | 
					                                %td.button-primary
 | 
				
			||||||
 | 
					                                  = link_to edit_user_registration_url do
 | 
				
			||||||
 | 
					                                    %span= t('settings.account_settings')
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_recovery_codes_changed.title' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= t 'devise.mailer.two_factor_recovery_codes_changed.explanation' %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=> <%= edit_user_registration_url %>
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,18 @@ en:
 | 
				
			||||||
        extra: If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one.
 | 
					        extra: If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one.
 | 
				
			||||||
        subject: 'Mastodon: Reset password instructions'
 | 
					        subject: 'Mastodon: Reset password instructions'
 | 
				
			||||||
        title: Password reset
 | 
					        title: Password reset
 | 
				
			||||||
 | 
					      two_factor_disabled:
 | 
				
			||||||
 | 
					        explanation: Two-factor authentication for your account has been disabled. Login is now possible using only e-mail address and password.
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Two-factor authentication disabled'
 | 
				
			||||||
 | 
					        title: 2FA disabled
 | 
				
			||||||
 | 
					      two_factor_enabled:
 | 
				
			||||||
 | 
					        explanation: Two-factor authentication has been enabled for your account. A token generated by the paired TOTP app will be required for login.
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Two-factor authentication enabled'
 | 
				
			||||||
 | 
					        title: 2FA enabled
 | 
				
			||||||
 | 
					      two_factor_recovery_codes_changed:
 | 
				
			||||||
 | 
					        explanation: The previous recovery codes have been invalidated and new ones generated.
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Two-factor recovery codes re-generated'
 | 
				
			||||||
 | 
					        title: 2FA recovery codes changed
 | 
				
			||||||
      unlock_instructions:
 | 
					      unlock_instructions:
 | 
				
			||||||
        subject: 'Mastodon: Unlock instructions'
 | 
					        subject: 'Mastodon: Unlock instructions'
 | 
				
			||||||
    omniauth_callbacks:
 | 
					    omniauth_callbacks:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -621,6 +621,11 @@ en:
 | 
				
			||||||
      return: Show the user's profile
 | 
					      return: Show the user's profile
 | 
				
			||||||
      web: Go to web
 | 
					      web: Go to web
 | 
				
			||||||
    title: Follow %{acct}
 | 
					    title: Follow %{acct}
 | 
				
			||||||
 | 
					  challenge:
 | 
				
			||||||
 | 
					    confirm: Continue
 | 
				
			||||||
 | 
					    hint_html: "<strong>Tip:</strong> We won't ask you for your password again for the next hour."
 | 
				
			||||||
 | 
					    invalid_password: Invalid password
 | 
				
			||||||
 | 
					    prompt: Confirm password to continue
 | 
				
			||||||
  datetime:
 | 
					  datetime:
 | 
				
			||||||
    distance_in_words:
 | 
					    distance_in_words:
 | 
				
			||||||
      about_x_hours: "%{count}h"
 | 
					      about_x_hours: "%{count}h"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,8 @@ en:
 | 
				
			||||||
        domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored
 | 
					        domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored
 | 
				
			||||||
      featured_tag:
 | 
					      featured_tag:
 | 
				
			||||||
        name: 'You might want to use one of these:'
 | 
					        name: 'You might want to use one of these:'
 | 
				
			||||||
 | 
					      form_challenge:
 | 
				
			||||||
 | 
					        current_password: You are entering a secure area
 | 
				
			||||||
      imports:
 | 
					      imports:
 | 
				
			||||||
        data: CSV file exported from another Mastodon server
 | 
					        data: CSV file exported from another Mastodon server
 | 
				
			||||||
      invite_request:
 | 
					      invite_request:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,7 @@ Rails.application.routes.draw do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    namespace :auth do
 | 
					    namespace :auth do
 | 
				
			||||||
      resource :setup, only: [:show, :update], controller: :setup
 | 
					      resource :setup, only: [:show, :update], controller: :setup
 | 
				
			||||||
 | 
					      resource :challenge, only: [:create], controller: :challenges
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										46
									
								
								spec/controllers/auth/challenges_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								spec/controllers/auth/challenges_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Auth::ChallengesController, type: :controller do
 | 
				
			||||||
 | 
					  render_views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let(:password) { 'foobar12345' }
 | 
				
			||||||
 | 
					  let(:user) { Fabricate(:user, password: password) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    sign_in user
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    let(:return_to) { edit_user_registration_path }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with correct password' do
 | 
				
			||||||
 | 
					      before { post :create, params: { form_challenge: { return_to: return_to, current_password: password } } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'redirects back' do
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(return_to)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'sets session' do
 | 
				
			||||||
 | 
					        expect(session[:challenge_passed_at]).to_not be_nil
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with incorrect password' do
 | 
				
			||||||
 | 
					      before { post :create, params: { form_challenge: { return_to: return_to, current_password: 'hhfggjjd562' } } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'renders challenge' do
 | 
				
			||||||
 | 
					        expect(response).to render_template('auth/challenges/new')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'displays error' do
 | 
				
			||||||
 | 
					        expect(response.body).to include 'Invalid password'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not set session' do
 | 
				
			||||||
 | 
					        expect(session[:challenge_passed_at]).to be_nil
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -80,7 +80,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
 | 
				
			||||||
        let(:user) do
 | 
					        let(:user) do
 | 
				
			||||||
          account = Fabricate.build(:account, username: 'pam_user1')
 | 
					          account = Fabricate.build(:account, username: 'pam_user1')
 | 
				
			||||||
          account.save!(validate: false)
 | 
					          account.save!(validate: false)
 | 
				
			||||||
          user = Fabricate(:user, email: 'pam@example.com', password: nil, account: account)
 | 
					          user = Fabricate(:user, email: 'pam@example.com', password: nil, account: account, external: true)
 | 
				
			||||||
          user
 | 
					          user
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										114
									
								
								spec/controllers/concerns/challengable_concern_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								spec/controllers/concerns/challengable_concern_spec.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe ChallengableConcern, type: :controller do
 | 
				
			||||||
 | 
					  controller(ApplicationController) do
 | 
				
			||||||
 | 
					    include ChallengableConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before_action :require_challenge!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def foo
 | 
				
			||||||
 | 
					      render plain: 'foo'
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def bar
 | 
				
			||||||
 | 
					      render plain: 'bar'
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    routes.draw do
 | 
				
			||||||
 | 
					      get  'foo' => 'anonymous#foo'
 | 
				
			||||||
 | 
					      post 'bar' => 'anonymous#bar'
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context 'with a no-password user' do
 | 
				
			||||||
 | 
					    let(:user) { Fabricate(:user, external: true, password: nil) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      sign_in user
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for GET requests' do
 | 
				
			||||||
 | 
					      before { get :foo }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not ask for password' do
 | 
				
			||||||
 | 
					        expect(response.body).to eq 'foo'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for POST requests' do
 | 
				
			||||||
 | 
					      before { post :bar }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not ask for password' do
 | 
				
			||||||
 | 
					        expect(response.body).to eq 'bar'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context 'with recent challenge in session' do
 | 
				
			||||||
 | 
					    let(:password) { 'foobar12345' }
 | 
				
			||||||
 | 
					    let(:user) { Fabricate(:user, password: password) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      sign_in user
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for GET requests' do
 | 
				
			||||||
 | 
					      before { get :foo, session: { challenge_passed_at: Time.now.utc } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not ask for password' do
 | 
				
			||||||
 | 
					        expect(response.body).to eq 'foo'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for POST requests' do
 | 
				
			||||||
 | 
					      before { post :bar, session: { challenge_passed_at: Time.now.utc } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not ask for password' do
 | 
				
			||||||
 | 
					        expect(response.body).to eq 'bar'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context 'with a password user' do
 | 
				
			||||||
 | 
					    let(:password) { 'foobar12345' }
 | 
				
			||||||
 | 
					    let(:user) { Fabricate(:user, password: password) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      sign_in user
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for GET requests' do
 | 
				
			||||||
 | 
					      before { get :foo }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'renders challenge' do
 | 
				
			||||||
 | 
					        expect(response).to render_template('auth/challenges/new')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # See Auth::ChallengesControllerSpec
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'for POST requests' do
 | 
				
			||||||
 | 
					      before { post :bar }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'renders challenge' do
 | 
				
			||||||
 | 
					        expect(response).to render_template('auth/challenges/new')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'accepts correct password' do
 | 
				
			||||||
 | 
					        post :bar, params: { form_challenge: { current_password: password } }
 | 
				
			||||||
 | 
					        expect(response.body).to eq 'bar'
 | 
				
			||||||
 | 
					        expect(session[:challenge_passed_at]).to_not be_nil
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'rejects wrong password' do
 | 
				
			||||||
 | 
					        post :bar, params: { form_challenge: { current_password: 'dddfff888123' } }
 | 
				
			||||||
 | 
					        expect(response.body).to render_template('auth/challenges/new')
 | 
				
			||||||
 | 
					        expect(session[:challenge_passed_at]).to be_nil
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
    context 'when signed in' do
 | 
					    context 'when signed in' do
 | 
				
			||||||
      subject do
 | 
					      subject do
 | 
				
			||||||
        sign_in user, scope: :user
 | 
					        sign_in user, scope: :user
 | 
				
			||||||
        get :new
 | 
					        get :new, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      include_examples 'renders :new'
 | 
					      include_examples 'renders :new'
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'redirects if user do not have otp_secret' do
 | 
					    it 'redirects if user do not have otp_secret' do
 | 
				
			||||||
      sign_in user_without_otp_secret, scope: :user
 | 
					      sign_in user_without_otp_secret, scope: :user
 | 
				
			||||||
      get :new
 | 
					      get :new, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
      expect(response).to redirect_to('/settings/two_factor_authentication')
 | 
					      expect(response).to redirect_to('/settings/two_factor_authentication')
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      describe 'when form_two_factor_confirmation parameter is not provided' do
 | 
					      describe 'when form_two_factor_confirmation parameter is not provided' do
 | 
				
			||||||
        it 'raises ActionController::ParameterMissing' do
 | 
					        it 'raises ActionController::ParameterMissing' do
 | 
				
			||||||
          post :create, params: {}
 | 
					          post :create, params: {}, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
          expect(response).to have_http_status(400)
 | 
					          expect(response).to have_http_status(400)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
            true
 | 
					            true
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
 | 
					          post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          expect(assigns(:recovery_codes)).to eq otp_backup_codes
 | 
					          expect(assigns(:recovery_codes)).to eq otp_backup_codes
 | 
				
			||||||
          expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
 | 
					          expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
 | 
				
			||||||
| 
						 | 
					@ -85,7 +85,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
            false
 | 
					            false
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
 | 
					          post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'renders the new view' do
 | 
					        it 'renders the new view' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ describe Settings::TwoFactorAuthentication::RecoveryCodesController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      sign_in user, scope: :user
 | 
					      sign_in user, scope: :user
 | 
				
			||||||
      post :create
 | 
					      post :create, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assigns(:recovery_codes)).to eq otp_backup_codes
 | 
					      expect(assigns(:recovery_codes)).to eq otp_backup_codes
 | 
				
			||||||
      expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
 | 
					      expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@ describe Settings::TwoFactorAuthenticationsController do
 | 
				
			||||||
      describe 'when creation succeeds' do
 | 
					      describe 'when creation succeeds' do
 | 
				
			||||||
        it 'updates user secret' do
 | 
					        it 'updates user secret' do
 | 
				
			||||||
          before = user.otp_secret
 | 
					          before = user.otp_secret
 | 
				
			||||||
          post :create
 | 
					          post :create, session: { challenge_passed_at: Time.now.utc }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          expect(user.reload.otp_secret).not_to eq(before)
 | 
					          expect(user.reload.otp_secret).not_to eq(before)
 | 
				
			||||||
          expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
 | 
					          expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,21 @@ class UserMailerPreview < ActionMailer::Preview
 | 
				
			||||||
    UserMailer.password_change(User.first)
 | 
					    UserMailer.password_change(User.first)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_disabled
 | 
				
			||||||
 | 
					  def two_factor_disabled
 | 
				
			||||||
 | 
					    UserMailer.two_factor_disabled(User.first)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_enabled
 | 
				
			||||||
 | 
					  def two_factor_enabled
 | 
				
			||||||
 | 
					    UserMailer.two_factor_enabled(User.first)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_recovery_codes_changed
 | 
				
			||||||
 | 
					  def two_factor_recovery_codes_changed
 | 
				
			||||||
 | 
					    UserMailer.two_factor_recovery_codes_changed(User.first)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions
 | 
					  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions
 | 
				
			||||||
  def reconfirmation_instructions
 | 
					  def reconfirmation_instructions
 | 
				
			||||||
    user = User.first
 | 
					    user = User.first
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue