1. Add a Forgot password? link to your view that includes the form which links to forgot_passwords_path. This is usually in your login form.
  2. Add the route get 'forgot_passwords', to: 'forgot_passwords#new'
  3. Add resources as well: resources :forgot_passwords, only: [:create] and resources :password_resets, only: [:show, :create] (Note that we will create a new controller forgot_passwords_controller.rb. This will be a virtual resource, since we don’t have a model/table for forgot_passwords. The reason we create the forgot_passwords controller is to hold our actions together and not add too many actions to our existing controllers. Theoretically we could also add the actions to our sessions controller.)
  4. Also add the route get 'forgot_password_confirmation', to: 'forgot_passwords#confirm' and get 'expired_token', to: 'password_resets#expired_token'
  5. Create the controller called forgot_passwords_controller.rb and add the new and create actions.
  6. In the views create the folder and file forgot_passwords/new.html.haml
  7. Add a form to forgot_passwords/new.html.haml that submits to forgot_passwords_path. It may look like this or similar (We’re using HAML in this case):

     <!-- forgot_passwords/new.html.haml -->
    
     %section.forgot_password.container
       .row
         .col-sm-10.col-bg-offset-1
           = form_tag forgot_passwords_path, class: "sign_in" do
             %header
               %h1 Forgot Password?
               %p We will send you an email with a link that you can use to reset your password.
             .form-group
               = label_tag :email, "Email Address"
               .row
                 .col-sm-4
                   = email_field_tag :email, class: "form-control"
             .form-group.action
               = submit_tag "Send Email", class: "btn btn-default"
    
    
  8. Flesh out the forgot_passwords_controller.rb create action

     # controllers/forgot_passwords_controller.rb
    
     class ForgotPasswordsController < ApplicationController
       def new
       end
    
       def create
         user = User.find_by(email: params[:email])
         if user
           user.update_with_token!
           AppMailer.send_forgot_password(user).deliver
           redirect_to forgot_password_confirmation_path
         else
           flash[:error] = params[:email].blank? ? "Email cannot be blank." : "There is no user this email address in the system."
           redirect_to forgot_passwords_path
         end
       end
     end
    
  9. Create the view template for the forgot password email

     <!-- views/app_mailer/send_forgot_password.html.haml -->
    
     !!! 5
     %html(lang="en-US")
       %body
         %p Please click on the link below to reset your password:
         %p= link_to "Reset my password", password_reset_url(@user.token)  
         <!-- Note we can't use pasword_reset_path, but have to use _url instead, because this will be not part of the web UI, but the link will be opened from the user's inbox -->
    
  10. Create a view template to confirm the forgot password email has been sent

    <!-- views/forgot_passwords/confirm.html.haml -->
    
    %section.confirm_password_reset.container
      .row
        .span10.offset1
          %p We have sent an email with instructions to reset your password.
    
    
  11. Add the send email action to AppMailer to send out a link for password reset:

    # mailers/app_mailer.rb
    
    # ...
    
    def send_forgot_password(user)
      @user = user
      mail to: user.email, from: "[email protected]", subject: "Please reset your password"
    end
    
    # ...
    
    
  12. Add a column to your users table to generate the tokens

    $ rails generate migration add_token_to_users

    # db/migrate/2015123456_add_token_to_users.rb
    class AddTokenToUsers < ActiveRecord::Migration
      def change
        add_column :users, :token, :string
      end
    end
    

    $ rake db:migrate

  13. Generate tokens in the user model

    We use the Ruby standard library SecureRandom.urlsafe_base64 to generate a token. http://ruby-doc.org/stdlib-2.2.1/libdoc/securerandom/rdoc/SecureRandom.html

    # models/user.rb
    
    # ... 
    
    def update_with_token!
      update_column(:token, generate_token)
    end
    
    private
    
    def generate_token
      SecureRandom.urlsafe_base64
    end
    
    # ... 
    
    
  14. Set default url options for the development, production and test environment.

    # config/environments/development.rb
    # config/environments/test.rb
    
    
    # ... 
    
    config.action_mailer.default_url_options = { host: 'localhost:3000'}
    
    # ... 
    
    

    In the production environment you have to do the same with a different domain:

    # config/environments/production.rb
    
    config.action_mailer.default_url_options = { host: 'yourdomain.com'}
    
    

    You have to restart the rails server to pick up this change.

  15. Create the password_resets_controller

    # controllers/password_resets_controller.rb
    
    class PasswordResetsController < ApplicationController
      def show
        user = User.find_by(token: params[:id])
        if user
          @token = user.token
        else
          redirect_to expired_token_path unless user
        end
      end
    
      def create
        user = User.find_by(token: params[:token])
        if user && user.update(password: params[:password], token: nil)
          flash[:success] = "Your password has been changed. Please sign in."
          redirect_to login_path
        elsif user
          @token = user.token
          render :show
        else
          redirect_to expired_token_path
        end
      end
    end
    
    
  16. Create the view template for password_resets/show.html.haml. We pass the token in a hidden form field.

    <!-- views/password_resets/show.html.haml -->
    
    %section.reset_password.container
      .row
        .col-sm-10.col-sm-offset-1
          = form_tag password_resets_path do
            %header
              %h1 Reset Your Password
            %fieldset.form-group
              = label_tag :password, "New Password"
              .row
                .col-sm-4
                  = text_field_tag :password
                  = hidden_field_tag :token, @token
            %fieldset.form-group.action
              = submit_tag "Reset Password", class: "btn btn-default"
    
  17. Create a static view template for expired tokens

    <!-- passwords_resets/expired_token.html.haml -->
    
    %section.invalid_token.container
      .row
        .span10.offset1
          %p Your reset password link has expired.
    


To summarize the different parts of your app:

Routes

# To summarize, your routes.rb should look like this

# ...
get 'forgot_passwords', to: 'forgot_passwords#new'
resources :forgot_passwords, only: [:create]
get 'forgot_password_confirmation', to: 'forgot_passwords#confirm'
resources :password_resets, only: [:show, :create]
get 'expired_token', to: 'password_resets#expired_token'
# ... 

Controller

# controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController
  def show
    user = User.find_by(token: params[:id])
    if user
      @token = user.token
    else
      redirect_to expired_token_path unless user
    end
  end

  def create
    user = User.find_by(token: params[:token])
    if user && user.update(password: params[:password], token: nil)
      flash[:success] = "Your password has been changed. Please sign in."
      redirect_to login_path
    elsif user
      @token = user.token
      render :show
    else
      redirect_to expired_token_path
    end
  end
end

# controllers/forgot_passwords_controller.rb

class ForgotPasswordsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email])
    if user
      user.update_with_token!
      AppMailer.send_forgot_password(user).deliver
      redirect_to forgot_password_confirmation_path
    else
      flash[:error] = params[:email].blank? ? "Email cannot be blank." : "There is no user this email address in the system."
      redirect_to forgot_passwords_path
    end
  end
end

App Mailer

# mailers/app_mailer.rb

# ...

def send_forgot_password(user)
  @user = user
  mail to: user.email, from: "[email protected]", subject: "Please reset your password"
end

# ...

Views

<!-- forgot_passwords/new.html.haml -->

%section.forgot_password.container
  .row
    .col-sm-10.col-bg-offset-1
      = form_tag forgot_passwords_path, class: "sign_in" do
        %header
          %h1 Forgot Password?
          %p We will send you an email with a link that you can use to reset your password.
        .form-group
          = label_tag :email, "Email Address"
          .row
            .col-sm-4
              = email_field_tag :email, class: "form-control"
        .form-group.action
          = submit_tag "Send Email", class: "btn btn-default"

<!-- views/app_mailer/send_forgot_password.html.haml -->

!!! 5
%html(lang="en-US")
  %body
    %p Please click on the link below to reset your password:
    %p= link_to "Reset my password", password_reset_url(@user.token) 
    <!-- Note we can't use pasword_reset_path, but have to use _url instead, because this will be not part of the web UI, but the link will be opened from the user's inbox -->
<!-- passwords_resets/expired_token.html.haml -->

%section.invalid_token.container
  .row
    .span10.offset1
      %p Your reset password link has expired.
<!-- views/password_resets/show.html.haml -->

%section.reset_password.container
  .row
    .col-sm-10.col-sm-offset-1
      = form_tag password_resets_path do
        %header
          %h1 Reset Your Password
        %fieldset.form-group
          = label_tag :password, "New Password"
          .row
            .col-sm-4
              = text_field_tag :password
              = hidden_field_tag :token, @token
        %fieldset.form-group.action
          = submit_tag "Reset Password", class: "btn btn-default"