Search code examples
ruby-on-railsauthenticationrubygemsdeviseruby-on-rails-7

Devise: update password succeeds without password confirmation & without respecting min length


In my registrations edit form, I have the basic password change logic implemented. When I wanted to style the error messages, I tested the functionality for the first time and I ran into two major issues:

  • password changes without it respecting the min. length set in the config file
  • password changes without the password confirmation

This only happens on the second try! First form submit shows the correct error messages, if I send the data (click the submit button) a second time: tadaaa - logged out and password changed. I don't even know what else to add, it is one of the most basic concepts of authentication.

What am I missing? Anyone has experience with this issue?

config/initializers/devise.rb

# ==> Configuration for :validatable
# Range for password length.
config.password_length = 6..128

user.rb

  devise :invitable, :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

registrations/edit.html.erb

<%= f.label       :change_password,
                  class: 'user__password-change' %>
<%= f.input       :password,
                  hint: "leave it blank if you don't want to change it",
                  required: false,
                  error: false %>
<%= f.error       :password, class: 'form-error' %>
<%= f.input       :password_confirmation,
                  required: false,
                  error: false %>
<%= f.error       :password_confirmation, class: 'form-error' %>

I know that I have a required: false on both, but this basically just takes the asterisk away.

registrations_controller.rb


  def update
    super
    @user.update(country: set_country, city: set_city)

    @couch = @user.couch
    @couchfacilities = @couch.couch_facilities
    update_couch
    update_couch_facilities

    if offers_couch == "0"
      set_couch_inactive
    elsif offers_couch == "1"
      set_couch_active
    end

    @usercharacteristics = @user.user_characteristics
    update_user_characteristics
  end

Solution

  • The core issue here is that you have not approached customizing the Devise controller in a good way at all.

    Lets start by looking at how the super method is implemented:

    class Devise::RegistrationsController < DeviseController
      # PUT /resource
      # We need to use a copy of the resource because we don't want to change
      # the current user in place.
      def update
        self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
        prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)
    
        resource_updated = update_resource(resource, account_update_params)
        yield resource if block_given?
        if resource_updated
          set_flash_message_for_update(resource, prev_unconfirmed_email)
          bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?
    
          respond_with resource, location: after_update_path_for(resource)
        else
          clean_up_passwords resource
          set_minimum_password_length
          respond_with resource
        end
      end
      ...
    end
    

    As you have implemented it now your code is being executed way after the response has already been decided. You added a ton of additional code paths as well as each additional update may fail which are not handled at all.

    If you want to tap into the flow and do something to the resource after it has been saved pass but before the response is rendered pass a block to super:

    def update
      super do |resource|
        resource.update(country: set_country, city: set_city)
      end
    end
    

    If you want to instead do something before its saved override the update_resource method:

    def update_resource(resource, params)
      resource.assign_attributes(
        country: set_country, city: set_city
      )
      super(resource, params)
    end
    

    You should also consider if you really need to jam all this functionality into a single endpoint.