Email confirmation with Clearance

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 nils 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!