cult3

Implementing Password Reset using Ruby on Rails and Trailblazer

Jul 13, 2016

Table of contents:

  1. An overview of how this is going to work
  2. Setting up the routes
  3. Setting up the database table
  4. Requesting a password reset email
  5. Resetting the password
  6. Conclusion

Allowing your users to reset their password is one of the foundational bits of functionality of just about every type of web application. It’s usually not that interesting to implement because 9 times out 10 the functionality is exactly the same for all applications.

However, this makes a great example of something we can build using Trailblazer because it’s a well known bit of functionality and it’s generally applicable to all types of web application!

In today’s tutorial we will be looking at implementing password reset using Ruby on Rails and Trailblazer.

An overview of how this is going to work

Before we jump into the code, first I want to give a quick high-level overview of how this is going to work.

There are basically two separate actions that we are going to need to build.

First, the user should be able to request a password reset email that contains a link with a unique token. We will need to create the process for accepting the user’s email address, generating the token, and then sending the email to the user.

Secondly, the email will contain a link back to the application. We need to be able to accept this token, and then accept a new password for the user.

So as you can see, this is functionality is comprised into two distinct processes.

Setting up the routes

Before I start implementing the functionality for resetting a user’s password, first I’m going to add the routes that I’m going to need.

I often find adding something high-level such as application routes helps me clarify in my head what I need to build:

Rails
  .application
  .routes
  .draw do
    # Password Reset
    get 'password-reset/request', to: 'password_reset/request#new'
    post 'password-reset/request', to: 'password_reset/request#create'
    get 'password-reset/reset', to: 'password_reset/reset#new'
    post 'password-reset/reset', to: 'password_reset/reset#create'
  end

As you can see, the two processes are sent to the request and reset URLs respectively, which are both nested under password-reset.

We need two GET request URLs for display the form for requesting a password reset email, and submitting a new password. And we need two POST request URLs for accepting the form request.

So hopefully the overview and seeing the routes has made understanding how this is going to work reasonably clear. The remainder of this tutorial will be split into two parts, requesting a token and resetting the password.

Setting up the database table

The first thing we need to do is to create a new database table to store the password reminder requests:

bin/rails g model PasswordReminder token:string user:references expires_at:timestamp

Here I’m using the Rails generator to create a new PasswordReminder model with fields for the token and expires_at, and an association to the User model. This command will create the model file, the migration, and a test file.

Each password reminder will have a unique token that is generated on creation. This token will be used in the URL that is used to identify the user when they click on the link from the email.

I’m also including an expires at timestamp. This will be automatically set to 1 hour into the future when the reminder is created. This will allow us to delete out expired reminders, and avoid any unnecessary security problems that may arise if we left them hanging around.

Requesting a password reset email

Next up we need to create a new Operation for requesting a password reset email. To do this, we will need to create a new Trailblazer Operation. We’ve looked at creating Operations in Getting started with Operations in Trailblazer.

Create a new directory under concepts called password_reset and another file under that directory called request:

module PasswordReset::Request
  class Operation < Trailblazer::Operation
  end
end

I won’t go into a lot of detail with this class as we’ve already covered the fundamentals of Trailblazer Operations and Contracts in What are Trailblazer Contracts?.

First I instruct the Operation that this is a create request and we’re creating a new PasswordReminder:

module PasswordReset::Request
  class Operation < Trailblazer::Operation
    include Model
    model PasswordReminder, :create
  end
end

This simply means we don’t have to manually create the new model object

Next I define the contract:

module PasswordReset::Request
  class Operation < Trailblazer::Operation
    include Model
    model PasswordReminder, :create

    contract do
      undef persisted?
      attr_reader :user

      property :email, virtual: true

      validates :email, presence: true, email: true
      validate :find_user_by_email

      def find_user_by_email
        @user = User.find_by(email: email)

        errors.add(:email, :not_found) unless @user
      end
    end
  end
end

In this situation I’m expecting the user to submit their email address. The email address is not a property on the PasswordReminder model, and so I need to set this property as virtual.

If the email has been provided I can use it to find the user, but if the email is not a registered user I can add an error to the errors property. Otherwise I will set the user instance property

With the contract all set up I can now define the process method that will deal with the processing of this operation:

require 'bcrypt'

module PasswordReset::Request
  class Operation < Trailblazer::Operation
    include Model
    model PasswordReminder, :create

    contract do
      undef persisted?
      attr_reader :user

      property :email, virtual: true

      validates :email, presence: true, email: true
      validate :find_user_by_email

      def find_user_by_email
        @user = User.find_by(email: email)

        errors.add(:email, :not_found) unless @user
      end
    end

    def process(params)
      validate(params[:user]) do |f|
        remove_existing_tokens
        generate_token
        set_expiry_timestamp
        associate_user
        f.save
        send_password_reset_email
        remove_expired_tokens
      end
    end

    def remove_existing_tokens
      PasswordReminder.delete_all(user_id: contract.user.id)
    end

    def generate_token
      model.token = SecureRandom.urlsafe_base64
    end

    def set_expiry_timestamp
      model.expires_at = Time.now + 1.hour
    end

    def associate_user
      model.user = contract.user
    end

    def send_password_reset_email
      UserMailer.password_reset(model).deliver_later
    end

    def remove_expired_tokens
      PasswordReminder.delete_all("expires_at < '#{Time.now}'")
    end
  end
end

First I will validate the incoming data and ensure that it is correct. This is basically just ensuring that the email is a valid email address for a registered user.

Next we can step through the process of the Operation:

def remove_existing_tokens
  PasswordReminder.delete_all(user_id: contract.user.id)
end

First I will remove any existing tokens that have already been created for that user. I only want there to be one password reset token per user.

def generate_token
  model.token = SecureRandom.urlsafe_base64
end

Next I will generate a new token for the new PasswordReminder that is getting created in this process.

def set_expiry_timestamp
  model.expires_at = Time.now + 1.hour
end

Next I will set the expiry timestamp to be the current time plus 1 hour.

def associate_user
  model.user = contract.user
end

Next I will associate the user from the contract lookup method to the PasswordReminder object.

After this method has been called we have everything in place to save the new PasswordReminder to the database.

Next I will send the password reset email:

def send_password_reset_email
  UserMailer.password_reset(model).deliver_later
end

And finally I will use this opportunity to remove any password reminder tokens that have expired:

def remove_expired_tokens
  PasswordReminder.delete_all("expires_at < '#{Time.now}'")
end

Adding the Email

In the previous section we sent an email using the UserMailer. Before I write my tests I’m just going to add this in. Strictly speaking, I shouldn’t be doing it in this order, but life isn’t always TDD.

First I will add a new method to the UserMailer class that I generated in Building out a User Confirmation flow in Trailblazer:

class UserMailer < ApplicationMailer
  def confirm_membership(user)
    @user = user
    @url = confirmation_confirm_url(token: @user.confirmation_token)
    mail(to: @user.email, subject: 'Confirm your Culttt Membership!')
  end

  def password_reset(password_reminder)
    @user = password_reminder.user
    @url = password_reset_reset_url(token: password_reminder.token)
    mail(to: @user.email, subject: 'Your Culttt password reset!')
  end
end

In this method I’m accepting the PasswordReminder, grabbing the user and setting it as an instance property of the class.

Next I’m going to generate the password reset url using the token from the reminder.

Finally I will call the mail method and pass the user’s email and my subject line.

To finish this part of the process off I will also need to create a new view for the email.

Create a new file under the user_mailer directory called password_reset.html.slim:

p Hey, p Click #{@url} to reset your password.

The Operation Tests

Now that we have the Operation and the email set up, I will write a couple of tests to make sure everything is working correctly:

require 'test_helper'

module PasswordReset::RequestTest
  class OperationTest < ActiveSupport::TestCase
    test 'require email' do
      res, op = PasswordReset::Request::Operation.run(user: {})

      assert_not(res)
      assert_includes(op.errors[:email], "can't be blank")
    end

    test 'require registered email' do
      res, op =
        PasswordReset::Request::Operation.run(
          user: {
            email: 'name@domain.com'
          }
        )

      assert_not(res)
      assert_includes(op.errors[:email], 'not found')
    end

    test 'create password reset' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model

      # Seed the db to ensure that existing reminders are removed
      PasswordReset::Request::Operation.run(user: { email: user.email })

      res, op =
        PasswordReset::Request::Operation.run(user: { email: user.email })

      mail = ActionMailer::Base.deliveries.last

      assert(res)
      assert_equal(op.model.user, user)
      assert_equal(user.email, mail[:to].to_s)
      assert_equal('Your Culttt password reset!', mail[:subject].to_s)
      assert_equal(1, PasswordReminder.count)
    end
  end
end

If you have been following along with these tutorials this should look pretty familiar by now. If not, I would recommend going back through the last couple of Trailblazer tutorials for a better understanding of what is going on here.

The first two tests are simply ensuring my validation rules are working as they should.

The final test is walking through the process of generating a new password reminder. In this test I’m also ensuring that any previous reminders for this user have been removed from the database. And I’m checking to make sure the email has been sent correctly.

Creating the Form

Next up we need to create the form that the user will submit. To do this I’m going to use a Cell. We have previously looked into using Trailblazer Cells in Getting started with Trailblazer Cells:

module PasswordReset::Request
  class Cell < Culttt::Cells::Form
    def show
      render
    end
  end
end

This Cell doesn’t need any fancy functionality so the Cell class is really simple.

Next I will need to create the view file:

- if form.errors.any? ul - form.errors.full_messages.each do |msg| li = msg =
form_tag password_reset_request_url, class: "password-reset-request-form" do |f|
div = label_tag :email, nil, class: "qa-email-label" = text_field_tag :email,
nil, class: "qa-email-input" div = submit_tag "Reset your password", class:
"qa-submit"

Again this is just a typical slim form view. If you have been following a long with this series this should look fairly familiar to you by now.

The nice thing about encapsulating the form in a Cell is that we can very easily write tests to ensure the form is created correctly:

require 'test_helper'

module PasswordReset::Request::CellTest
  class CellTest < Cell::TestCase
    controller Confirmation::RequestController

    test 'has correct markup' do
      html =
        concept(
          'password_reset/request/cell',
          PasswordReset::Request::Operation.present({})
        ).()

      html.must_have_selector('form.password-reset-request-form')
      html.must_have_selector('label.qa-email-label')
      html.must_have_selector('input.qa-email-input')
      html.must_have_selector('input.qa-submit')
    end
  end
end

In this test file I’m generating the markup for the form and then making assertions to ensure that the correct input fields have been generated correctly.

Adding the Controller

Now that we have the Operation and the Cell in place we can add the Controller to coordinate the request:

module PasswordReset
  class RequestController < ApplicationController
    def new
      form PasswordReset::Request::Operation
    end

    def create
      run PasswordReset::Request::Operation do |op|
        return redirect_to login_url
      end

      render :new, status: 400
    end
  end
end

This is another great example of how beautifully simple your Controllers will be if you decide to use Trailblazer.

At this point I’m also going to write a couple of Controller tests:

require 'test_helper'

module PasswordReset
  class RequestControllerTest < ActionController::TestCase
    test 'display form' do
      get :new

      assert_response(:success)
    end

    test 'fail with invalid email' do
      post :create, user: { email: 'invalid email' }

      assert_response(400)
    end

    test 'fail with not found email' do
      post :create, user: { email: 'name@domain.com' }

      assert_response(400)
    end

    test 'send password reset email' do
      @user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model

      assert_difference 'ActionMailer::Base.deliveries.size' do
        post :create, user: { email: @user.email }
      end

      assert_response(302)
      assert_redirected_to(login_url)
    end
  end
end

These tests simply walk through the process of invoking the Controller methods and asserting that the correct action is taken.

Testing the flow

Now that we have all of the request side functionality in place, I’m going to write a couple of integration tests to test the application is working from the outside in:

require 'test_helper'

module PasswordResetFlowsTest
  class RequestFlowsTest < ActionDispatch::IntegrationTest
    test 'attempt with invalid email' do
      get '/password-reset/request'
      assert_response :success

      post_via_redirect '/password-reset/request', user: { email: '' }
      assert_equal '/password-reset/request', path
      assert_response 400
    end

    test 'attempt with not found email' do
      get '/password-reset/request'
      assert_response :success

      post_via_redirect '/password-reset/request',
                        user: {
                          email: 'name@domain.com'
                        }
      assert_equal '/password-reset/request', path
      assert_response 400
    end

    test 'send the password reset request' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model

      get '/password-reset/request'
      assert_response :success

      assert_difference 'ActionMailer::Base.deliveries.size' do
        post_via_redirect '/password-reset/request', user: { email: user.email }
      end

      assert_equal '/login', path
      assert_response :success
    end
  end
end

Hopefully these integration tests are fairly self explanatory as they should provide documentation for how the application should be used from the perspective of the user from the outside.

In each test I’m basically just asserting that the user is redirected to the correct place based upon their actions.

Resetting the password

With the request process in place we can now turn our attention to the process which will allow the user to use the reminder to reset their password.

The first thing we will create will be the operation that will handle the request:

module PasswordReset::Reset
  class Operation < Trailblazer::Operation
    include Resolver
    include Model
    model PasswordReminder

    def self.model!(params)
      PasswordReminder.find_by!(token: params[:token])
    end

    contract do
      property :password, virtual: true

      validates :password, presence: true, length: { minimum: 8 }
    end

    def process(params)
      validate(params[:user]) do |f|
        generate_digest
        delete_reminder
      end
    end

    def generate_digest
      model.user.password_digest = BCrypt::Password.create(contract.password)
    end

    def delete_reminder
      PasswordReminder.delete(model.id)
    end
  end
end

There are a couple of important things to note with this operation

Firstly I’m overriding the self.model! method to find the password reminder by the token. The token should be supplied via the URL and so if the reminder is not found, we can just bail out of the operation.

Secondly, this operation is centered around the PasswordReminder model but we’re actually resetting the password on the User model. To deal with this we can set the password property to be virtual in the contract.

Finally we can use the typical process of the operation to handle the business logic. First we validate that the password meets the requirements of being present and at least 8 characters long.

Next we can generate the password digest and finally we can delete the password reminder from the database.

We can also write a couple of tests to ensure that the operation is working correctly:

require 'test_helper'

module PasswordReset::ResetTest
  class OperationTest < ActiveSupport::TestCase
    test 'require valid token' do
      assert_raises ActiveRecord::RecordNotFound do
        PasswordReset::Reset::Operation.run(token: '')
      end
    end

    test 'require password' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model
      reset =
        PasswordReset::Request::Operation.(user: { email: user.email }).model

      res, op =
        PasswordReset::Reset::Operation.run(token: reset.token, user: {})

      assert_not(res)
      assert_includes(op.errors[:password], "can't be blank")
    end

    test 'require valid password' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model
      reset =
        PasswordReset::Request::Operation.(user: { email: user.email }).model

      res, op =
        PasswordReset::Reset::Operation.run(
          token: reset.token,
          user: {
            password: 'abc'
          }
        )

      assert_not(res)
      assert_includes(
        op.errors[:password],
        'is too short (minimum is 8 characters)'
      )
    end

    test 'reset user password' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model
      reset =
        PasswordReset::Request::Operation.(user: { email: user.email }).model

      res, op =
        PasswordReset::Reset::Operation.run(
          token: reset.token,
          user: {
            password: 'password'
          }
        )

      assert(res)
      assert(user.reload.password_digest, BCrypt::Password.create('password'))
      assert_equal(0, PasswordReminder.count)
    end
  end
end

In the first test I’m ensuring that an Exception is thrown if the token is invalid. In the second and third tests I’m ensuring that the password is required and it should be at least 8 characters long. And finally in the last test I’m ensuring that the password is reset and the password reminder is removed from the database.

Creating the Cell

Next up we need to provide a form to allow the user to submit their new password. As usual we can encapsulate this in a Cell:

module PasswordReset::Reset
  class Cell < Culttt::Cells::Form
    def show
      render
    end
  end
end

Once again as this is such a simple example we don’t need to add anything to the Cell. Here is the accompanying view for the form:

- if form.errors.any? ul - form.errors.full_messages.each do |msg| li = msg =
form_tag password_reset_reset_url, class: "password-reset-reset-form" do |f| div
= label_tag :password, nil, class: "qa-password-label" = text_field_tag
:password, nil, class: "qa-password-input" div = submit_tag "Reset your
password", class: "qa-submit"

I will also include a test to ensure that the correct markup is generated:

require 'test_helper'

module PasswordReset::Reset::CellTest
  class CellTest < Cell::TestCase
    controller Confirmation::RequestController

    test 'has correct markup' do
      user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model
      reset =
        PasswordReset::Request::Operation.(user: { email: user.email }).model

      html =
        concept(
          'password_reset/reset/cell',
          PasswordReset::Reset::Operation.present(token: reset.token)
        ).()

      html.must_have_selector('form.password-reset-reset-form')
      html.must_have_selector('label.qa-password-label')
      html.must_have_selector('input.qa-password-input')
      html.must_have_selector('input.qa-submit')
    end
  end
end

Adding the Controller

With the Operation and the Cell in place we can now create the Controller:

module PasswordReset
  class ResetController < ApplicationController
    def new
      form PasswordReset::Reset::Operation
    end

    def create
      run PasswordReset::Reset::Operation do |op|
        return redirect_to login_url
      end

      render :new, status: 400
    end
  end
end

Once again the beauty of Trailblazer can be seen in how simple this Controller is.

At this point I will also write a couple of tests to ensure that everything is hooked up correctly:

require 'test_helper'

module PasswordReset
  class ResetControllerTest < ActionController::TestCase
    def setup
      @user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model
      @reset =
        PasswordReset::Request::Operation.(user: { email: @user.email }).model
    end

    test 'return 404 on invalid token' do
      assert_raises ActiveRecord::RecordNotFound do
        get :new
      end
    end

    test 'display form' do
      get :new, token: @reset.token

      assert_response(:success)
    end

    test 'fail with missing password' do
      post :create, token: @reset.token, user: {}

      assert_response(400)
    end

    test 'fail with invalid password' do
      post :create, token: @reset.token, user: { password: 'abc' }

      assert_response(400)
    end

    test 'reset user password' do
      post :create, token: @reset.token, user: { password: 'password' }

      assert_response(302)
      assert_redirected_to(login_url)
    end
  end
end

As you can see, these tests are really just the same tests as the Operation tests from earlier but at a higher level of abstraction.

Testing the flow

Finally I’m going to add a couple of integration tests to verify that the functionality is working from the outside in:

class ResetFlowsTest < ActionDispatch::IntegrationTest
  def setup
    @user = User::Create::Operation::Default.(user: attributes_for(:user)).model
    @reset =
      PasswordReset::Request::Operation.(user: { email: @user.email }).model
  end

  test 'attempt with invalid token' do
    assert_raises ActiveRecord::RecordNotFound do
      get '/password-reset/reset?token=invalid'
    end
  end

  test 'attempt with invalid password' do
    get "/password-reset/reset?token=#{@reset.token}"
    assert_response :success

    post_via_redirect '/password-reset/reset',
                      token: @reset.token,
                      user: {
                        password: 'abc'
                      }
    assert_equal '/password-reset/reset', path
    assert_response 400
  end

  test 'reset user password' do
    get "/password-reset/reset?token=#{@reset.token}"
    assert_response :success

    post_via_redirect '/password-reset/reset',
                      token: @reset.token,
                      user: {
                        password: 'password'
                      }
    assert_equal '/login', path
    assert_response :success
  end
end

Again, I’m essentially testing the same things again but at another level of abstraction. As we saw in Writing Integration Tests in Ruby on Rails, these tests are probably the most important because they show that each component of the application is working together correctly, and they also provide documentation to show how the application should work.

I think a good rule of thumb is that you should only be doing stuff in the browser once you’re very confident everything is working. It’s much easier to write a test, than it is to keep manually going through the process in the browser.

Conclusion

Phew that was a long tutorial, well done for getting this far, I hope it was worth it!

Resetting user passwords is something that almost all applications need in one form or another. Despite this being a kinda boring thing to implement, I think it is a good illustration of many of the things we’ve been looking at over the last couple of weeks.

So I hope today’s tutorial has inspired you to take Trailblazer for a spin for your next Ruby project!

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.