Rails provides the has_secure_password method, which makes it gloriously easy to implement authentication in our application.
But we often need an extra layer of verification before allowing users to update certain fields. For e.g. Users must provide their “old” password when updating their email/password fields.

Before Rails 7.1

To implement this, we must manually add and validate the current_password accessor:

# app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password

  attr_accessor :current_password
end
# app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  def update
    password_challenge = password_params.delete(:current_password)
    @password_challenge_succeeded = current_user.authenticate(password_challenge)

    if @password_challenge_succeeded && current_user.update(password_params)
      # ...
    end
  end

  private

  def password_params
    params.require(:user).permit(
      :password, 
      :password_confirmation, 
      :current_password
    )
  end
end
# app/views/users/password_edit.html.erb

<%= form_for current_user do |f| %>
  <div class='mb-4'>
    <%= f.label :current_password, 'Current Password' %>
    <%= f.password_field :current_password %>
    <!-- Render error if @password_challenge_succeeded is false -->
    <% unless @password_challenge_succeeded %>
      <p class='error'>Current password is invalid.</p>
    <% end %>
  </div>

  <div class='mb-4'>
    <%= f.label :password, 'New Password' %>
    <%= f.password_field :password %>
  </div>

  <div class='mb-6'>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </div>

  <div>
    <%= f.submit 'Update' %>
  </div>
<% end %>

Rails 7.1 onwards

has_secure_password now includes a password_challenge accessor and its corresponding validation. So we no longer need our own accessor and can use password_challenge to verify the existing password.
When password_challenge is set, it will validate against the currently persisted password digest (i.e. password_digest_was).

# app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password
end
# app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  def update
    if current_user.update(password_params)
      # ...
    end
  end

  private

  def password_params
    # Note: password_challenge will validate only if it is set to a value other than nil.
    params.require(:user).permit(
      :password, 
      :password_confirmation, 
      :password_challenge
    ).with_defaults(password_challenge: '')
  end
end

And in the views we can replace current_password field with password_challenge.

# app/views/users/password_edit.html.erb

<%= form_for current_user do |f| %>
  <div class='mb-4'>
    <%= f.label :password_challenge, 'Current Password' %>
    <%= f.password_field :password_challenge %>
    <% if f.object.errors[:password_challenge].present? %>
      <p class='error'> <%= f.object.errors[:password_challenge].first %> </p>
    <% end %>
  </div>
    .
    .
    .
<% end %>

This allows password_challenge to be implemented with the same ease as the password_confirmation. If the password_challenge fails, a validation error will be added just like any other attribute.

Check out this pull request for more details.