sorcery gemのパスワードリセット機能の修正

sorcerygemのパスワードリセット機能の修正

sorcerygemのパスワードリセット機能で、以下の2点の不具合がありこれを修正するのにとても手こずったので、備忘録としてこちらに残します。
・[パスワードの欄]を空欄で[パスワード再確認の欄]のみを入力した場合にバリデーションエラーにならない。
・[パスワードの欄]を空欄で[パスワード再確認の欄]も空欄にした場合にバリデーションエラーにならない。

具体的に以下のような状態です。

Image from Gyazo

Image from Gyazo

コードは、以下のようになっています。

[app/controllers/password_resets_controller.rb]

class PasswordResetsController < ApplicationController
  # In Rails 5 and above, this will raise an error if
  # before_action :require_login
  # is not declared in your ApplicationController.
    
  # request password reset.
  # you get here when the user entered their email in the reset password form and submitted it.

  def new; end

  def create
    @user = User.find_by(email: params[:email])
        
    # This line sends an email to the user with instructions on how to reset their password (a url with a random token)
    @user.deliver_reset_password_instructions! if @user
        
    # Tell the user instructions have been sent whether or not email was found.
    # This is to not leak information to attackers about which emails exist in the system.
    if logged_in?
      flash.now[:success] = t '.success_message'
      render template: 'profiles/show'
    else
      flash[:success] = t '.success_message'
      redirect_to login_path
    end
  end
    
  # This is the reset password form.
  def edit
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)

    if @user.blank?
      not_authenticated
      return
    end
  end
      
  # This action fires when the user has sent the reset password form.
  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)

    if @user.blank?
      not_authenticated
      return
    end

    # the next line makes the password confirmation validation work
    @user.password_confirmation = params[:user][:password_confirmation]
    # the next line clears the temporary token and updates the password
    if @user.change_password(params[:user][:password])
      flash[:success] = t '.success_message'
      redirect_to login_path
    else
      render :action => "edit"
    end
  end
end
[app/models/use.rb]

class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :password, length: { minimum: 10 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: { case_sensitive: false }, presence: true
  validates :name, presence: true

  has_many :cooking_memories, dependent: :destroy
end

なぜ[パスワードの欄]を空欄で[パスワード再確認の欄]のみを入力した場合と、[パスワードの欄]を空欄で[パスワード再確認の欄]も空欄にした場合にパスワートリセットに成功してしまうのかを色々検証してみた。
以下がデバッグした検証結果

[パスワード再確認の欄]のみに入力した場合

     40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password])
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"qmu2f+c2ssTLaQBED4yEI9yWEZn0n02TAVtX0XYm76B1OoNrmOe8DSFKkxBJpsxPdAjWzOetoE4hi9eQE3T9Sg==", "user"=>{"password"=>"", "password_confirmation"=>"11090112sS"}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"JvreH-DiL2Jus4UztTdb"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id]
=> "JvreH-DiL2Jus4UztTdb"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token) 
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = 'JvreH-DiL2Jus4UztTdb' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):11:in `update'
=> #<User:0x00007fdd53356af0
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:43:29 JST +09:00,
 reset_password_token: "JvreH-DiL2Jus4UztTdb",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 02:47:42 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation] 
=> "11090112sS"
[6] pry(#<PasswordResetsController>)> @user.password_confirmation 
=> "11090112sS"
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):15:in `update'
=> true
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password])
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)* else  
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):16:in `update'
   (0.3ms)  BEGIN
  ↳ (pry):16:in `update'
  User Update (0.6ms)  UPDATE `users` SET `users`.`updated_at` = '2022-08-16 02:49:07.255211', `users`.`reset_password_token` = NULL WHERE `users`.`id` = 29
  ↳ (pry):16:in `update'
   (0.9ms)  COMMIT
  ↳ (pry):16:in `update'
true
=> nil

上記の事からバリデーションエラーにならずに、[パスワード再確認の欄]に入力された値のみでパスワードが更新されてしまっている。(UPDATE文が走っているが実際は、パスワードは元のまま)

[パスワードの欄]と[パスワード再確認の欄]の両方を空欄にした場合

    40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password])
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"y7rHggoY87OduOb0RcPwxjdisZUASiaUxD52xebtIvEU6/KWdcn9enebdaAD6biqn/x2wBN4y0nk7vaEg78wGw==", "user"=>{"password"=>"", "password_confirmation"=>""}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"JTAEfrBEaBZk7D_wPVrH"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id]
=> "JTAEfrBEaBZk7D_wPVrH"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token)
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = 'JTAEfrBEaBZk7D_wPVrH' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):23:in `update'
=> #<User:0x00007fdd53075de0
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:49:07 JST +09:00,
 reset_password_token: "JTAEfrBEaBZk7D_wPVrH",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 02:52:48 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation] 
=> ""
[6] pry(#<PasswordResetsController>)> @user.password_confirmation
=> ""
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):27:in `update'
=> true
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password]) 
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)* else  
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):28:in `update'
   (0.2ms)  BEGIN
  ↳ (pry):28:in `update'
  User Update (0.4ms)  UPDATE `users` SET `users`.`updated_at` = '2022-08-16 02:54:31.562467', `users`.`reset_password_token` = NULL WHERE `users`.`id` = 29
  ↳ (pry):28:in `update'
   (0.7ms)  COMMIT
  ↳ (pry):28:in `update'
true
=> nil

上記の事からバリデーションエラーにならずに、パスワードが更新されてしまっている。(UPDATE文が走っているが実際は、パスワードは元のまま)

検証から以下のことが分かった。
・新規でユーザー登録する時は、[パスワードの欄]と[パスワード再確認の欄]の両方に入力しないとバリデーションエラーになるが、パスワード更新時は[パスワードの欄]を空欄で[パスワード再確認の欄]のみを入力した場合も、[パスワードの欄]を空欄で[パスワード再確認の欄]も空欄にした場合もバリデーションエラーにならない。(理由は、不明。。。)

・[パスワードの欄]と[パスワード再確認の欄]でバリデーションがかかるかどうかを場合分すると以下のようになった。

パスワードの欄 パスワード再確認の欄 バリデーションがかかるか?
入力無し 入力有り バリデーションがかからない
入力無し 入力無し バリデーションがかからない
入力有り 入力無し バリデーションがかかる
入力有り 入力有り バリデーションがかかる

つまり、[パスワードの欄]に入力があれば通常通りバリデーションがかかるようになることが分かりました。
これを実現するためには、[パスワードの欄]にpresence: trueのバリデーションを設定する。
以下のように訂正する。

[app/models/use.rb]

class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :password, length: { minimum: 10 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password, presence: true
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: { case_sensitive: false }, presence: true
  validates :name, presence: true

  has_many :cooking_memories, dependent: :destroy
end

validates :password, presence: trueを追記後に再度以下のように検証してみた
[パスワード再確認の欄]のみに入力した場合

    40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password])
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"yqv/Oi2zwTL6cD0F6CVFka/ZKJHnMFxTRnmt0d5u+3eax5pG28TzvWIrwZzrHaWwVBUGYCkY5oZhaYHD1AIZLQ==", "user"=>{"password"=>"", "password_confirmation"=>"11090112sS"}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"uqpk5GV1pjYCRv43fA4z"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id]
=> "uqpk5GV1pjYCRv43fA4z"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token)
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = 'uqpk5GV1pjYCRv43fA4z' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):48:in `update'
=> #<User:0x00007fdd52026160
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:58:18 JST +09:00,
 reset_password_token: "uqpk5GV1pjYCRv43fA4z",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 03:16:47 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation 
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation]
=> "11090112sS"
[6] pry(#<PasswordResetsController>)> @user.password_confirmation 
=> "11090112sS"
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):52:in `update'
=> false
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password])
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"
[8] pry(#<PasswordResetsController>)* else  
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"  
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):53:in `update'
false
["パスワードを入力してください"]
=> nil

上記の事からバリデーションエラーになっていることが分かった。

[パスワードの欄]と[パスワード再確認の欄]の両方を空欄にした場合

    40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password])
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"nL+Dzuf1GvCWF+pdl5KrLIQONZSsk0z7JMxUTMQEBivM0+ayEYIofw5MFsSUqksNf8IbZWK79i4D3HhezmjkcQ==", "user"=>{"password"=>"", "password_confirmation"=>""}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"pZ6xzpWxkDNxZxKo2-u9"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id] 
=> "pZ6xzpWxkDNxZxKo2-u9"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token)
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = 'pZ6xzpWxkDNxZxKo2-u9' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):62:in `update'
=> #<User:0x00007fdd4da3cc08
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:58:18 JST +09:00,
 reset_password_token: "pZ6xzpWxkDNxZxKo2-u9",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 03:22:00 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation 
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation]
=> ""
[6] pry(#<PasswordResetsController>)> @user.password_confirmation 
=> ""
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):66:in `update'
=> false
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password])
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"
[8] pry(#<PasswordResetsController>)* else  
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"  
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):69:in `update'
false
["パスワードを入力してください"]
=> nil

上記の事からバリデーションエラーになっていることが分かった。

上記の事から以下のようにコードを修正した。

  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)

    if @user.blank?
      not_authenticated
      return
    end

    # the next line makes the password confirmation validation work
    @user.password = params[:user][:password]
    @user.password_confirmation = params[:user][:password_confirmation]
    # the next line clears the temporary token and updates the password
    if @user.valid? && @user.change_password(params[:user][:password])
      flash[:success] = t '.success_message'
      redirect_to login_path
    else
      render :edit
    end
  end

結果は、以下のようになった。

[パスワード再確認の欄]のみに入力した場合

    40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password]) && @user.valid?
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"Z0Eb44G1z9MS2HT3aLuCsR5PIpAHs9t2JUd+rd4V/Hw3LX6fd8L9XIqDiG5rg2KQ5YMMYcmbYaMCV1K/1HkeJg==", "user"=>{"password"=>"", "password_confirmation"=>"11090112sS"}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"3b6jkuLC1X11wzriynL4"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id]
=> "3b6jkuLC1X11wzriynL4"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token)
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = '3b6jkuLC1X11wzriynL4' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):3:in `update'
=> #<User:0x00007fe568f2aff8
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:58:18 JST +09:00,
 reset_password_token: "3b6jkuLC1X11wzriynL4",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 03:33:52 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation]
=> "11090112sS"
[6] pry(#<PasswordResetsController>)> @user.password_confirmation
=> "11090112sS"
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):7:in `update'
=> false
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password]) && @user.valid? 
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"
[8] pry(#<PasswordResetsController>)* else  
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"  
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):8:in `update'
false
["パスワードを入力してください"]
=> nil

[パスワードの欄]と[パスワード再確認の欄]の両方を空欄にした場合

    40: def update
 => 41:   binding.pry
    42:   @token = params[:id]
    43:   @user = User.load_from_reset_password_token(@token)
    44: 
    45:   if @user.blank?
    46:     not_authenticated
    47:     return
    48:   end
    49: 
    50:   # the next line makes the password confirmation validation work
    51:   @user.password_confirmation = params[:user][:password_confirmation]
    52:   # the next line clears the temporary token and updates the password
    53:   if @user.change_password(params[:user][:password]) && @user.valid?
    54:     flash[:success] = t '.success_message'
    55:     redirect_to login_path
    56:   else
    57:     render :action => "edit"
    58:   end
    59: end

[1] pry(#<PasswordResetsController>)> params
=> <ActionController::Parameters {"_method"=>"patch", "authenticity_token"=>"yoojFbl6kEu+IyyXNEa+CcvAUnl0A5W8yhXEhulNQj+a5kZpTw2ixCZ40A43fl4oMAx8iLorL2ntBeiU4yGgZQ==", "user"=>{"password"=>"", "password_confirmation"=>""}, "commit"=>"更新する", "controller"=>"password_resets", "action"=>"update", "id"=>"3b6jkuLC1X11wzriynL4"} permitted: false>
[2] pry(#<PasswordResetsController>)> @token = params[:id]
=> "3b6jkuLC1X11wzriynL4"
[3] pry(#<PasswordResetsController>)> @user = User.load_from_reset_password_token(@token)
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`reset_password_token` = '3b6jkuLC1X11wzriynL4' ORDER BY `users`.`id` ASC LIMIT 1
  ↳ (pry):17:in `update'
=> #<User:0x00007fe569c32a70
 id: 29,
 name: "テストユーザー",
 email: "test@gmail.com",
 crypted_password:
  "$2a$10$KvVxnw2NXOnK3FT3x9VTCuRwwZ7Eb4V45swlJpCpQWmMRpzHThpTC",
 salt: "W-pmhgxHUAVBxs717_D9",
 role: 0,
 created_at: Tue, 16 Aug 2022 02:17:59 JST +09:00,
 updated_at: Tue, 16 Aug 2022 02:58:18 JST +09:00,
 reset_password_token: "3b6jkuLC1X11wzriynL4",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Tue, 16 Aug 2022 03:33:52 JST +09:00,
 access_count_to_reset_password_page: 0>
[4] pry(#<PasswordResetsController>)> @user.password_confirmation
=> nil
[5] pry(#<PasswordResetsController>)> @user.password_confirmation = params[:user][:password_confirmation]
=> ""
[6] pry(#<PasswordResetsController>)> @user.password_confirmation
=> ""
[7] pry(#<PasswordResetsController>)> @user.valid?
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):21:in `update'
=> false
[8] pry(#<PasswordResetsController>)> if @user.change_password(params[:user][:password]) && @user.valid? 
[8] pry(#<PasswordResetsController>)*   puts true
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"
[8] pry(#<PasswordResetsController>)*   puts false
[8] pry(#<PasswordResetsController>)*   puts "#{@user.errors.full_messages}"  
[8] pry(#<PasswordResetsController>)* end  
  CACHE User Exists? (0.0ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'test@gmail.com' AND `users`.`id` != 29 LIMIT 1
  ↳ (pry):22:in `update'
=> nil

ブラウザの画面でも以下のようにちゃんとエラーになった。

Image from Gyazo