#274 Remember Me & Reset Password
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (17.5 MB)
- m4vSmaller H.264 Video (11.9 MB)
- webmFull Size VP8 Video (14.3 MB)
- ogvFull Size Theora Video (27.6 MB)
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 users log in to our application, their id
is stored in the session. This takes place in the SessionsController
’s create
action.
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 logged-in users close 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 id
s 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.
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 self[column] = SecureRandom.urlsafe_base64 end while User.exists?(column => self[column]) end end
To create the token we use ActiveSupport
’s SecureRandom
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.
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.
<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.
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.
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.
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.
<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
.
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:
<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.
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.
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.
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.
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
.
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.
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 we 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.
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.
<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.
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.
If we enter non-matching passwords we’ll see an error message but when we submit the form correctly our password is successfully 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.