Topic: Implement "forgot password" in restful authentication plugin

* Edit:  the new restful_authentication uses acts_as_state_machine which is better suited for this and is what i use personally.  19Dec07

It was surprisingly easy to implement a "forgot password" functionality to the restful authentication plugin and since I'd looked around and couldn't find instructions on it, I thought I'd post what I did. Hopefully it'll save you some time... Oh, and suggestions on how to improve my code are welcome as well.

p.s. I decided not to create a brand new controller and stay fully restful since the restful authentication plugin already has an extra method called "activate" so I thought it wouldn't be a stretch to add 2 more methods and call them "forgot" and "reset".

In routes.rb

  map.forgot    '/forgot',                    :controller => 'users',     :action => 'forgot'
  map.reset     'reset/:reset_code',          :controller => 'users',     :action => 'reset'

In users_controller.rb
  def forgot
    if request.post?
      user = User.find_by_email(params[:user][:email])
      if user
        user.create_reset_code
        flash[:notice] = "Reset code sent to #{user.email}"
      else
        flash[:notice] = "#{params[:user][:email]} does not exist in system"
      end
      redirect_back_or_default('/')
    end
  end
 
  def reset
    @user = User.find_by_reset_code(params[:reset_code]) unless params[:reset_code].nil?
    if request.post?
      if @user.update_attributes(:password => params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
        self.current_user = @user
        @user.delete_reset_code
        flash[:notice] = "Password reset successfully for #{@user.email}"
        redirect_back_or_default('/')
      else
        render :action => :reset
      end
    end
  end

In user_notifier.rb
class UserNotifier < ActionMailer::Base
  def signup_notification(user)
    setup_email(user)
    @subject    += 'Please activate your new account' 
    @body[:url]  = "http://www.mysite.com/activate/#{user.activation_code}"
  end
 
  def activation(user)
    setup_email(user)
    @subject    += 'Your account has been activated!'
    @body[:url]  = "http://www.mysite.com"
  end
 
  def reset_notification(user)
    setup_email(user)
    @subject    += 'Link to reset your password'
    @body[:url]  = "http://www.mysite.com/reset/#{user.reset_code}"
  end
 
  protected
    def setup_email(user)
      @recipients  = "#{user.email}"
      @from        = "support@mysite.com"
      @subject     = "[mysite] "
      @sent_on     = Time.now
      @body[:user] = user
    end
end

in user_observer.rb
class UserObserver < ActiveRecord::Observer
  def after_create(user)
    UserNotifier.deliver_signup_notification(user)
  end

  def after_save(user)
    UserNotifier.deliver_activation(user) if user.recently_activated?
    UserNotifier.deliver_reset_notification(user) if user.recently_reset?
  end
end


in reset_notification.rhtml
Request to reset password received for <%= @user.login %>

Visit this url to choose a new password:

  <%= @url %>
 
(Your password will remain the same if no action is taken)


in user.rb
  def create_reset_code
    @reset = true
    self.attributes = {:reset_code => Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )}
    save(false)
  end
 
  def recently_reset?
    @reset
  end

  def delete_reset_code
    self.attributes = {:reset_code => nil}
    save(false)
  end


in forgot.rhtml
<%= error_messages_for :user %>

<% form_for :user do |f| -%>
<table id="newedit">
    <thead>
        <tr>
            <th colspan="2" >Request link to reset password</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td align="right"><label>Email:</label></td>
            <td><%= f.text_field :email %></td>
        </tr>       
        <tr>
            <td></td>
            <td><%= submit_tag 'Submit' %></td>
        </tr>
    </tbody>
</table>
<% end -%>


in reset.rhtml
<%= error_messages_for :user %>

<% form_for :user do |f| -%>
<table id="newedit">
    <thead>
        <tr>
            <th colspan="2" >Pick a new password for <%= @user.email %></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td align="right"><label>Password:</label></td>
            <td><%= f.password_field :password %></td>
        </tr>

        <tr>
            <td align="right"><label>Confirm Password:</label></td>
            <td><%= f.password_field :password_confirmation %></td>
        </tr>
       
        <tr>
            <td></td>
            <td><%= submit_tag 'Reset' %></td>
        </tr>
    </tbody>
</table>
<% end -%>

Last edited by sthapit (2007-12-19 21:19:41)

Re: Implement "forgot password" in restful authentication plugin

Thanks for this.  This functionality is something I've been putting off figuring out for my app.  Cheers.

Re: Implement "forgot password" in restful authentication plugin

this might also be helpful:

http://technoweenie.stikipad.com/plugin … +passwords

Re: Implement "forgot password" in restful authentication plugin

Question:

Have followed this code & included a reset_code column in my DB.  When i go through the forgot request - this request code is not added to the tail of the URL. I'm currently reworking to replicate the activation style but not having much luck yet. any ideas?

thanks

Re: Implement "forgot password" in restful authentication plugin

the new version of restful authentication uses the acts_as_state_machine plugin - i would use that method if i was going to do this again and just add a new "reset" state and reset_code column.

Re: Implement "forgot password" in restful authentication plugin

sthapit,

what did you have in mind for the "reset state"?

Re: Implement "forgot password" in restful authentication plugin

acts_as_state_machine :initial => :pending
  state :passive
  state :pending, :enter => :make_activation_code
  state :active,  :enter => :do_activate
  state :suspended
  state :deleted, :enter => :do_delete

this is what's currently in user.rb.  just add another state :reset.  maybe you can also reuse the activation_code column.  it's up to you.

Re: Implement "forgot password" in restful authentication plugin

sthapit,

have been playing around with acts_as_state_machine and i'm getting caught during the activation,I get the notification email -but it dones't want to flick over from pending to active -you had any luck here.

Ps. states are as in your last post.

User controller

  def activate
    self.current_user = params[:activation_code].blank? ? :false : User.find_by_activation_code(params[:activation_code]))
    if logged_in? && !current_user.activated?
      current_user.activate!
      flash[:notice] = "Signup complete!"
    end
    redirect_to (:controller => "site", :action => "index")
  end

out of my user rb.

  def make_activation_code
      self.deleted_at = nil
      self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
    end
   
    def do_delete
      self.deleted_at = Time.now.utc
    end

    def do_activate
      self.activated_at = Time.now.utc
      self.deleted_at = self.activation_code = nil
    end

Re: Implement "forgot password" in restful authentication plugin

do you have this in your model?

user.rb

  event :activate do
    transitions :from => :pending, :to => :active
  end

Re: Implement "forgot password" in restful authentication plugin

yep!

user.rb

 acts_as_state_machine :initial => :pending
  state :passive
  state :pending, :enter => :make_activation_code
  state :active,  :enter => :do_activate
  state :suspended
  state :deleted, :enter => :do_delete
 
 
  event :register do
    transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| !(u.crypted_password.blank? && u.password.blank?) }
  end
 
  event :activate do
    transitions :from => :pending, :to => :active
  end
 
  event :suspend do
    transitions :from => [:passive, :pending, :active], :to => :suspended
  end
 
  event :delete do
    transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted
  end

  event :unsuspend do
    transitions :from => :suspended, :to => :active,  :guard => Proc.new {|u| !u.activated_at.blank? }
    transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| !u.activation_code.blank? }
    transitions :from => :suspended, :to => :passive
  end


It feels like its not picking up the activation_code col in the db.

db migration

  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
      t.column :activation_code, :string,  :limit => 40
      t.column :activated_at,              :datetime
      t.column :state,                     :string, :null => :no, :default => 'passive'
      t.column :deleted_at,                :datetime
      t.column :password_reset_code,       :string, :limit => 40     
    end
  end

weird, any ideas would be much appreciated

Re: Implement "forgot password" in restful authentication plugin

very strange.  try running the tests for your plugin and make sure they all pass - maybe there was a bug in the version you downloaded.

Re: Implement "forgot password" in restful authentication plugin

Agreed,

I might have to come back to this installation further down the track.  Thanks for your help sthapit.#big_smile#

Re: Implement "forgot password" in restful authentication plugin

you can't quit now!  you've spent too much time to not make this work smile  just go to your application folder and type "rake tests"

Re: Implement "forgot password" in restful authentication plugin

Dammit, TDD. Hold on!

Re: Implement "forgot password" in restful authentication plugin

Thanks for this

Re: Implement "forgot password" in restful authentication plugin

I could be wrong about this (I'm cannibalising this code, rather than using the restful authentication plugin) but it seems to me that find_by_reset_code will return the first user record with a null reset_code, if no reset code is supplied in the URL.

Re: Implement "forgot password" in restful authentication plugin

you're totally right.  changed the code above to:

User.find_by_reset_code(params[:reset_code]) unless params[:reset_code].nil?

Re: Implement "forgot password" in restful authentication plugin

Author: dcaspira

Great find collingridge -just did it before coming across sthapit's post!

Also - @sthapit - had a brain drain moment re authentication, through testing i found my routes were all out of whack.

For others that suffer the same occasional flaw :-)

  map.activate  '/activate/:activation_code', :controller => 'users',        :action => 'activate'

Re: Implement "forgot password" in restful authentication plugin

Here is another one you guys may have been playing around with.

With password_rest I'm not totally happy with the way that the current_user is referenced - check the below code. When the user puts in a password that is too short by Validation crit - is passes on the failure notice - but it start a session as they are current_users.  Kinda counter-intuitive.

I'm playing around with this now - seems like a session control issue.

have you guys done anything about this?

    def reset_password
        @title = "Reset Password"
        @user = User.find_by_password_reset_code(params[:password_reset_code])  unless params[:password_reset_code].nil?
        #raise if @user.nil?

        return if @user unless params[:user]

        #if ((params[:user][:password] == params[:user][:password_confirmation]) && !params[:user][:password_confirmation].blank?)
        if (params[:user][:password] ==  params[:user][:password_confirmation])
            self.current_user = @user #for the next two lines to work
            current_user.password_confirmation = params[:user][:password_confirmation]
            current_user.password = params[:user][:password]
            @user.reset_password
            flash[:notice] = current_user.save ? "Done, Your is Password reset" : "Password not reset. Hint, make your Password atleast 8 characters long."
            redirect_back_or_default('/')
        else
           
            flash[:notice] = "Password mismatch.. please try again"
        end
        rescue
    logger.error "Invalid Reset Code entered"
    flash[:notice] = "That is an invalid password reset action. Please check your email and try again."
    redirect_back_or_default('/')
end

Re: Implement "forgot password" in restful authentication plugin

Well,

I reset the session after the saving of user info and notified the user to login.

heres the code - simple reset_session call.

Not 100% sold on the user experience though seems a bit clunky having to login again - but atleast the functionality is correct - you guys have any thoughts?

            self.current_user = @user #for the next two lines to work
            current_user.password_confirmation = params[:user][:password_confirmation]
            current_user.password = params[:user][:password]
            @user.reset_password
            reset_session
            flash[:notice] = current_user.save ? "Done, Your is Password reset - Please Login Now" : "Password not reset. Hint, make your Password atleast 8 characters long."
            redirect_back_or_default('/')