Jul 13, 2016
Table of contents:
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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
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.
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.
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!