homeASCIIcasts

274: Remember Me & Reset Password 

(view original Railscast)

Other translations: Fr Ja Es

Although there are a number of good available authentication solutions for Rails applications there’s still a lot to be said for rolling your own. In episode 250 [watch, read] we did exactly that and later, in episode 270 [watch, read], we showed how Rails 3.1 makes it even easier by giving us has_secure_password which will automatically generate password hashes.

The authentication we implemented in those episodes was fairly basic so in this episode we’ll add a couple of new features to make it better. First we’ll add a “Remember Me” checkbox to the login page so that users can choose to be logged in automatically then we’ll add a “Reset Password” link that will allow users who have forgotten their password to have it reset. We’ll implement these features by extending the application we wrote in episode 270. This application uses Rails 3.1 but what we’ll show here will work just as well in Rails 3.0.

Adding a “Remember Me” Checkbox

When a user logs in to our application their id is stored in the session. This takes place in the SessionsController’s create action.

/app/controllers/sessions_controller.rb

def create
  user = User.find_by_email(params[:email])
  if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    redirect_to root_url, :notice => "Logged in!"
  else
    flash.now.alert = "Invalid email or password"
    render "new"
  end
end

When a logged-in user closes their browser the session cookie is deleted and they need to log in again the next time they open the application. We’ll replace this session cookie with a permanent one so that we can persist each user’s id.

The most obvious problem with this is that the ids are stored as sequential integers. If the id is stored in a permanent cookie it would be easy for a malicious user to change the value and so view other users’ data. To stop this we’ll generate a unique token for each user that is unguessable and store that value in the cookie instead.

Each user will have their own token which will need to be stored in the database so we’ll create a migration that will add an auth_token field to the users table and then migrate the database.

$ rails g migration add_auth_token_to_users auth_token:string

We need a way to generate this unique token when a user is created and so we’ll write a method called generate_token in the User model. This method will take a column argument so that we can have multiple tokens later if need be.

/app/models/user.rb

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation
  has_secure_password
  validates_presence_of :password, :on => :create
  before_create { generate_token(:auth_token) }
  def generate_token(column)
    begin
      
    end while User.exists?(column => self[column])
  end
end

To create the token we use ActiveSupport’s class to generate a random string. We check that no other user exists with that token and repeatedly generate another random token while this is true. We call the method in a before_create filter so that the token is generated when a new user is saved for the first time. If we have pre-existing users in the database we’ll need to create tokens for them as we could create a rake task to do this, though we won’t do so here.

We’ll modify the SessionsController’s create action now so that when a user logs in we store their token in a cookie. We’ll change the destroy action too so that the cookie is removed when a user logs out.

/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end
  def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password])
      cookies.permanent[:auth_token] = user.auth_token
      redirect_to root_url, :notice => "Logged in!"
    else
      flash.now.alert = "Invalid email or password"
      render "new"
    end
  end
  def destroy
    cookies.delete(:auth_token)
    redirect_to root_url, :notice => "Logged out!"
  end
end

Any user who logs in now is logged in permanently. They may not want this so we’ll add a checkbox to the login form so that they can choose for themselves. The changes to the form are straightforward enough: we just need to add the checkbox along with a label that says what it’s for.

/app/views/sessions/new.html.erb

<h1>Log in</h1>
<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :email %>
    <%= text_field_tag :email, params[:email] %>
  </div>
  <div class="field">
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>
  <div class="field">
    <%= label_tag :remember_me %>
    <%= check_box_tag :remember_me, 1, params[:remember_me] %>
  </div>  
  <div class="actions"><%= submit_tag "Log in" %></div>
<% end %>

We can now modify the SessionsController so that the permanent cookie is only set if the user has checked the checkbox. If they haven’t the login details will be stored in a session cookie.

/app/controllers/sessions_controller.rb

def create
  user = User.find_by_email(params[:email])
  if user && user.authenticate(params[:password])
    if params[:remember_me]
      cookies.permanent[:auth_token] = user.auth_token
    else
      cookies[:auth_token] = user.auth_token  
    end
    redirect_to root_url, :notice => "Logged in!"
  else
    flash.now.alert = "Invalid email or password"
    render "new"
  end
end

There’s one more change we need to make. The ApplicationController needs to be changed so that it reads the authentication token from the cookie rather than a user’s id from the session.

/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  private
  def current_user
    @current_user ||= User.find_by_auth_token( ↵
      cookies[:auth_token]) if cookies[:auth_token]
  end
  helper_method :current_user
end

We can try this out now. When we log into our application now we’ll see the “Remember me” checkbox. If we check the box when we log in and then close and reopen the browser we’ll be logged in automatically. Our “remember me” functionality is working just as we want.

The form with the “remember me’ checkbox.

Adding “Forgotten Password” Functionality

Now we’ll take a look at allowing a user to reset a forgotten password. We’ll start by adding a suitable link in the login form.

/app/views/sessions/new.html.erb

<h1>Log in</h1>
<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :email %>
    <%= text_field_tag :email, params[:email] %>
  </div>
  <div class="field">
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>
  <p><%= link_to "forgotten password?", ↵
    new_password_reset_path %></p>
  <div class="field">
    <%= label_tag :remember_me %>
    <%= check_box_tag :remember_me, 1, params[:remember_me] %>
  </div>  
  <div class="actions"><%= submit_tag "Log in" %></div>
<% end %>

The link points to new_password_reset_path which is part of a resource that we haven’t yet written. We’ll fix that now by creating a PasswordResets controller with a new action.

$ rails g controller password_resets new

We want to treat this controller as a resource so we’ll modify the routes file, replacing the generated route with a call to resources.

/config/routes.rb

Auth::Application.routes.draw do
  get "logout" => "sessions#destroy", :as => "logout"
  get "login" => "sessions#new", :as => "login"
  get "signup" => "users#new", :as => "signup"
  root :to => "home#index"
  resources :users
  resources :sessions
  resources :password_resets
end

This isn’t a proper model-backed resource but it will work for us.

In the new action’s view we’ll create a form to allow a user to enter their email address and request that their password is reset. The form looks like this:

/app/views/password_resets/new.html.erb

<h1>Reset Password</h1>
<%= form_tag password_resets_path, :method => :post do %>
  <div class="field">
    <%= label_tag :email %>
    <%= text_field_tag :email, params[:email] %>
  </div>
  <div class="actions"><%= submit_tag "Reset Password" %></div>
<% end %>

This isn’t a model-backed resource so we’re using form_tag here. The form POSTs to the PasswordResets controller’s create action and we’ll write that action next. In it we’ll find the User with the supplied email address and send them instructions on how to reset their password. This will be done in a new send_password_reset method in the User model.

/app/controllers/password_resets.rb

def create
  user = User.find_by_email(params[:email])
  user.send_password_reset if user
  redirect_to root_url, :notice => "Email sent with ↵
    password reset instructions."
end

The notice is shown whether the user is found or not. This makes things a little more secure so that a malicious user can’t determine whether a given user exists in the database.

We’ll write the send_password_reset method now. In it we’ll send an email containing a token for the password reset request. We want the token to expire after a given period, say a couple of hours, so that the link is valid only for a short time after the reset is requested. To hold this data we’ll need a couple of extra fields in the users table so we’ll write and run a migration to create these.

$ rails g migration add_password_reset_to_users password_reset_token:string password_reset_sent_at:datetime

In send_password_reset we’ll use the generate_token method we wrote earlier to create a password reset token. We’ll also set the password_reset_sent_at field so that we know when the token should expire and then save the User so that these details are stored. After we’ve saved the changes to the User we’ll pass it to a UserMailer so that we can sent the reset email.

/app/models/user.rb

def send_password_reset
  generate_token(:password_reset_token)
  self.password_reset_sent_at = Time.zone.now
  save!
  UserMailer.password_reset(self).deliver
end

We haven’t created the UserMailer yet so we’ll do that next.

$ rails g mailer user_mailer password_reset

In the mailer we’ll assign the user to an instance variable so that we can access it from the template and set the recipient and subject.

/app/mailers/user_mailer.rb

class UserMailer < ActionMailer::Base
  default from: "from@example.com"
  def password_reset(user)
    @user = user
    mail :to => user.email, :subject => "Password Reset"
  end
end

In the template we’ll write some instructions and provide a link to reset the password.

/app/views/user_mailer/password_reset_text.erb

To reset your password click the URL below.
<%= edit_password_reset_url(@user.password_reset_token) %>
If you did not request your password to be reset please ignore this email and your password will stay as it is.

The link in the email sends the user to the PasswordResetsController’s edit action. Technically this isn’t the best RESTful approach but it will work well enough for us. To get URLs working in mailers we’ll need to alter our environment configuration and add the following line to development.rb.

/config/environments/development.rb

Auth::Application.configure do
  # Other config items omitted.
  config.action_mailer.default_url_options = { :host => ↵ 
    "localhost:3000" }
end

We’ll add a similar line in production.rb with the live domain name.

Let’s try this out. If we visit the password reset page and enter our email address we should be told that an email containing the reset instructions has been sent.

The home page showing that the password reset email has been sent.

When we check the development log we’ll see the details of the email.

Sent mail to eifion@asciicasts.com (65ms)
Date: Thu, 14 Jul 2011 20:18:48 +0100
From: from@example.com
To: eifion@asciicasts.com
Message-ID: <4e1f4118af661_31a81639e544652a@noonoo.home.mail>
Subject: Password Reset
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the URL below.
http://localhost:3000/password_resets/DeStUAsv2QTX_SR3ub_N0g/edit
If you did not request your password to be reset please ignore this email and your password will stay as it is.
Redirected to http://localhost:3000/
Completed 302 Found in 1889ms

The email includes a link that contains the password reset URL. This URL includes the reset token as the id parameter.

Next w

e have to write the edit action. In it we’ll fetch the user by their reset token. Note that we use the method with an exclamation mark so that if the user isn’t found a 404 error is thrown.

/app/controllers/password_resets_controller.rb

def edit
  @user = User.find_by_password_reset_token!(params[:id])
end

In the associated view we’ll create a form to allow the user to reset their password.

/app/views/password_resets/edit.html.erb

<h1>Reset Password</h1>
<%= form_for @user, :url => password_reset_path(params[:id]) ↵
  do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2>Form is invalid</h2>
      <ul>
        <% for message in @user.errors.full_messages %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>
  <div class="field">
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </div>
  <div class="actions"><%= f.submit "Update Password" %></div>
<% end %>

We use form_for in this form as we’re modifying a resource. Because of this we have to explicitly set the :url parameter so that the form isn’t POSTed to the UsersController. Instead it is sent to the PasswordResetsController’s update action, passing in the reset token as the id. The form contains a section for displaying any error messages and fields for entering and confirming the new password.

We’ll write the update action next. This first checks that the password reset token is less than two hours old; if it isn’t then we redirect the user to the reset form again. If the token is recent enough we then try to update the user. If this is successful we redirect to the home page and show a message; if not there must have been an error in the form so we render it again.

/app/controllers/password_resets_controller.rb

def update
  @user = User.find_by_password_reset_token!(params[:id])
  if @user.password_reset_sent_at < 2.hours.ago
    redirect_to new_password_reset_path, :alert => "Password ↵ 
      reset has expired."
  elsif @user.update_attributes(params[:user])
    redirect_to root_url, :notice => "Password has been reset."
  else
    render :edit
  end
end

We can try this out by pasting the URL from the reset email into the browser.

Resetting our password.

If we enter non-matching passwords we’ll see an error message but when we submit the form correctly our password is successfully reset.

Our password has been reset.

We can use this password reset idea to add other features, for example to confirm new account registrations. This is very similar to password resets but instead of resetting the password when the link is clicked we set a flag in the database to say that the account is confirmed as registered.

That’s it for this episode on remembering logins and resetting passwords. Some tools, such as Devise, provide this functionality but it can be useful to do it from scratch especially if you’re going to need to do a lot of customization.