Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby on Rails / Devise - Requiring password on email change

This is it, for all the marbles, if I can get this issue solved then I have a project completed.

Anyway, I am using Ruby on Rails 3 with Devise for user authentication. As you may know, in the user admin/edit by default, a user has to enter their current password in the current_password field if they provide a new password. There is a TON of information out there on how to disable current_password so users can change and save freely.

However, I can find very little on doing the opposite: requiring the current password for more fields...in my case, the email field. AND only require the current password when that email addy is changed, not if it remains the same. Currently users can freely change their email without giving their current password, and for security reasons, I don't want this.

After looking through the Devise wiki, I did find this page and I thought I could reverse this code to complete this solution. I managed to work this little bit out in my user.rb model (I stripped out of the unnecessary logic for this post)....

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable, :lockable and :timeoutable
  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation, :avatar, :remember_me, :agree
  attr_accessor :accessible, :agree, :signed_in
  attr_readonly :name

  # Validation
  validates :name, :presence => TRUE, :uniqueness => TRUE, :length => { :within => 4..20 }
  validates :agree, :term_agreement => TRUE, :unless => :signed_in
  validates_attachment_size :avatar, :less_than => 1.megabyte
  validates_attachment_content_type :avatar, :content_type => ['image/jpeg', 'image/png', 'image/gif']
  validates :current_password, :presence => TRUE, :if => :password_required?

  protected

  def password_required?
    email_changed?
  end

end

It "almost" works. If I save the user profile with changing nothing, or change other non-password required field (like the user avatar), the profile saves fine, no password required. So far, so good.

And if I change the email address, the validation is triggered....but what happens is that both the current_password AND password (for new password) fields trigger as required. And if I fill in the password in all three (password, password_confirmation, current_password) just for the hell of it, it won't take, just gives a validation error again.

Basically, ONLY the current_password should be required if the email address is changed. How would I make this work in my code?

  • UPDATE *******

I checked out my log in response to the below suggestion by bowsersenior, and see the following lines when I attempt to save and update the email...

User Load (0.2ms)  SELECT `users`.`id` FROM `users` WHERE (LOWER(`users`.`email`) = LOWER('[email protected]')) AND (`users`.id <> 1) LIMIT 1
  User Load (0.2ms)  SELECT `users`.`id` FROM `users` WHERE (`users`.`name` = BINARY 'Administrator') AND (`users`.id <> 1) LIMIT 1
  SQL (0.1ms)  ROLLBACK

I wonder if that 'ROLLBACK' has something to do with the final issue?

like image 625
Shannon Avatar asked Mar 24 '11 01:03

Shannon


3 Answers

Give this a try:

validates :current_password, :presence => TRUE, :if => :email_changed?

I strongly suggest you leave password_required? alone. That could lead to bugs with security and unstable behavior.

like image 97
bowsersenior Avatar answered Nov 19 '22 14:11

bowsersenior


I believe the Devise way of doing this is as follows:

class RegistrationsController < Devise::RegistrationsController
  def update
    @user = User.find(current_user.id)

    successfully_updated = if needs_password?(@user, params)
      @user.update_with_password(params[:user])
    else
      # remove the virtual current_password attribute update_without_password
      # doesn't know how to ignore it
      params[:user].delete(:current_password)
      @user.update_without_password(params[:user])
    end

    if successfully_updated
      set_flash_message :notice, :updated
      redirect_to after_update_path_for(@user)
    else
      render "edit"
    end
  end

  private

  def needs_password?(user, params)
    user.email != params[:user][:email]
  end
end

In other words, everything happens at the Controller level and we use either User#update_with_password, when user want to change his email (we know that by invoking needs_password? in the update action) or User#update_without_password, when user changes his other profile data.

Also remember you need to "register" this new RegistrationsController in your routes:

devise_for :users, :controllers => { :registrations => "registrations" }

Source:

  • https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
like image 4
Paweł Gościcki Avatar answered Nov 19 '22 14:11

Paweł Gościcki


Similar to Pawel's answer, but rather than override the controller method update, how about this, based on the devise wiki

class RegistrationsController < Devise::RegistrationsController
  protected

  def update_resource(resource, params)
    if resource.email != params[:email] || params[:password].present?
      super
    else
      params.delete(:current_password)
      resource.update_without_password(params)
    end
  end
end
like image 1
justingordon Avatar answered Nov 19 '22 15:11

justingordon