Clearance is thoughtbot’s authentication library for Rails applications.
We needed to verify that people signing up to our application owned the email address they signed up with. Clearance doesn’t provide an email confirmation step out of the box, but it does provide a simple and powerful API that we can use to add such behavior.
This post will follow step by step how to add email confirmation to a Rails application with Clearance in test-driven fashion.
Feature spec
We start modifying the relevant feature spec, to take into account the new functionality. We add a step right after sign up, where a user wants to sign in but is prevented with an error message about their email not being confirmed yet.
feature "User authentication" do
- scenario "Visitor signs up and signs out" do
+ scenario "Visitor signs up, tries to sign in, confirms email and signs out" do
visit root_path
click_link t("labels.sign_up")
fill_in "Email", with: "clarence@example.com"
fill_in "Password", with: "password"
click_button t("labels.sign_up")
+ click_link t("labels.sign_in")
+
+ fill_in "Email", with: "clarence@example.com"
+ fill_in "Password", with: "password"
+ click_button t("labels.sign_in")
+
+ expect(page).to have_content t("flashes.confirm_your_email")
+
+ open_email "clarence@example.com"
+ click_first_link_in_email
expect(page).to have_content "clarence@example.com"
click_button t("labels.sign_out")
expect(current_path).to eq(sign_in_path)
end
end
Because we use email spec helpers, we add the dependency to our application:
# Gemfile
gem "email_spec"
# spec/rails_helper.rb
require "clearance/rspec"
RSpec.configure do |config|
config.include EmailSpec::Helpers
config.include EmailSpec::Matchers
# ...
end
The test fails because as of now the application signs the user in right after sign up, without any confirmation step. This spec will fail until we finish implementing the feature, which we now continue developing doing TDD.
New user attributes
We need to add functionality in between a user signing up and signing in. To do
so, we need new attributes in the User
model to decide whether a user’s email
address is confirmed.
We add an email_confirmed_at
datetime attribute to User
which will indicate a
user is confirmed when it has a value. We also add an email_confirmation_token
attribute, which is the unique token that is generated on user sign up and sent
to their email, so we can validate that they have access to the email they are
signing up with.
The migration looks like this:
class AddConfirmedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :email_confirmation_token, :string, null: false, default: ""
add_column :users, :email_confirmed_at, :datetime
end
end
Having null: false, default: ""
options for email_confirmation_token
guarantees that the value will always be a string, whether it’s an actual token
or an empty string, avoiding unnecessary nil
s in our system and making sure it
always responds to String methods. This avoids type-check conditionals.
On the other hand, email_confirmed_at
can be nil
, and in that case the user
has not yet confirmed their email account.
Populate new attributes
Clearance sign up creates a new user by default, with no email confirmation logic. We want our application to prevent users from authenticating after a successful sign up. For this to happen, we override Clearance’s user creation endpoint to set the email confirmation token on new users, and send them an email with the token.
Let’s test drive the new controller:
describe UsersController do
describe "#create" do
context "with valid attributes" do
it "creates user and sends confirmation email" do
email = "user@example.com"
post :create, user: { email: email, password: "password" }
expect(controller.current_user).to be_nil
expect(last_email_confirmation_token).to be_present
should_deliver_email(
to: email,
subject: t("email.subject.confirm_email"),
)
end
end
end
def should_deliver_email(to:, subject:)
expect(ActionMailer::Base.deliveries).not_to be_empty
email = ActionMailer::Base.deliveries.last
expect(email).to deliver_to(to)
expect(email).to have_subject(subject)
end
def last_email_confirmation_token
User.last.email_confirmation_token
end
end
To make this test pass we need to define the new controller that hooks into the Clearance sign up flow, and let our application know that we will handle sign up instead of Clearance.
We include explicit Clearance routes in our application using the generator:
rails generate clearance:routes
And modify the generated routes so the sign up endpoint routes to our soon-to-be controller:
- resources :users, controller: "clearance/users", only: [:create] do
+ resources :users, only: :create do
resource :password,
controller: "clearance/passwords",
only: [:create, :edit, :update]
end
With the route hooked up, we create our controller making use of
methods defined in Clearance::UsersController
. (Note that our version
doesn’t have sign_in @user). This makes the unit spec pass:
class UsersController < Clearance::UsersController
def create
@user = user_from_params
@user.email_confirmation_token = Clearance::Token.new
if @user.save
UserMailer.registration_confirmation(@user).deliver_later
redirect_back_or url_after_create
else
render template: "users/new"
end
end
end
UserMailer
specs and implementation is outside the scope of this post.
Keep users out
At this point, we run the feature spec again, and it fails at the very same step
as before this work. We are marking users as not confirmed after sign up, but
we are not adding the check during sign in and thus behavior has not yet
changed. We will make use of Clearance SignInGuard
stack to override default
behavior.
SignInGuard
offers fine-grained control over the process of signing in a user.
Clearance initializes an object that inherits from SignInGuard
and responds to
call
with a session and the current guards stack.
SignInGuard
provides methods to help make writing guards simple: success
,
failure
, next_guard
, signed_in?
, and current_user
.
We can think of Guards as a middleware, in which a chain of objects is run one after the other, and if they all succeed they authenticate the user. Otherwise they display an error message and show again the sign in form.
We add a new guard to our Clearance configuration:
Clearance.configure do |config|
config.routes = false
config.sign_in_guards = [ConfirmedUserGuard]
end
And define it:
# app/guards/confirmed_user_guard.rb
class ConfirmedUserGuard < Clearance::SignInGuard
def call
if user_confirmed?
next_guard
else
failure I18n.t("flashes.confirm_your_email")
end
end
def user_confirmed?
signed_in? && current_user.email_confirmed_at.present?
end
end
With this set up, our feature spec advances a few steps to fail in the following step:
expect(page).to have_content "clarence@example.com"
We are correctly disallowing signed up users to sign in, but we are not providing them with an endpoint to actually confirm their account yet. Users who sign up and don’t confirm are properly left out, but how can a user confirm their account and authenticate?
Let users in
We create a controller spec to allow a user to confirm their account, provided they have the proper confirmation token:
describe EmailConfirmationsController do
describe "#update" do
context "with invalid confirmation token" do
it "raises RecordNotFound exception" do
expect do
get :update, token: "inexistent"
end.to raise_exception(ActiveRecord::RecordNotFound)
end
end
context "with valid confirmation token" do
it "confirms user and signs it in" do
user = create(
:user,
email_confirmation_token: "valid_token",
email_confirmed_at: nil,
)
get :update, token: "valid_token"
user.reload
expect(user.email_confirmed_at).to be_present
expect(controller.current_user).to eq(user)
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq t("flashes.confirmed_email")
end
end
end
end
Note that we define an update
action, as it’s updating a resource in the
server, but define it with a GET
HTTP verb. This is because the user is not
submitting a form, but clicking on a confirmation link in their email.
The route and controller that make this spec pass follow:
get "/confirm_email/:token" => "email_confirmations#update", as: "confirm_email"
class EmailConfirmationsController < ApplicationController
def update
user = User.find_by!(email_confirmation_token: params[:token])
user.confirm_email
sign_in user
redirect_to root_path, notice: t("flashes.confirmed_email")
end
end
While writing the controller we find that we’d like to have that
user.confirm_email
method that doesn’t yet exist, so we write its spec and
implementation:
# spec/models/user_spec.rb
describe User do
# ...
describe "#confirm_email" do
it "sets email_confirmed_at value" do
user = create(
:user,
email_confirmation_token: "token",
email_confirmed_at: nil,
)
user.confirm_email
expect(user.email_confirmed_at).to be_present
end
end
# ...
end
class User < ActiveRecord::Base
include Clearance::User
# ...
def confirm_email
self.email_confirmed_at = Time.current
save
end
end
Done
We test-drove adding email confirmation to a Rails application that uses Clearance, making use of Clearance Guards for extending default behavior.
At this point all specs are green, and we can deploy this to be tested in staging servers. If it passes code review and acceptance testing, we are ready to deploy this to production. Yay!