This tutorial will help you set up password reset restfully, with Authlogic. This tutorial is based on this blog post. Hopefully, having the tutorial as a Git repository, it will be more up to date.
To reset a password, the user goes through these steps:
- The user requests a password reset
- An email is sent to the user with instructions
- The user verifies their identity by using the link in the email
- The user is presented with a simple form for updating the password
This tutorial includes all code, including tests. Below you'll see the tools that are used. If you're not familiar with any of them, check them out or replace them with your favorite tools.
Note that this tutorial assumes you have your user and user session stuff already set up.
Perishable token as the name implies is a temporary string. Authlogic use it for simple identification. Add a column called perishable_token to your table and Authlogic will basically handle the rest.
Generate the migration: $ script/generate migration add_perishable_token_to_users
The migration contents: class AddPerishableTokenToUsers < ActiveRecord::Migration def self.up add_column :users, :perishable_token, :string, :default => "", :null => false add_index :users, :perishable_token end
def self.down
remove_column :users, :perishable_token
end
end
Don't forget to migrate the database: $ rake db:migrate
Next up we add a controller called password resets: $ script/generate controller password_resets
With this contents: class PasswordResetsController < ApplicationController # Method from: http://github.com/binarylogic/authlogic_example/blob/master/app/controllers/application_controller.rb before_filter :require_no_user before_filter :load_user_using_perishable_token, :only => [ :edit, :update ]
def new
end
def create
@user = User.find_by_email(params[:email])
if @user
@user.deliver_password_reset_instructions!
flash[:notice] = "Instructions to reset your password have been emailed to you"
redirect_to root_path
else
flash.now[:error] = "No user was found with email address #{params[:email]}"
render :action => :new
end
end
def edit
end
def update
@user.password = params[:password]
# Only if your are using password confirmation
# @user.password_confirmation = params[:password]
if @user.save
flash[:success] = "Your password was successfully updated"
redirect_to @user
else
render :action => :edit
end
end
private
def load_user_using_perishable_token
@user = User.find_using_perishable_token(params[:id])
unless @user
flash[:error] = "We're sorry, but we could not locate your account"
redirect_to root_url
end
end
end
Add this route: map.resources :password_resets, :only => [ :new, :create, :edit, :update ]
<h1>Reset Password</h1>
<p>Please enter your email address below and then press "Reset Password".</p>
<% form_tag password_resets_path do %>
<%= text_field_tag :email %>
<%= submit_tag "Reset Password" %>
<% end %>
%h1 Reset Password
%p Please enter your email address below and then press "Reset Password".
- form_tag password_resets_path do
= text_field_tag :email
= submit_tag "Reset Password"
<h1>Update your password</h1>
<p>Please enter the new password below and then press "Update Password".</p>
<% form_tag password_reset_path, :method => :put do %>
<%= password_field_tag :password %>
<%= submit_tag "Update Password" %>
<% end %>
%h1 Update your password
%p Please enter the new password below and then press "Update Password".
- form_tag password_reset_path, :method => :put do
= password_field_tag :password
= submit_tag "Update Password"
To use this test directly you need the method should_require_no_user. Read the blog post Testing User Privileges With Shoulda for more information. If you feel that it is unnessesary, just remove those four lines from the test.
class PasswordResetsControllerTest < ActionController::TestCase
request_new = proc do
get :new
end
request_create = proc do
post :create, :email => Factory(:user).email
end
request_edit = proc do
get :edit, :id => Factory(:user).perishable_token
end
request_update = proc do
@user = Factory(:user)
put :update, :id => @user.perishable_token, :user => { :password => "newpassword" }
end
# Remove these if you don't want to include the Shoulda macros
should_require_no_user "on GET to :new", &request_new
should_require_no_user "on POST to :create", &request_create
should_require_no_user "on GET to :edit", &request_edit
should_require_no_user "on PUT to :update", &request_update
context "when not logged in" do
context "on GET to :new" do
setup &request_new
should_respond_with :success
should_render_template :new
end
context "on POST to :create" do
setup &request_create
should_assign_to :user
should_respond_with :redirect
should_redirect_to("the root path") { root_path }
should "send an email" do
assert_sent_email
end
end
context "on GET to :edit" do
setup &request_edit
should_assign_to :user
should_respond_with :success
should_render_template :edit
end
context "on PUT to :update" do
setup &request_update
should_assign_to :user
should_respond_with :redirect
should_redirect_to("the users profile") { @user }
end
end
end
As you might noticed, in the password resets controller there is a method call on the user to deliver_password_reset_instructions!.
Lets add that method to the user model: class User < ActiveRecord::Base def deliver_password_reset_instructions! reset_perishable_token! Notifier.deliver_password_reset_instructions(self) end end
And test it: class UserTest < ActiveSupport::TestCase context "A user" do setup { @user = Factory(:user) }
context "Delivering password instructions" do
setup { @user.deliver_password_reset_instructions! }
should_change("perishable token") { @user.perishable_token }
should "send an email" do
assert_sent_email
end
end
end
end
Add the mailer method: class Notifier < ActionMailer::Base def password_reset_instructions(user) subject "Password Reset Instructions" from "[email protected]" recipients user.email content_type "text/html" sent_on Time.now body :edit_password_reset_url => edit_password_reset_url(user.perishable_token) end end
<h1>Password Reset Instructions</h1>
<p>
A request to reset your password has been made. If you did not make
this request, simply ignore this email. If you did make this
request, please follow the link below.
</p>
<%= link_to "Reset Password!", @edit_password_reset_url %>
%h1 Password Reset Instructions
%p
A request to reset your password has been made. If you did not make
this request, simply ignore this email. If you did make this
request, please follow the link below.
= link_to "Reset Password!", @edit_password_reset_url
Test the mailer: class NotifierTest < ActionMailer::TestCase context "delivering password reset instructions" do setup do @user = Factory(:user) @user.deliver_password_reset_instructions! end
should "send an email" do
assert_sent_email do |email|
email.subject =~ /Password Reset Instructions/
email.body =~ /#{@user.perishable_token}/
email.body =~ /Password Reset Instructions/
end
end
end
end
And at last, you might also want to create a Cucumber test for it. Note that you need the Email Spec plugin for this feature.
Feature: Password Reset
In order to retrieve a lost password
As a user of this site
I want to reset it
Scenario: Reset password
Given I am not logged in
And a user exists with email: "[email protected]", password: "password"
And I am on the login page
Then I should see "Forgot Password"
When I follow "Forgot Password"
Then I should see "Reset Password"
And I should see "Please enter your email address below"
When I fill in "email" with "[email protected]"
And I press "Reset Password"
Then I should see "Instructions to reset your password have been emailed to you"
And "[email protected]" should have an email
When I open the email
Then I should see "Password Reset Instructions" in the email body
When I follow "Reset Password" in the email
Then I should see "Update your password"
When I fill in "password" with "newpassword"
And I press "Update Password"
Then I should see "Your password was successfully updated"
When I am not logged in
Then I should be able to log in with email "[email protected]" and password "newpassword"
Scenario: Reset password no account
Given I am not logged in
And I am on the login page
Then I should see "Forgot Password"
When I follow "Forgot Password"
Then I should see "Reset Password"
And I should see "Please enter your email address below"
When I fill in "email" with "[email protected]"
And I press "Reset Password"
Then I should see "No user was found with email address [email protected]"
Given /^I am not logged in$/ do
visit logout_path
end
Then /^I should be able to log in with email "([^\"]*)" and password "([^\"]*)"$/ do |email, password|
UserSession.new(:email => email, :password => password).save.should == true
end
# If you use Pickle (http://github.com/ianwhite/pickle) this step is already defined
Given /^a user exists with email: "([^\"]*)", password: "([^\"]*)"$/ do |email, password|
Factory(:user, :email => email, :password => password)
end
Feel free to contact me if there's anything in the tutorial that is incorrect or if you have any good improvement suggestions.
If you liked this tutorial, I can also recommend the Authlogic Activation Tutorial.