How to Implement Password Reset Functionality In Your Rails Application

Arvin Fernandez
7 min readAug 23, 2021
Photo borrowed from litmus.com

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'

end
end

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.

--

--

Arvin Fernandez

Junior Developer trying to help other developers(and myself) better understand programming.