How to Implement Password Reset Functionality In Your Rails Application
Last week I took on a coding activity from a company and one of the requirements was to implement a password reset feature. Since I have never done this, I imagined this to be a difficult task. An email has to be sent to the user and authentication between the email and app must happen. I didn’t know how to do either.
Fortunately it was on Rails, which makes it super easy to implement this feature. And after a short google session I found the necessary resources.
In this post I will be discussing the steps required to achieve this functionality. Assuming you have your user model set up already, of course. Let’s dive into it!
Draw Your Routes
The first thing you’re going to want to do is to get your routes set up in your routes.rb
file so that when the user clicks that ‘Forgot your password’ link they are sent to the appropriate place and not a 404 page.
All of these routes will be sent to a PasswordResetsController which we will generate later on.
/password/reset
For this path you’re going to want to create 2 routes, a GET and a POST. The GET will be sent to password_resets#new
which will be responsible for rendering a simple form.
The POST will be taken care of in password_resets#create
which will be used to send the email the user entered in the form.
/password/reset/edit
Once we verify the user’s email(don’t worry we will get to that later) they will receive a link in the email provided which redirects them to a new form. Allowing them to enter their new password.
For this we will need another form which will require us draw 2 more routes. A GET and a PATCH which will be sent to password_resets#edit
and password_resets#update
, respectively.
When all is said and done you should have these 4 new routes in your routes.rb
file:
config/routes.rbRails.application.routes.draw do
...
get '/password/reset', to: 'password_resets#new' post '/password/reset', to: 'password_resets#create' get '/password/reset/edit', to: 'password_resets#edit' patch '/password/reset/edit', to: 'password_resets#update'
end
Generate a PasswordMailer
First run rails g mailer Password reset
in your command line.
By the name of it you can probably guess what this class will be responsible for. The PasswordMailer is a class which inherits from ActiveRecord’s ActionMailer and gives us the ability to send emails from our application.
They work very similar to controllers in the sense that you must define methods with corresponding views.
Create a Reset Method
Go to your PasswordMailer located app/mailers/password_mailer.rb
. You would already see a reset
method with some code inside of it because of the argument passed to the generator command above.
We are going to change it so that it sends a reset email to the address the user entered.
def reset
@user = params[:user]
mail to: @user.email_address
end
We have access to that params[:user]
because of the controller that called this mailer and corresponding method. Which you will see later on when we create our PasswordResetsController
, bear with me.
Now we need a token. A way to verify that user is who they claim to be and it has to be associated to the user so that when returned by the client the server can map it to the correct user.
We could pass in the id but someone could easily manipulate the link and change some other user’s password. But Rails is awesome and comes with tools to generate a token.
Using the signed_id
method we can generate a token that is tamper proof and can be directly associated to the user.
User.first.signed_id
=> "eyJfcmapbHMiOnsibWVzc2FnZSI6Ik1RPT0iLCJleHAiOm51bGwsInB1ciI6InVzZXIifX0=--93626b24c19e6ef1f12b0022500711a5ceab9f43e549c3af145ba027b60a4d49e"
And as if thats not secure enough we can be extra safe and pass in some expires_in
and purpose
parameters.
Our reset
method should now look like this:
def reset
@user = params[:user]
@token = @user.signed_id(purpose: 'password_reset', expires_in: 15.minutes)
mail to: @user.email_address
end
Create Your Reset
Views
You should see two reset
views under app/views/password_mailer
one text and the other html. Mailers have an extra text.erb
file so that if the client is unable to render the html they still have something to fall back on.
You can write something like this:
app/views/password_mailer/reset.html.erbHi <%= @user.name %>
<br>
Someone requested a reset of your password.
<br>
If this was you, click on the link to reset your password. The link will expire automatically in 15 minutes.
<br><%= link_to "Reset Password", password_reset_edit_url(token: @token) %>
Remember that text files don’t support html so your ActionView helper methods won’t render correctly if you try to use them. Ruby code is valid as long as it doesn’t return html.
app/views/password_mailer/reset.text.erbHi <%= @user.name %>Someone requested a reset of your password.If this was you, click on the link to reset your password. The link will expirte automatically in 15 minutes.<%= password_reset_edit_url(token: @token) %>
Notice how we used password_reset_edit_url
instead of password_reset_edit_path
. This is because mailers are not part of a request and have no idea what the domain of your application is. Therefore we must provide the full URL with path and domain.
Also note how the token is included so that when the user clicks the link in the email the server is able to authenticate the user using params[:token]
We must also make one more change in our development.rb
file. Right before the last end
add this line:
app/config/environments/development.rbconfig.action_mailer.default_url_options = {host: 'localhost:3000'}
This line is setting localhost:3000
as your default url host for your development environment.
Generate a PasswordResetsController
In your command line, run the rails g controller PasswordResets new create edit update
.
After running the command above you should have 4 new views under password_resets
folder in addition to the new PasswordResetsController. You’re only going to need new
and edit
so feel free to delete those other ones.
password_resets#new
This action won’t do much. The only responsibility of this action will be to render a form which will ask the user for the email address associated with the account.
If you follow rails convention, and you should unless you absolutely can’t, it shouldn’t contain any code inside. By default rails will try to render a view with the same name as the action, new
.
Inside new.html.erb
, create a form which asks the user for an email address and makes a POST request to the password_reset_path
. It should look something like this:
app/views/password_resets/new.html.erb<h2>Forgot your password?</h2><%= form_with url: password_reset_path do |form| %> <%= form.label :email_address %> <%= form.text_field :email_address %> <%= form.submit 'Reset Password' %><%end%>
After this you should add a link_to ‘Forgot your password?’, password_reset_path
somewhere in your login view which will redirect them to this form.
password_resets#create
This is where it starts to get interesting.
First we want to check if the email the user entered actually exists in our database using the params hash information. If it does we are going to send them an email with the reset link.
After, we want to redirect them to the home page and show them a message saying that if the user was found an email was sent to them.
app/controllers/password_resets_controller.rbclass PasswordResetsController < ApplicationController
... def create if @user = User.find_by_email_address(params[:email_address])
PasswordMailer.with(user: @user).reset.deliver_later
end redirect_to '/login', notice: 'If an account with that email was found, we have sent a link to reset password'
endend
Notice how we pass @user
to PasswordMailer using the with
method so that it can be accessed in params. Then we call the reset
method we defined earlier. And finally we send the email using deliver_later
, an asynchronous method. Because sending an email can take a while we use deliver_later
instead of deliver_now
.
For security purposes we should give the notice every time. Regardless of the user existing in our database, we don’t wan’t to expose that an account with that email address actually exists. The same reason why we don’t specify whether the email or password was invalid in a login attempt.
password_resets#edit
When the user clicks the link provided in the email they should be redirected to the form to reset the password but first we must authenticate the users token.
Using find_signed
we can look up a user using a the token generated by signed_id
. Because we specified a purpose when we generated it we must also provide it here.
@user = User.find_signed(params[:token], purpose: 'password_reset')
If the token expires, find_signed
will fail silently setting our @user
variable to nil
.
Instead user the bang version, find_signed!
, which raises an ActiveSupport::MessageVerifier::InvalidSignature
exception when the token is expired. Then handle use rescue
to handle the error, like so:
def edit @user = User.find_signed!(params[:token], purpose: 'password_reset')rescue ActiveSupport::MessageVerifier::InvalidSignature redirect_to '/login', alert: 'Your token has expired. Please try again'end
Now let’s generate that form.
In your edit.html.erb
add this:
<h2>Reset your password</h2><%= form_with model: @user, url: password_reset_edit_path(token: params[:token]) do |form| %> <div class="field"> <%= form.label :password %> <%= form.password_field :password %> </div> <div class="field"> <%= form.label :password_confirmation %> <%= form.password_field :password_confirmation %> </div> <%= form.submit 'Reset Password' %><%end%>
Since the user is not logged in we will need to include the token once again in our patch request. So that our corresponding action can look up the user and update their password.
The reason we use the user model here is so that the password_field
and password_confirmation_field
names are correctly nested under user allowing us to use a strong params method.
password_resets#update
Here we do the same as edit
to find our user with the token. Then update the password with whatever the user entered.
def update @user = User.find_signed!(params[:token], purpose: 'password_reset') if @user.update(password_params) redirect_to '/login', notice: 'Your password was reset succesfully. Please sign in.' else render 'edit' endrescue ActiveSupport::MessageVerifier::InvalidSignature redirect_to '/login', alert: 'Your token has expired. Please try again'end
Make sure to create a password_params
method under private so that the information from the form can be used to update the password.
If passwords don’t match we re-render the form and display errors.
Conclusion
Your users can now reset their passwords. Test out the functionality for yourself. Once you enter an email that is associated to one of your users and hit submit you will be able to see the email that is sent out in your logs.