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.