Add reset password functionality to your Rails app using temporary tokens
- Add a
Forgot password?
link to your view that includes the form which links toforgot_passwords_path
. This is usually in your login form. - Add the route
get 'forgot_passwords', to: 'forgot_passwords#new'
- Add resources as well:
resources :forgot_passwords, only: [:create]
andresources :password_resets, only: [:show, :create]
(Note that we will create a new controllerforgot_passwords_controller.rb
. This will be a virtual resource, since we don’t have a model/table forforgot_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.) - Also add the route
get 'forgot_password_confirmation', to: 'forgot_passwords#confirm'
andget 'expired_token', to: 'password_resets#expired_token'
- Create the controller called
forgot_passwords_controller.rb
and add thenew
andcreate
actions. - In the views create the folder and file
forgot_passwords/new.html.haml
-
Add a form to
forgot_passwords/new.html.haml
that submits toforgot_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"
-
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
-
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 -->
-
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.
-
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 # ...
-
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
-
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 # ...
-
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.
-
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
-
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"
-
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"