Clients are now getting authenticated with every request and without a valid API key, their request won’t go through. We can now shift our attention to users. We want people to sign up on Alexandria before they order their ebooks.
Identifying, authenticating and authorizing users will be done in the next chapter. For now, we need to create the User
model and its associated controller. After that, we will add a way for users to confirm their accounts and reset their passwords.
We will be doing everything from scratch instead of using a gem like Devise
or Authlogic
. While I do not recommend rolling your own authentication every time you create a new application, my experience with the authentication gems for API-only applications has been disappointing. That’s why I’ve decided to show you how to build it from scratch so you understand the difference with the authentication system in a regular Rails application.
The User
model will have quite a lot of fields. We need to not only store user’s personal information, but also some metadata that will help the admin. We also need some columns to store tokens for the “confirmation” and “reset password” processes.
Here is the list of columns for the users
table.
id
email
password_digest
given_name
family_name
last_logged_in_at
confirmation_token
confirmation_redirect_link
confirmed_at
confirmation_sent_at
reset_password_token
reset_password_redirect_link
reset_password_sent_at
To secure the password, we will be using the bcrypt
gem associated with the has_secure_password
method.
Add the bcrypt
gem to your Gemfile
…
# Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.5.0'
gem 'rails', '5.2.0'
gem 'pg'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'carrierwave'
gem 'carrierwave-base64'
gem 'pg_search'
gem 'kaminari'
gem 'bcrypt', '~> 3.1.7'
# Hidden Code
and get it installed.
bundle install
Now, let’s generate the User
model.
rails g model User email:string password_digest:string given_name:string \
family_name:string last_logged_in_at:timestamp confirmation_token:string \
confirmed_at:timestamp confirmation_sent_at:timestamp \
reset_password_token:string reset_password_redirect_url:text \
reset_password_sent_at:timestamp role:integer
Output
Running via Spring preloader in process 36599
invoke active_record
create db/migrate/TIMESTAMP_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
invoke factory_bot
create spec/factories/users.rb
We need to add indexes and unique constraints to some of the columns in the users
table. More specifically, we need indexes on all the columns that we will use to search for users: email
for authentication, confirmation_token
for confirmation, and reset_password_token
for the “reset password” process.
# db/migrate/TIMESTAMP_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email, index: true, unique: true
t.string :password_digest
t.string :given_name
t.string :family_name
t.timestamp :last_logged_in_at
t.string :confirmation_token, index: true, unique: true
t.text :confirmation_redirect_url
t.timestamp :confirmed_at
t.timestamp :confirmation_sent_at
t.string :reset_password_token, index: true, unique: true
t.text :reset_password_redirect_url
t.timestamp :reset_password_sent_at
t.integer :role, default: 0
t.timestamps
end
end
end
Run the migration we just generated.
rails db:migrate && RAILS_ENV=test rails db:migrate
Let’s update the factory for the User
model.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
email { 'john@example.com' }
password { 'password' }
given_name { 'John' }
family_name { 'Doe' }
role { :user }
trait :confirmation_redirect_url do
confirmation_token { '123' }
confirmation_redirect_url { 'http://google.com' }
end
trait :confirmation_no_redirect_url do
confirmation_token { '123' }
confirmation_redirect_url { nil }
end
trait :reset_password do
reset_password_token { '123' }
reset_password_redirect_url { 'http://example.com?some=params' }
reset_password_sent_at { Time.now }
end
trait :reset_password_no_params do
reset_password_token { '123' }
reset_password_redirect_url { 'http://example.com' }
reset_password_sent_at { Time.now }
end
end
end
Now we can define the expectations for the behavior of the User
model.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, :type => :model do
let(:user) { build(:user) }
it 'has a valid factory' do
expect(build(:user)).to be_valid
end
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).ignoring_case_sensitivity }
it { should validate_presence_of(:password) }
it 'generates a confirmation token' do
user.valid?
expect(user.confirmation_token).to_not be nil
end
it 'downcases email before validating' do
user.email = 'John@example.com'
expect(user.valid?).to be true
expect(user.email).to eq 'john@example.com'
end
end
And here is the User
model. It’s pretty standard, but pay attention to how we generate the confirmation token when a user is created and how we downcase the email before the validation step.
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
before_validation :generate_confirmation_token, on: :create
before_validation :downcase_email
enum role: [:user, :admin]
validates :email, presence: true,
uniqueness: true,
length: { maximum: 255 },
format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
validates :password, presence: true, length: { minimum: 8 }, if: :new_record?
validates :given_name, length: { maximum: 100 }
validates :family_name, length: { maximum: 100 }
validates :confirmation_token, presence: true,
uniqueness: { case_sensitive: true }
private
def generate_confirmation_token
self.confirmation_token = SecureRandom.hex
end
def downcase_email
email.downcase! if email
end
end
Run the tests.
rspec spec/models/user_spec.rb
Success (GREEN)
User
has a valid factory
should validate that :email cannot be empty/falsy
should validate that :email is unique
should validate that :password cannot be empty/falsy
generates a confirmation token
downcases email before validating
Finished in 0.20229 seconds (files took 2.1 seconds to load)
6 examples, 0 failures
Looks good; now, let’s proceed with the user presenter.
UserPresenter
Before we can create the users
controller, we need a presenter to use all the awesome tools we implemented earlier.
Create a new file…
touch app/presenters/user_presenter.rb
and fill it with the following. We allow sorting, filtering and building with all the fields except confirmation_token
and reset_password_token
which cannot be used for sorting and filtering.
# app/presenters/user_presenter.rb
class UserPresenter < BasePresenter
FIELDS = [:id, :email, :given_name, :family_name, :role, :last_logged_in_at,
:confirmed_at, :confirmation_sent_at, :reset_password_sent_at,
:created_at, :updated_at]
sort_by *FIELDS
filter_by *FIELDS
build_with *[FIELDS.push([:confirmation_token, :reset_password_token,
:confirmation_redirect_url,
:reset_password_redirect_url])].flatten
end
Time to create the controller.
UsersController
Create the controller file and its spec file with the command below.
touch app/controllers/users_controller.rb \
spec/requests/users_spec.rb
Add the route to the config/routes.rb
file.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books, except: :put
resources :authors, except: :put
resources :publishers, except: :put
resources :users, except: :put
get '/search/:text', to: 'search#index'
end
root to: 'books#index'
end
Before we actually implement the controller, we need to add a mailer. When new users are created, we want an email to be sent to them so they can confirm their account.
Generate a new mailer.
rails g mailer UserMailer confirmation_email
Update the content of the app/mailers/application_mailer.rb
file with the following. We just changed the “from” email.
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: 'admin@alexandria.com'
layout 'mailer'
end
Next, we can add the code for the UserMailer
class that will be responsible for sending the confirmation email. It is also going to update the confirmation_sent_at
field of the user before sending.
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def confirmation_email(user)
@user = user
@user.update_column(:confirmation_sent_at, Time.now)
mail to: @user.email, subject: 'Confirm your Account!'
end
end
Finally, here is the template we are going to use to send the email. There’s not much to see, except for the link pointing back to the Alexandria API that will confirm the account using the generated confirmation token.
<% # app/views/user_mailer/confirmation_email.text.erb %>
Hello,
Confirm your email by clicking on the link below:
CONFIRMATION LINK HERE
Welcome to Alexandria!
Update the tests for this email according to our changes.
# spec/mailers/user_mailer_spec.rb
require 'rails_helper'
RSpec.describe UserMailer, :type => :mailer do
describe 'confirmation_email' do
let(:user) { create(:user) }
let(:mail) { UserMailer.confirmation_email(user) }
it 'renders the headers' do
expect(mail.subject).to eq('Confirm your Account!')
expect(mail.to).to eq([user.email])
expect(mail.from).to eq(['admin@alexandria.com'])
end
it 'renders the body' do
expect(mail.body.encoded).to match('Hello')
end
end
end
You can delete the HTML template generated for the confirmation email; we won’t be using it.
rspec spec/mailers/user_mailer_spec.rb
Success (GREEN)
...
UserMailer
confirmation_email
renders the headers
renders the body
Finished in 0.41258 seconds (files took 2.46 seconds to load)
2 examples, 0 failures
Let’s take a look at the users controller now. This time, I’m not going to show you how to write the tests. I believe you have the skills to write them yourself and you can reuse the code we wrote for the books controller. Feel free to check the code below before writing the tests.
# spec/requests/users_spec.rb
require 'rails_helper'
RSpec.describe 'Users', type: :request do
before do
allow_any_instance_of(UsersController).to(
receive(:validate_auth_scheme).and_return(true))
allow_any_instance_of(UsersController).to(
receive(:authenticate_client).and_return(true))
end
let(:john) { create(:user) }
let(:users) { [john] }
describe 'GET /api/users' do
end
describe 'GET /api/users/:id' do
end
describe 'POST /api/users' do
end
describe 'PATCH /api/users/:id' do
end
describe 'DELETE /api/users/:id' do
end
end
Most of the code is similar to the controllers we’ve built before except for the create
action. There, we use…
UserMailer.send_confirmation(user).deliver_now
to send the confirmation email right away. In real-world applications, this kind of side action should be handled in a background process in order to return a response to the client as fast as possible.
To delay it, we could use deliver_later
instead of deliver_now
, but that would require setting up ActiveJob
and something like Sidekiq
which is outside of the scope of this book.
Another thing to note in this controller is in the user_params
method:
def user_params
params.require(:data).permit(:email, :password,
:given_name, :family_name,
:role, :confirmation_redirect_link)
end
We allow a parameter named confirmation_redirect_link
to be sent and saved in the user record. But what is this field for? Well, it’s meant to contain a redirect URL sent by the client to which the server will redirect once the client has confirmed its account.
For example, let’s say a user signs up using our React front-end application. This user receives an email containing a confirmation link pointing to Alexandria. Once confirmed, we want to give the option to the client to redirect the user where it wants to. For the React front-end, that would be the login page available at http://alexandria/login
(for example). For the iOS application, it could be an URI pointing that will trigger the application to open on the login page.
We will see in the next section what the server will do if the client doesn’t provide a redirect URL.
Here is the complete code of the users controller.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
users = orchestrate_query(User.all)
render serialize(users)
end
def show
render serialize(user)
end
def create
if user.save
UserMailer.confirmation_email(user).deliver_now
render serialize(user).merge(status: :created, location: user)
else
unprocessable_entity!(user)
end
end
def update
if user.update(user_params)
render serialize(user).merge(status: :ok)
else
unprocessable_entity!(user)
end
end
def destroy
user.destroy
render status: :no_content
end
private
def user
@user ||= params[:id] ? User.find_by!(id: params[:id]) : User.new(user_params)
end
alias_method :resource, :user
def user_params
params.require(:data).permit(:email, :password,
:given_name, :family_name,
:role, :confirmation_redirect_url)
end
end
If you’ve written some tests, it’s time to run them.
rspec spec/requests/users_spec.rb
Success (GREEN)
...
Finished in 0.94902 seconds (files took 2.27 seconds to load)
21 examples, 0 failures
Users can now be created. We are not going to create a registrations
controller to handle signups. Instead, we trust the client to implement that part and all we want is a call to POST /api/users
with valid parameters.
But we need to ensure that users’ emails are correct before we enable their accounts. The only way to do this is by sending an email using the function we implemented.
Well, it’s time to create this user_confirmations
controller!
touch app/controllers/user_confirmations_controller.rb \
spec/requests/user_confirmations_spec.rb
Let’s not forget to add the route as well. We only need the show
action for this controller. We rename the parameter in the URL from confirmation_token
to id
with param: :confirmation_token
.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books, except: :put
resources :authors, except: :put
resources :publishers, except: :put
resources :users, except: :put
resources :user_confirmations, only: :show, param: :confirmation_token
get '/search/:text', to: 'search#index'
end
root to: 'books#index'
end
Let’s write a few simple tests for this new resource. Notice how we use the FactoryBot
traits we created in the users
factory earlier.
# spec/requests/user_confirmations_spec.rb
require 'rails_helper'
RSpec.describe 'UserConfirmations', type: :request do
describe 'GET /api/user_confirmations/:confirmation_token' do
context 'with existing token' do
context 'with confirmation redirect url' do
subject { get "/api/user_confirmations/#{john.confirmation_token}" }
let(:john) { create(:user, :confirmation_redirect_url) }
it 'redirects to http://google.com' do
expect(subject).to redirect_to('http://google.com')
end
end
context 'without confirmation redirect url' do
let(:john) { create(:user, :confirmation_no_redirect_url) }
before { get "/api/user_confirmations/#{john.confirmation_token}" }
it 'returns HTTP status 200' do
expect(response.status).to eq 200
end
it 'renders "Your are now confirmed!"' do
expect(response.body).to eq 'You are now confirmed!'
end
end
end
context 'with nonexistent token' do
before { get '/api/user_confirmations/fake' }
it 'returns HTTP status 404' do
expect(response.status).to eq 404
end
it 'renders "Token not found"' do
expect(response.body).to eq 'Token not found'
end
end
end
end
We also need to add a method, confirm
, to the User
model to update the fields that need to be updated once a user has been confirmed.
# app/models/user.rb
class User < ApplicationRecord
# Hidden Code
def confirm
update_columns({
confirmation_token: nil,
confirmed_at: Time.now
})
end
private
def generate_confirmation_token # Hidden Code
def downcase_email # Hidden Code
end
Let’s take a look at the controller. In the show
action, we need to confirm the user and either redirect him or show him some text. Checking if a user with that confirmation token actually exists has been extracted into a before_action
filter.
# app/controllers/user_confirmations_controller.rb
class UserConfirmationsController < ActionController::API
before_action :confirmation_token_not_found
def show
user.confirm
if user.confirmation_redirect_url
redirect_to(user.confirmation_redirect_url)
else
render plain: 'You are now confirmed!'
end
end
private
def confirmation_token_not_found
render(status: 404, plain: 'Token not found') unless user
end
def confirmation_token
@confirmation_token ||= params[:confirmation_token]
end
def user
@user ||= User.where(confirmation_token: confirmation_token).first
end
end
Run the tests to ensure that we correctly implemented the confirmation process.
rspec spec/requests/user_confirmations_spec.rb
Success (GREEN)
Finished in 0.1999 seconds (files took 2.14 seconds to load)
5 examples, 0 failures
With a functional user confirmations controller, we can now update the confirmation email.
<% # app/views/user_mailer/confirmation_email.text.erb %>
Hello,
Confirm your email by clicking on the link below:
<%= link_to('Confirm Your Account',{
controller: "user_confirmations",
action: :show,
confirmation_token: @user.confirmation_token
}) %>
Welcome to Alexandria!
rspec
Success (GREEN)
...
Finished in 9.02 seconds (files took 2.25 seconds to load)
233 examples, 0 failures
If you want to try it manually, use the command below to send a request to create a new user. Replace the value of API_KEY
with the key we generated in the previous chapter.
rails s
curl -X POST \
-H "Authorization: Alexandria-Token api_key=1:my_api_key" \
-H "Content-Type: application/json" \
-d '{"data":{"email":"john@gmail.com",
"password": "password",
"confirmation_redirect_url":"http://google.com"}}' \
http://localhost:3000/api/users
Output
{ "data":
{
"id":1,
"email":"john@gmail.com",
"given_name":null,
"family_name":null,
"role":"user",
"last_logged_in_at":null,
"confirmed_at":null,
"confirmation_sent_at":"2016-06-08T09:38:53.708Z",
"reset_password_sent_at":null,
"created_at":"2016-06-08T09:38:53.688Z",
"updated_at":"2016-06-08T09:38:53.688Z",
"confirmation_token":"88047d1ee2fe8759b7107c58a4da10c2",
"reset_password_token":null,
"confirmation_redirect_url":"http://google.com",
"reset_password_redirect_url":null
}
}
You should see the email being sent in the logs:
UserMailer#confirmation_email: processed outbound mail in 44.6ms
Sent mail to john@gmail.com (5.7ms)
Date: Wed, 08 Jun 2016 15:54:44 +0700
From: admin@alexandria.com
To: john@gmail.com
Message-ID: <HIDDEN>
Subject: Confirm your Account!
Mime-Version: 1.0
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Hello,
Confirm your email by clicking on the link below:
<a href="http://localhost:3000/api/user_confirmations/88047d1...">
Confirm Your Account
</a>
Welcome to Alexandria!
Access the URL defined in the email and the account will get confirmed. You will also get redirected to Google. Great!
With the confirmation done, there is only one thing left in this chapter: providing users with a way to retrieve their lost passwords. The flow to reset a password will look like this:
To handle those tests, we will create a new controller and a “table-less” model.
Let’s get started.
First, create the controller file.
touch app/controllers/password_resets_controller.rb \
spec/requests/password_resets_spec.rb
Add the routes for this new controller in the config/routes.rb
file. We only need the show
and create
actions.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books, except: :put
resources :authors, except: :put
resources :publishers, except: :put
resources :users, except: :put
resources :user_confirmations, only: :show, param: :confirmation_token
resources :password_resets, only: [:show, :create, :update],
param: :reset_token
get '/search/:text', to: 'search#index'
end
root to: 'books#index'
end
Next, let’s write some tests for the endpoint that will initiate the reset password process. Available at /api/password_resets
with the POST
method, this resource will insert the information provided by the client in the user.
As the API creators, we know that it will only store them in the user record but from the outside, all the client cares about is that calling this URI will create a “password reset.”
At this point in the module, I’m not going to explain the tests anymore. The tests themselves should be enough to understand what’s going on since they document our intentions pretty well.
# spec/requests/password_resets_spec.rb
require 'rails_helper'
RSpec.describe 'PasswordResets', type: :request do
let(:john) { create(:user) }
describe 'POST /api/password_resets' do
# This resource can only be accessed by an authenticated client.
# For those steps, we will skip the authentication
before do
allow_any_instance_of(PasswordResetsController).to(
receive(:validate_auth_scheme).and_return(true))
allow_any_instance_of(PasswordResetsController).to(
receive(:authenticate_client).and_return(true))
end
context 'with valid parameters' do
let(:params) do
{
data: {
email: john.email,
reset_password_redirect_url: 'http://example.com'
}
}
end
before { post '/api/password_resets', params: params }
it 'returns 204' do
expect(response.status).to eq 204
end
it 'sends the reset password email' do
expect(ActionMailer::Base.deliveries.last.subject).to eq(
'Reset your password'
)
end
# Here we check that all the fields have properly been updated
it 'adds the reset password attributes to "john"' do
expect(john.reset_password_token).to be nil
expect(john.reset_password_sent_at).to be nil
updated = john.reload
expect(updated.reset_password_token).to_not be nil
expect(updated.reset_password_sent_at).to_not be nil
expect(updated.reset_password_redirect_url).to eq 'http://example.com'
end
end
context 'with invalid parameters' do
let(:params) { { data: { email: john.email } } }
before { post '/api/password_resets', params: params }
it 'returns HTTP status 422' do
expect(response.status).to eq 422
end
end
context 'with nonexistent user' do
let(:params) { { data: { email: 'fake@example.com' } } }
before { post '/api/password_resets', params: params }
it 'returns HTTP status 404' do
expect(response.status).to eq 404
end
end
end
end
rspec spec/requests/password_resets_spec.rb
Failure (RED)
Finished in 0.10167 seconds (files took 2.36 seconds to load)
5 examples, 5 failures
Let’s write enough code to make all those tests pass!
First, we need to add a new method in the UserMailer
to send our new email. Write a test for it in the user_mailer_spec
file.
# spec/mailers/user_mailer_spec.rb
require 'rails_helper'
RSpec.describe UserMailer, :type => :mailer do
describe '#confirmation_email' # Hidden Code
describe '#reset_password' do
let(:user) { create(:user, :reset_password) }
let(:mail) { UserMailer.reset_password(user) }
it 'renders the headers' do
expect(mail.subject).to eq('Reset your password')
expect(mail.to).to eq([user.email])
expect(mail.from).to eq(['admin@alexandria.com'])
end
it 'renders the body' do
expect(mail.body.encoded).to match(
'Use the link below to reset your password'
)
end
end
end
Then add the reset_password
method in the UserMailer
.
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def confirmation_email # Hidden Code
def reset_password(user)
@user = user
@user.update_column(:confirmation_sent_at, Time.now)
mail to: @user.email, subject: 'Reset your password'
end
end
Create a new email template…
touch app/views/user_mailer/reset_password.text.erb
and fill it with a nice email to send users if they want to reset their passwords.
<% # app/views/user_mailer/reset_password.text.erb %>
Hello,
Use the link below to reset your password.
<%= link_to('Reset your password', {
controller: "password_resets",
action: :show,
reset_token: @user.reset_password_token }) %>
If you didn't initiate this password reset, you can discard this email.
rspec spec/mailers/user_mailer_spec.rb
Success (GREEN)
UserMailer
confirmation_email
renders the headers
renders the body
#reset_password
renders the headers
renders the body
Finished in 0.44334 seconds (files took 2.36 seconds to load)
4 examples, 0 failures
We also want to add an easy-to-use method in the User
model that will take care of updating all the fields related to the reset password process.
# app/models/user.rb
class User < ApplicationRecord
# Hidden Code
def confirm # Hidden Code
def init_password_reset(redirect_url)
assign_attributes({
reset_password_token: SecureRandom.hex,
reset_password_sent_at: Time.now,
reset_password_redirect_url: redirect_url
})
save
end
private
def generate_confirmation_token # Hidden Code
def downcase_email # Hidden Code
end
We want to use the nice stuff that comes with ActiveRecord
models like validations. This will allow us to write a cleaner controller that follows the logic of our previous ones.
To do this, we are going to create a “table-less” model named PasswordReset
. Create a file for this special model.
touch app/models/password_reset.rb
And here is the first version of that “model”. Like a regular model, it will receive some parameters (email
and reset_password_redirect_url
) and respond to the valid?
method if those parameters match our expectations.
The create
method will be used to check that the reset request was correctly initialized by checking that a user is present, that the given parameters are valid and, finally, that the user was updated without any issue.
# app/models/password_reset.rb
class PasswordReset
include ActiveModel::Model
attr_accessor :email, :reset_password_redirect_url
validates :email, presence: true
validates :reset_password_redirect_url, presence: true
def create
user && valid? && user.init_password_reset(reset_password_redirect_url)
end
def user
@user ||= retrieve_user
end
private
def retrieve_user
user = User.where(email: email).first
raise ActiveRecord::RecordNotFound unless user
user
end
end
Here is the implementation for the controller and our first action, create
. The create
action will be used by clients to initiate a new password reset, while the show
action will be accessed through the email sent to the user. Finally, the update
action will be used to update the password at the end of the process.
Notice how we use the PasswordReset
model to keep our controller DRY.
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
skip_before_action :validate_auth_scheme, only: :show
skip_before_action :authenticate_client, only: :show
def create
if reset.create
UserMailer.reset_password(reset.user).deliver_now
render status: :no_content, location: reset.user
else
unprocessable_entity!(reset)
end
end
private
def reset
@reset ||= reset = PasswordReset.new(reset_params)
end
def reset_params
params.require(:data).permit(:email, :reset_password_redirect_url)
end
end
Run the tests to see if everything is working as expected.
rspec spec/requests/password_resets_spec.rb
Success (GREEN)
...
PasswordResets
POST /password_resets
with valid parameters
returns 204
sends the reset password email
adds the reset password attributes to "john"
with invalid parameters
returns HTTP status 422
with nonexistent user
returns HTTP status 404
Finished in 0.62079 seconds (files took 2.3 seconds to load)
5 examples, 0 failures
The second step is handling the link clicked in the reset email that was sent to the user.
Here are the tests for the /password_resets/:reset_token
resource accessible with GET
.
# spec/requests/password_resets_spec.rb
require 'rails_helper'
RSpec.describe 'PasswordResets', type: :request do
let(:john) { create(:user) }
describe 'POST /api/password_resets' # Hidden Code
describe 'GET /api/password_resets/:reset_token' do
context 'with existing user (valid token)' do
subject { get "/api/password_resets/#{john.reset_password_token}" }
context 'with redirect URL containing parameters' do
let(:john) { create(:user, :reset_password) }
it 'redirects to "http://example.com?some=params&reset_token=TOKEN"' do
token = john.reset_password_token
expect(subject).to redirect_to(
"http://example.com?some=params&reset_token=#{token}"
)
end
end
context 'with redirect URL not containing any parameters' do
let(:john) { create(:user, :reset_password_no_params) }
it 'redirects to "http://example.com?reset_token=TOKEN"' do
expect(subject).to redirect_to(
"http://example.com?reset_token=#{john.reset_password_token}"
)
end
end
end
context 'with nonexistent user' do
before { get "/api/password_resets/123" }
it 'returns HTTP status 404' do
expect(response.status).to eq 404
end
end
end # 'GET /password_resets/:reset_token'
end
Next, we need to update the PasswordReset
model a bit. We want it to give us back the redirect URL containing the reset token. This token will be needed in the last step, so we need to give it to the client.
We also need to change the way we retrieve the user. For the create
step, we are using the user’s email while in the update
action, we use the reset_token
sent by the client.
# app/models/password_reset.rb
class PasswordReset
include ActiveModel::Model
attr_accessor :email, :reset_password_redirect_url, :reset_token
validates :email, presence: true
validates :reset_password_redirect_url, presence: true
def create
user && valid? && user.init_password_reset(reset_password_redirect_url)
end
def redirect_url
build_redirect_url
end
def user
@user ||= retrieve_user
end
private
def retrieve_user
user = email ? user_with_email : user_with_token
raise ActiveRecord::RecordNotFound unless user
user
end
def user_with_email
User.where(email: email).first
end
def user_with_token
User.where(reset_password_token: reset_token).first
end
def build_redirect_url
url = user.reset_password_redirect_url
query_params = Rack::Utils.parse_query(URI(url).query)
if query_params.any?
"#{url}&reset_token=#{reset_token}"
else
"#{url}?reset_token=#{reset_token}"
end
end
end
Now, let’s add the show
action to our controller. This action is super simple; all it does is instantiate the PasswordReset
“model” and redirect to the URL given by the client earlier.
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
include ActiveModel::Model
# Hidden Code
def create # Hidden Code
def show
reset = PasswordReset.new({ reset_token: params[:reset_token] })
redirect_to reset.redirect_url
end
private
def reset # Hidden Code
def reset_params # Hidden Code
end
All should be well when we run the tests.
rspec spec/requests/password_resets_spec.rb
Success (GREEN)
PasswordResets
POST /password_resets
with valid parameters
returns 204
sends the reset password email
adds the reset password attributes to "john"
with invalid parameters
returns HTTP status 422
with nonexistent user
returns HTTP status 404
GET /password_resets/:reset_token
with existing user (valid token)
with redirect URL containing parameters
redirects to 'http://example.com?some=params&reset_token=TOKEN'
with redirect URL not containing any parameters
redirects to 'http://example.com?reset_token=TOKEN'
with nonexistent user
returns HTTP status 404
Finished in 0.63131 seconds (files took 2.35 seconds to load)
8 examples, 0 failures
Finally, the last step! As usual, let’s add some tests that will test the password update.
# spec/requests/password_resets_spec.rb
require 'rails_helper'
RSpec.describe 'PasswordResets', type: :request do
let(:john) { create(:user) }
describe 'POST /api/password_resets' # Hidden Code
describe 'GET /api/password_resets/:reset_token' # Hidden Code
describe 'PATCH /api/password_resets/:reset_token' do
before do
allow_any_instance_of(PasswordResetsController).to(
receive(:validate_auth_scheme).and_return(true))
allow_any_instance_of(PasswordResetsController).to(
receive(:authenticate_client).and_return(true))
end
context 'with existing user (valid token)' do
let(:john) { create(:user, :reset_password) }
before do
patch "/api/password_resets/#{john.reset_password_token}", params: params
end
context 'with valid parameters' do
let(:params) { { data: { password: 'new_password' } } }
it 'returns HTTP status 204' do
expect(response.status).to eq 204
end
it 'updates the password' do
expect(john.reload.authenticate('new_password')).to_not be false
end
end
context 'with invalid parameters' do
let(:params) { { data: { password: '' } } }
it 'returns HTTP status 422' do
expect(response.status).to eq 422
end
end
end
context 'with nonexistent user' do
before do
patch '/api/password_resets/123', params: {
data: { password: 'password' }
}
end
it 'returns HTTP status 404' do
expect(response.status).to eq 404
end
end
end # 'PATCH /password_resets/:reset_token' end
end
Let’s add another little method, complete_password_reset
, in the User
model to update all the required fields.
# app/models/user.rb
class User < ApplicationRecord
# Hidden Code
def confirm # Hidden Code
def init_password_reset # Hidden Code
def complete_password_reset(password)
assign_attributes({
password: password,
reset_password_token: nil,
reset_password_sent_at: nil,
reset_password_redirect_url: nil
})
save
end
private
def generate_confirmation_token # Hidden Code
def downcase_email # Hidden Code
end
Here is the last iteration of the PasswordReset
“model.” Now that we need to handle updating the password, we have to define two sets of validations. The email
and redirect_url
are required only when initiating a password reset. For the completion, only the password
is needed.
To avoid creating another table-less model, we can just use conditional validations using an attribute named updating
that will be set to true
only in the update
method.
# app/models/password_reset.rb
class PasswordReset
include ActiveModel::Model
attr_accessor :email, :reset_password_redirect_url, :password,
:reset_token, :updating
validates :email, presence: true, unless: :updating
validates :reset_password_redirect_url, presence: true, unless: :updating
validates :password, presence: true, if: :updating
def create
user && valid? && user.init_password_reset(reset_password_redirect_url)
end
def redirect_url
build_redirect_url
end
def update
self.updating = true
user && valid? && user.complete_password_reset(password)
end
def user
@user ||= retrieve_user
end
private
def retrieve_user
user = email ? user_with_email : user_with_token
raise ActiveRecord::RecordNotFound unless user
user
end
def user_with_email
User.where(email: email).first
end
def user_with_token
User.where(reset_password_token: reset_token).first
end
def build_redirect_url
url = user.reset_password_redirect_url
query_params = Rack::Utils.parse_query(URI(url).query)
if query_params.any?
"#{url}&reset_token=#{reset_token}"
else
"#{url}?reset_token=#{reset_token}"
end
end
end
The update
action in the password_resets
controller looks just like the create
one. The only differences are that we call a different method on the reset instance and we don’t send an email in the update
action.
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
skip_before_action :validate_auth_scheme, only: :show
skip_before_action :authenticate_client, only: :show
def show
reset = PasswordReset.new({ reset_token: params[:reset_token] })
redirect_to reset.redirect_url
end
def create
if reset.create
UserMailer.reset_password(reset.user).deliver_now
render status: :no_content, location: reset.user
else
unprocessable_entity!(reset)
end
end
def update
reset.reset_token = params[:reset_token]
if reset.update
render status: :no_content
else
unprocessable_entity!(reset)
end
end
private
def reset
@reset ||= reset = PasswordReset.new(reset_params)
end
def reset_params
params.require(:data).permit(:email, :reset_password_redirect_url, :password)
end
end
Are we done yet? Let’s run the tests to answer that question.
rspec spec/requests/password_resets_spec.rb
Success (GREEN)
...
Finished in 0.71258 seconds (files took 2.28 seconds to load)
12 examples, 0 failures
We could reduce the code duplication even more by creating a shared method that receives a block, but that would probably make the code confusing. Here’s an example anyway of what we could do.
def create
handle_password_reset(:create) do
UserMailer.reset_password(reset.user).deliver_now
end
end
def update
reset.reset_token = params[:reset_token]
handle_password_reset(:update)
end
def handle_password_reset(method)
if reset.send(method)
yield if block_given?
render status: :no_content
else
unprocessable_entity!(reset)
end
end
Integration tests can be used to test entire feature; for example, the entire reset password flow.
mkdir spec/features && \
touch spec/features/password_reset_flow_spec.rb
# spec/features/password_reset_flow_spec.rb
require 'rails_helper'
RSpec.describe 'Password Reset Flow', type: :request do
let(:john) { create(:user) }
let(:api_key) { ApiKey.create }
let(:headers) do
{ 'HTTP_AUTHORIZATION' =>
"Alexandria-Token api_key=#{api_key.id}:#{api_key.key}" }
end
let(:create_params) do
{ email: john.email, reset_password_redirect_url: 'http://example.com' }
end
let(:update_params) { { password: 'new_password' } }
it 'resets the password' do
expect(john.authenticate('password')).to_not be false
expect(john.reset_password_token).to be nil
# Step 1
post '/api/password_resets', params: { data: create_params }, headers: headers
expect(response.status).to eq 204
reset_token = john.reload.reset_password_token
expect(ActionMailer::Base.deliveries.last.body).to match reset_token
# Step 2
sbj = get "/api/password_resets/#{reset_token}"
expect(sbj).to redirect_to("http://example.com?reset_token=#{reset_token}")
# Step 3
patch "/api/password_resets/#{reset_token}",
params: { data: update_params }, headers: headers
expect(response.status).to eq 204
expect(john.reload.authenticate('new_password')).to_not be false
end
end
Run this test.
rspec spec/features/password_reset_flow_spec.rb
Success (GREEN)
...
Password Reset Flow
resets the password
Finished in 0.46872 seconds (files took 1.96 seconds to load)
1 example, 0 failures
Run all the tests to ensure that we didn’t break anything.
rspec
Success (GREEN)
Finished in 15.72 seconds (files took 4 seconds to load)
248 examples, 0 failures
Let’s push our changes.
git status
Output
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: Gemfile
modified: Gemfile.lock
modified: app/mailers/application_mailer.rb
modified: config/routes.rb
modified: db/schema.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/controllers/password_resets_controller.rb
app/controllers/user_confirmations_controller.rb
app/controllers/users_controller.rb
app/mailers/user_mailer.rb
app/models/password_reset.rb
app/models/user.rb
app/presenters/user_presenter.rb
app/views/user_mailer/
db/migrate/20160609070848_create_users.rb
spec/factories/users.rb
spec/fixtures/
spec/mailers/
spec/models/user_spec.rb
spec/requests/password_resets_spec.rb
spec/requests/user_confirmations_spec.rb
spec/requests/users_spec.rb
no changes added to commit (use "git add" and/or "git commit -a")
Stage them.
git add .
Commit the changes.
git commit -m "Implement Users"
Output
[master 962cce6] Implement Users
22 files changed, 845 insertions(+), 6 deletions(-)
create mode 100644 app/controllers/password_resets_controller.rb
create mode 100644 app/controllers/user_confirmations_controller.rb
create mode 100644 app/controllers/users_controller.rb
create mode 100644 app/mailers/user_mailer.rb
create mode 100644 app/models/password_reset.rb
create mode 100644 app/models/user.rb
create mode 100644 app/presenters/user_presenter.rb
create mode 100644 app/views/user_mailer/confirmation_email.text.erb
create mode 100644 app/views/user_mailer/reset_password.text.erb
create mode 100644 db/migrate/20160609070848_create_users.rb
create mode 100644 spec/factories/users.rb
create mode 100644 spec/fixtures/user_mailer/confirmation_email
create mode 100644 spec/mailers/user_mailer_spec.rb
create mode 100644 spec/models/user_spec.rb
create mode 100644 spec/requests/password_resets_spec.rb
create mode 100644 spec/requests/user_confirmations_spec.rb
create mode 100644 spec/requests/users_spec.rb
Push to GitHub.
git push origin master
This chapter was a big one! In it, we implemented all the logic needed for users to create accounts, confirm them and reset their passwords if needed. In the next chapter, we are going to add the user authentication part.