code - lucatironi Code snippets and tutorials

Using rails-api to build an authenticated JSON API with warden

An updated version of my previous tutorials on building an authenticated JSON API with Ruby on Rails


In this tutorial I will build a small web application that provides a JSON API to manage customers through a REST interface. The requests to the endpoints will be authenticated through a token based authentication strategy, passing custom headers (X-User-Email and X-Auth-Token) containing the user’s credentials.

A sessions endpoint is available to issue a new authentication token on login and disposing it on logout.

The goal of the tutorial is building the base of an up-to-date, well tested, minimal and functional backend API that can be used for clients such as Angular/Ember web apps or even Mobile applications. Take a look to the previous tutorials to have an idea of the differences with those examples.

Requirements

  • Rails::API is a subset of a normal Rails application, created for applications that don’t require all functionality that a complete Rails application provides.
  • Warden provides a mechanism for authentication in Rack based Ruby applications. It is used by many other libraries and gems, like Devise.
  • RSpec is a gem to do behavior-driven development in Ruby (on Rails). We will use the Rails helpers for our models, controllers, routes and integration tests.

The complete code for this tutorial can be found on my Github account.

Setup

Let’s start by installing the rails-api gem and creating the app using the rails-api command in the terminal.

gem install rails-api

rails-api new example_api --skip-turbolinks --skip-sprockets --skip-test-unit --skip-javascript

I issued the command with some options to skip unused functionalities like turbolinks and sprockets, since we will not have a “frontend”. I also disabled the default test-unit framework since we will use RSpec for our tests.

Testing

Speaking of RSpec, let’s start the development of our new app by installing and configuring it. First add the gem and some companion gems like shoulda-matchers and the spring-commands-rspec to enable the usage of RSpec with Spring.

# file: Gemfile
source 'https://rubygems.org'

gem 'rails', '4.2.3'
gem 'rails-api'
gem 'sqlite3'

group :development do
  gem 'spring'
  gem 'spring-commands-rspec'
end

group :test do
  gem 'shoulda-matchers', require: false
end

group :development, :test do
  gem 'rspec-rails', '~> 3.0'
end

Install all the gems, run the installation of RSpec to generate the spec_helper.rb and rails_helper.rb files and finally create the bin/rspec binstub.

bundle install

bin/rails generate rspec:install

bundle exec spring binstub rspec

For more information about Spring, refer to the official documentation.

Customers

Once we have our testing framework up and running, we can start by creating our first model. I will use a Customer resource as an example to build the first (and only) endpoint. If you are building a real application, please create your model(s) accordingly.

Model

Use the rails generator to create the model and the migration. Our model will have three attributes: full_name, email and phone. Remember to run the migration after the files are automatically generated.

bin/rails generate model customer full_name:string email:string phone:string

bin/rake db:migrate

Before starting to create our first spec, we need to add shoulda-matchers to the spec/rails_helper.rb file. Add require 'shoulda/matchers' at the beginning of the file:

# file: spec/rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'spec_helper'
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'shoulda/matchers'

# ...

I will use the shoulda-matchers matchers to test our models, specifying which database columns (and attributes) it should have. I add also the matcher for the validation that the model is expected to have.

# file: spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do

  describe "db structure" do
    it { is_expected.to have_db_column(:full_name).of_type(:string) }
    it { is_expected.to have_db_column(:email).of_type(:string) }
    it { is_expected.to have_db_column(:phone).of_type(:string) }
    it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
    it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }
  end

  describe "validations" do
    it { is_expected.to validate_presence_of(:full_name) }
  end

end

Run the specs with bin/rspec and see them fail once. Add the Customer model and relaunch the specs.

# file: app/models/customer.rb
class Customer < ActiveRecord::Base
  validates_presence_of :full_name
end
bin/rspec

The output of the specs should be similar to this one:

$ bin/rspec

......

Finished in 0.02619 seconds (files took 0.49037 seconds to load)
6 examples, 0 failures

If everything looks fine, let’s start adding the first endpoint to our API.

Routing

Start by using again the rails generator to create the CustomersController and related routing specs.

bin/rails generate controller customers index show create update destroy --no-view-specs --skip-routes

I will start by adding the routing specs to be sure that our controller is routable as expected.

# file: spec/routing/customers_routing_spec.rb
require 'rails_helper'

RSpec.describe CustomersController, type: :routing do
  it { expect(get:    "/customers").to   route_to("customers#index") }
  it { expect(get:    "/customers/1").to route_to("customers#show", id: "1") }
  it { expect(post:   "/customers").to   route_to("customers#create") }
  it { expect(put:    "/customers/1").to route_to("customers#update", id: "1") }
  it { expect(delete: "/customers/1").to route_to("customers#destroy", id: "1") }
end

To make these specs pass we need to add the customers resource to the routes.rb file.

# file: config/routes.rb
Rails.application.routes.draw do
  resources :customers, only: [:index, :show, :create, :update, :destroy]
end

Launch again the bin/rspec command and see the specs pass.

Controller

It’s time to add real value to our API. Let’s start by defining how our controller is expected to behave when is issued the canonical REST actions: index, show, create, update and destroy.

# file: spec/controllers/customers_controller_spec.rb
require 'rails_helper'

RSpec.describe CustomersController, type: :controller do

  # This should return the minimal set of attributes required to create a valid
  # Customer. As you add validations to Customer, be sure to
  # adjust the attributes here as well.
  let(:valid_attributes) {
    { full_name: "John Doe", email: "john.doe@example.com", phone: "123456789" }
  }

  let(:invalid_attributes) {
    { full_name: nil, email: "john.doe@example.com", phone: "123456789" }
  }

  let!(:customer) { Customer.create(valid_attributes) }

  describe "GET #index" do
    it "assigns all customers as @customers" do
      get :index, { format: :json }
      expect(assigns(:customers)).to eq([customer])
    end
  end

  describe "GET #show" do
    it "assigns the requested customer as @customer" do
      get :show, { id: customer.id, format: :json }
      expect(assigns(:customer)).to eq(customer)
    end
  end

  describe "POST #create" do
    context "with valid params" do
      it "creates a new Customer" do
        expect {
          post :create, { customer: valid_attributes, format: :json  }
        }.to change(Customer, :count).by(1)
      end

      it "assigns a newly created customer as @customer" do
        post :create, { customer: valid_attributes, format: :json  }
        expect(assigns(:customer)).to be_a(Customer)
        expect(assigns(:customer)).to be_persisted
      end
    end

    context "with invalid params" do
      it "assigns a newly created but unsaved customer as @customer" do
        post :create, { customer: invalid_attributes, format: :json  }
        expect(assigns(:customer)).to be_a_new(Customer)
      end

      it "returns unprocessable_entity status" do
        put :create, { customer: invalid_attributes }
        expect(response.status).to eq(422)
      end
    end
  end

  describe "PUT #update" do
    context "with valid params" do
      let(:new_attributes) {
        { full_name: "John F. Doe", phone: "234567890" }
      }

      it "updates the requested customer" do
        put :update, { id: customer.id, customer: new_attributes, format: :json  }
        customer.reload
        expect(customer.full_name).to eq("John F. Doe")
        expect(customer.phone).to eq("234567890")
      end

      it "assigns the requested customer as @customer" do
        put :update, { id: customer.id, customer: valid_attributes, format: :json  }
        expect(assigns(:customer)).to eq(customer)
      end
    end

    context "with invalid params" do
      it "assigns the customer as @customer" do
        put :update, { id: customer.id, customer: invalid_attributes, format: :json  }
        expect(assigns(:customer)).to eq(customer)
      end

      it "returns unprocessable_entity status" do
        put :update, { id: customer.id, customer: invalid_attributes, format: :json }
        expect(response.status).to eq(422)
      end
    end
  end

  describe "DELETE #destroy" do
    it "destroys the requested customer" do
      expect {
        delete :destroy, { id: customer.id, format: :json  }
      }.to change(Customer, :count).by(-1)
    end

    it "redirects to the customers list" do
      delete :destroy, { id: customer.id, format: :json  }
      expect(response.status).to eq(204)
    end
  end

end

The controller specs are long and detailed, but they are covering all the possible expectations about the controller. We are testing that our index action returns the right collection of Customer objects, the show action retrieves the right object, create is persisting a new object only if it’s valid and update is modifying accordingly to the given new attributes. Finally we check that destroy actually deletes the given record from the database.

# file: app/controllers/customers_controller.rb
class CustomersController < ApplicationController
  before_action :set_customer, only: [:show, :update, :destroy]

  def index
    @customers = Customer.all

    render json: @customers
  end

  def show
    render json: @customer
  end

  def create
    @customer = Customer.new(customer_params)

    if @customer.save
      render json: @customer, status: :created, location: @customer
    else
      render json: @customer.errors, status: :unprocessable_entity
    end
  end

  def update
    if @customer.update(customer_params)
      head :no_content
    else
      render json: @customer.errors, status: :unprocessable_entity
    end
  end

  def destroy
    @customer.destroy

    head :no_content
  end

  private

    def set_customer
      @customer = Customer.find(params[:id])
    end

    def customer_params
      params.require(:customer).permit(:full_name, :email, :phone)
    end

end

The actual code for the controller is as simple as this: not so different from the one you can get from the rails scaffold command. We are returning the objects as JSON and rendering also the errors as JSON in case of failed validations or exceptions.

Adding some real data

We tested our controller with the specs, but it’s now time to see the actual result in action. Let’s add some fake data with the seeds.

# file: db/seeds.rb
[
  { full_name: "John Doe",   email: "john.doe@example.com",   phone: "033 1234 5678"},
  { full_name: "Mark Smith", email: "mark.smith@example.com", phone: "034 6789 1234"},
  { full_name: "Tom Clark",  email: "tom.clark@example.com",  phone: "033 4321 9876"},
  { full_name: "Sue Palmer", email: "sue.palmer@example.com", phone: "034 9876 1234"},
  { full_name: "Kate Lee",   email: "kate.lee@example.com",   phone: "033 6789 4321"}
].each do |customer_attributes|
  Customer.create(customer_attributes)
end

Run the rake command to insert the seeds in your database and start the rails application server.

bin/rake db:seed

bin/rails server

Visit localhost:3000/customers.json to see the result:

[
  {
    id: 1,
    full_name: "John Doe",
    email: "john.doe@example.com",
    phone: "033 1234 5678",
    created_at: "2015-08-22T18:11:46.572Z",
    updated_at: "2015-08-22T18:11:46.572Z"
  }, {
    id: 2,
    full_name: "Mark Smith",
    email: "mark.smith@example.com",
    phone: "034 6789 1234",
    created_at: "2015-08-22T18:11:46.584Z",
    updated_at: "2015-08-22T18:11:46.584Z"
  }, {
    id: 3,
    full_name: "Tom Clark",
    email: "tom.clark@example.com",
    phone: "033 4321 9876",
    created_at: "2015-08-22T18:11:46.587Z",
    updated_at: "2015-08-22T18:11:46.587Z"
  }, {
    id: 4,
    full_name: "Sue Palmer",
    email: "sue.palmer@example.com",
    phone: "034 9876 1234",
    created_at: "2015-08-22T18:11:46.591Z",
    updated_at: "2015-08-22T18:11:46.591Z"
  }, {
    id: 5,
    full_name: "Kate Lee",
    email: "kate.lee@example.com",
    phone: "033 6789 4321",
    created_at: "2015-08-22T18:11:46.595Z",
    updated_at: "2015-08-22T18:11:46.595Z"
  }
]

You should see the JSON payload above as expected.

Using ActiveModel::Serializer to build the JSON response

Looking at the payload we just created, you can see that we don’t have control on which data we would like to expose. To do so, we will use the ActiveModel::Serializer gem, created as a companion of the rails-api project.

# file: Gemfile
gem 'active_model_serializers', github: 'rails-api/active_model_serializers'

Add the gem to Gemfile and bundle it. Use the rails generator provided to add the CustomerSerializer model.

bundle install

bin/rails generate serializer customer

Add the attributes that we want to return as JSON:

# file: app/serializers/customer_serializer.rb
class CustomerSerializer < ActiveModel::Serializer
  attributes :id, :full_name, :email, :phone
end

The payload should be now like the following one, without the timestamps.

[
  {
    id: 1,
    full_name: "John Doe",
    email: "john.doe@example.com",
    phone: "033 1234 5678"
  }, {
    id: 2,
    full_name: "Mark Smith",
    email: "mark.smith@example.com",
    phone: "034 6789 1234"
  }, {
    id: 3,
    full_name: "Tom Clark",
    email: "tom.clark@example.com",
    phone: "033 4321 9876"
  }, {
    id: 4,
    full_name: "Sue Palmer",
    email: "sue.palmer@example.com",
    phone: "034 9876 1234"
  }, {
    id: 5,
    full_name: "Kate Lee",
    email: "kate.lee@example.com",
    phone: "033 6789 4321"
  }
]

Adding better errors on exceptions

Our endpoint is getting better and better, but we will encounter some issues when some of the common exceptions are raised, like 404 on record not found or strong_parameters is returning ActionController::ParameterMissing: the API will return normal html without a clear way to understand the error from the client perspective. To fix this we will add some specs to define that all our controllers will behave accordingly in those cases, rescuing the exceptions and return well formatted JSON.

Let’s add first a shared example to our specs. Start by enabling the usage of the spec/support directory where wil create our shared example file.

# file: spec/rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

The spec will be used in controllers and defines that in case of ActiveRecord::RecordNotFound or ActionController::ParameterMissing, the payload will be JSON with the correct status code and error message.

# file: spec/support/api_controller.rb
require 'rails_helper'

RSpec.shared_examples "api_controller" do

  describe "rescues from ActiveRecord::RecordNotFound" do

    context "on GET #show" do
      before { get :show, { id: 'not-existing', format: :json } }

      it { expect(response.status).to eq(404) }
      it { expect(response.body).to be_blank }
    end

    context "on PUT #update" do
      before { put :update, { id: 'not-existing', format: :json } }

      it { expect(response.status).to eq(404) }
      it { expect(response.body).to be_blank }
    end

    context "on DELETE #destroy" do
      before { delete :destroy, { id: 'not-existing', format: :json } }

      it { expect(response.status).to eq(404) }
      it { expect(response.body).to be_blank }
    end

  end

  describe "rescues from ActionController::ParameterMissing" do

    context "on POST #create" do
      before { post :create, { wrong_params: { foo: :bar }, format: :json } }

      it { expect(response.status).to eq(422) }
      it { expect(response.body).to match(/error/) }
    end

  end

end

Add to the customer controller specs the shared example with the it_behaves_like method call.

# file: spec/controllers/customer_controller_spec.rb
require 'rails_helper'

RSpec.describe CustomersController, type: :controller do

  it_behaves_like "api_controller"

  # ...
end

If you run the specs now, they will fail because we haven’t define yet the code to rescue the two exceptions. Add the following code to the application_controller.rb:

# file: app/controllers/application_controller.rb
class ApplicationController < ActionController::API

  rescue_from ActiveRecord::RecordNotFound,       with: :not_found
  rescue_from ActionController::ParameterMissing, with: :missing_param_error

  def not_found
    render status: :not_found, json: ""
  end

  def missing_param_error(exception)
    render status: :unprocessable_entity, json: { error: exception.message }
  end

end

By running again the bin/rspec command, the specs should pass as expected and our customers endpoint will return proper JSON on exceptions.

Authentication

The second part of the tutorial will add an authentication layer to the API. Let’s start by building a service that can issue secure tokens to an existing user in order to use them to make authenticated requests to the protected endpoints.

Usually Ruby on Rails applications rely on complex setups to provide authentication (and user account management), typically using Devise as a go-to solution. In this tutorial I would like to implement something from scratch, building the simplest working and secure authentication system possible, starting from the tools that Rails provide by default like has_secure_password and has_secure_token (a backport of a functionality from Rails 5) and packing everything with a custom build strategy with Warden.

Setup

Let’s start by adding the gems needed for the first iteration.

# file: Gemfile
gem 'bcrypt', '~> 3.1.7'
gem 'has_secure_token'

bcrypt is needed to use the has_secure_password feature from Rails and has_secure_token will enable the automatic generation of secure and unique token in our service object.

bundle install

Install the gems with the bundle command.

Token Issuer

We are going to create a service object to issue new authentication tokens for our logged in user, in order to have multiple authenticated sessions for a given user and being able to log out each one of them without affecting the others. Usually the authentication token is saved in the user record, enabling only one sessions at the time, since logging out and resetting the token will de facto log out every other sessions.

Let’s start by enabling the autoload of files in the app/services directory.

# file: config/application.rb

# ...

module ExampleApi
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
    config.autoload_paths += %W(#{config.root}/app/services/**/)

    # ...

  end
end

Let’s then create the specs for our TokenIssuer class. Its responsibilities are to create and return tokens (the model will come later) and purge the expired tokens of a user.

# file: spec/services/token_issuer_spec.rb
require 'rails_helper'

RSpec.describe TokenIssuer, type: :model do

  let(:resource) {
    double(:resource, id: 1,
      authentication_tokens: authentication_tokens) }
  let(:authentication_tokens) {
    double(:authentication_tokens, create!: authentication_token) }
  let(:authentication_token) {
    double(:authentication_token, body: "token") }
  let(:request) {
    double(:request, remote_ip: "100.10.10.23", user_agent: "Test Browser") }

  describe ".create_and_return_token" do

    it "creates a new token for the user" do
      expect(resource.authentication_tokens).to receive(:create!)
        .with(last_used_at: DateTime.current,
          ip_address: request.remote_ip,
          user_agent: request.user_agent)
        .and_return(authentication_token)
      described_class.create_and_return_token(resource, request)
    end

    it "returns the token body" do
      allow(resource.authentication_tokens).to receive(:create!)
        .and_return(authentication_token)
      expect(described_class.create_and_return_token(resource, request)).to eq("token")
    end

  end

  describe ".purge_old_tokens" do

    it "deletes all the user's tokens" do
      expect(resource.authentication_tokens).to receive_message_chain(:order, :offset, :destroy_all)
      described_class.purge_old_tokens(resource)
    end

  end

end

The implementation of the class is as follow. The basic functionality is to create and return a new AuthenticationToken for a user, to find a token between the user’s tokens and finally destroy an expired token.

A constant MAXIMUM_TOKENS_PER_USER overridable on initialization of the service sets how many tokens a user can keep active whenever the purge is called. This method can be used in a cron job in order to keep the unused tokens at bay.

# file: app/services/token_issuer.rb
class TokenIssuer
  MAXIMUM_TOKENS_PER_USER = 20

  def self.build
    new(MAXIMUM_TOKENS_PER_USER)
  end

  def self.create_and_return_token(resource, request)
    build.create_and_return_token(resource, request)
  end

  def self.expire_token(resource, request)
    build.expire_token(resource, request)
  end

  def self.purge_old_tokens(resource)
    build.purge_old_tokens(resource)
  end

  def initialize(maximum_tokens_per_user)
    self.maximum_tokens_per_user = maximum_tokens_per_user
  end

  def create_and_return_token(resource, request)
    token = resource.authentication_tokens.create!(
      last_used_at: DateTime.current,
      ip_address:   request.remote_ip,
      user_agent:   request.user_agent)

    token.body
  end

  def expire_token(resource, request)
    find_token(resource, request.headers["X-Auth-Token"]).try(:destroy)
  end

  def find_token(resource, token_from_headers)
    resource.authentication_tokens.detect do |token|
      token.body == token_from_headers
    end
  end

  def purge_old_tokens(resource)
    resource.authentication_tokens
      .order(last_used_at: :desc)
      .offset(maximum_tokens_per_user)
      .destroy_all
  end

  private

    attr_accessor :maximum_tokens_per_user

end

User and AuthenticationToken models

Let’s create now the User and AuthenticationToken models.

bin/rails generate model authentication_token body:string user:references last_used_at:datetime ip_address:string user_agent:string

bin/rails generate model user email:string password_digest:string
# file: db/migrate/XXX_create_authentication_tokens.rb
class CreateAuthenticationTokens < ActiveRecord::Migration
  def change
    create_table :authentication_tokens do |t|
      t.string :body
      t.references :user, index: true, foreign_key: true
      t.datetime :last_used_at
      t.string :ip_address
      t.string :user_agent

      t.timestamps null: false
    end
  end
end

I added an index on the email attribute since we will look for users through the email while doing the authentication.

# file: db/migrate/XXX_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email, index: :email
      t.string :password_digest

      t.timestamps null: false
    end
  end
end
# file: spec/models/authentication_token_spec.rb
require 'rails_helper'

RSpec.describe AuthenticationToken, type: :model do

  describe "db structure" do
    it { is_expected.to have_db_column(:user_id).of_type(:integer) }
    it { is_expected.to have_db_column(:body).of_type(:string) }
    it { is_expected.to have_db_column(:ip_address).of_type(:string) }
    it { is_expected.to have_db_column(:user_agent).of_type(:string) }
    it { is_expected.to have_db_column(:last_used_at).of_type(:datetime) }
    it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
    it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }

    it { is_expected.to have_db_index(:user_id) }
  end

  describe "associations" do
    it { is_expected.to belong_to(:user) }
  end

end
# file: app/models/authentication_token.rb
class AuthenticationToken < ActiveRecord::Base
  belongs_to :user
  has_secure_token :body
end
# file: spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do

  describe "db structure" do
    it { is_expected.to have_db_column(:email).of_type(:string) }
    it { is_expected.to have_db_column(:password_digest).of_type(:string) }
    it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
    it { is_expected.to have_db_column(:updated_at).of_type(:datetime) }

    it { is_expected.to have_db_index(:email) }
  end

  describe "associations" do
    it { is_expected.to have_many(:authentication_tokens) }
  end

  describe "secure password" do
    it { is_expected.to have_secure_password }
    it { is_expected.to validate_length_of(:password) }

    it { expect(User.new({ email: "user@email.com", password: nil }).save).to be_falsey }
    it { expect(User.new({ email: "user@email.com", password: "foo" }).save).to be_falsey }
    it { expect(User.new({ email: "user@email.com", password: "af3714ff0ffae" }).save).to be_truthy }
  end

end
# file: app/models/user.rb
class User < ActiveRecord::Base
  has_many :authentication_tokens
  has_secure_password
  validates :password, length: { minimum: 8 }
end

The specs and the models are simple and we are just testing the attributes, validations an the has_secure_password and has_secure_token features.

Adding Warden to handle authentication

We are coming to the core of our authentication layer. I was inspired by this tutorial by Oliver Brisse, so all the credits are due to him.

Start by adding the Warden gem and install it.

# file: Gemfile
gem 'warden'
bundle install

Let’s add an initializer to require and load our new strategy and setup the middleware.

# file: initializers/warden.rb
require 'authentication_token_strategy'

Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)

Rails.application.config.middleware.insert_after ActionDispatch::ParamsParser, Warden::Manager do |manager|
  manager.default_strategies :authentication_token
  manager.failure_app = UnauthenticatedController
end

Authentication Token Strategy

We now just need to create the authentication token strategy specs and class.

# file: spec/lib/authentication_token_strategy_spec.rb
require 'rails_helper'

RSpec.describe AuthenticationTokenStrategy, type: :model do
  let!(:user) {
    User.create(email: "user@example.com", password: "password") }
  let!(:authentication_token) {
    AuthenticationToken.create(user_id: user.id, body: "token", last_used_at: DateTime.current) }

  let(:env) {
    { "HTTP_X_USER_EMAIL" => user.email,
      "HTTP_X_AUTH_TOKEN" => authentication_token.body } }

  let(:subject) { described_class.new(nil) }

  describe "#valid?" do

    context "with valid credentials" do
      before { allow(subject).to receive(:env).and_return(env) }

      it { is_expected.to be_valid }
    end

    context "with invalid credentials" do
      before { allow(subject).to receive(:env).and_return({}) }

      it { is_expected.not_to be_valid }
    end

  end

  describe "#authenticate!" do

    context "with valid credentials" do
      before { allow(subject).to receive(:env).and_return(env) }

      it "returns success" do
        expect(User).to receive(:find_by)
          .with(email: user.email)
          .and_return(user)
        expect(TokenIssuer).to receive_message_chain(:build, :find_token)
          .with(user, authentication_token.body)
          .and_return(authentication_token)
        expect(subject).to receive(:success!).with(user)
        subject.authenticate!
      end

      it "touches the token" do
        expect(subject).to receive(:touch_token)
          .with(authentication_token)
        subject.authenticate!
      end
    end

    context "with invalid user" do
      before { allow(subject).to receive(:env)
        .and_return({ "HTTP_X_USER_EMAIL" => "invalid@email",
                      "HTTP_X_AUTH_TOKEN" => "invalid-token" }) }

      it "fails" do
        expect(User).to receive(:find_by)
          .with(email: "invalid@email")
          .and_return(nil)
        expect(TokenIssuer).not_to receive(:build)
        expect(subject).not_to receive(:success!)
        expect(subject).to receive(:fail!)
        subject.authenticate!
      end
    end

    context "with invalid token" do
      before { allow(subject).to receive(:env)
        .and_return({ "HTTP_X_USER_EMAIL" => user.email,
                      "HTTP_X_AUTH_TOKEN" => "invalid-token" }) }

      it "fails" do
        expect(User).to receive(:find_by)
          .with(email: user.email)
          .and_return(user)
        expect(TokenIssuer).to receive_message_chain(:build, :find_token)
          .with(user, "invalid-token")
          .and_return(nil)
        expect(subject).not_to receive(:success!)
        expect(subject).to receive(:fail!)
        subject.authenticate!
      end
    end
  end

end

As you can see, the strategy uses the valid? and authenticate! methods to check if the parameters (our two custom headers) are present and if the user exists and has a valid token.

# file: lib/authentication_token_strategy.rb
class AuthenticationTokenStrategy < ::Warden::Strategies::Base

  def valid?
    user_email_from_headers.present? && auth_token_from_headers.present?
  end

  def authenticate!
    failure_message = "Authentication failed for user/token"

    user = User.find_by(email: user_email_from_headers)
    return fail!(failure_message) unless user

    token = TokenIssuer.build.find_token(user, auth_token_from_headers)
    if token
      touch_token(token)
      return success!(user)
    end

    fail!(failure_message)
  end

  def store?
    false
  end

  private

    def user_email_from_headers
      env["HTTP_X_USER_EMAIL"]
    end

    def auth_token_from_headers
      env["HTTP_X_AUTH_TOKEN"]
    end

    def touch_token(token)
      token.update_attribute(:last_used_at, DateTime.current) if token.last_used_at < 1.hour.ago
    end

end

One last touch is updating the token every time the user uses it to authenticate itself in order to keep it between the non-expirable tokens.

Controllers

With the strategy up and running, we need now to add some helpers to the application controller in order to require the authentication to all the actions we want to restrict access to.

I will create a controller concern with some helper methods and a before_action prepended to all the actions that will try to authenticate the user with the provided credentials (if any) present in the request.

# file: app/controllers/concerns/warden_helper.rb
module WardenHelper
  extend ActiveSupport::Concern

  included do
    helper_method :warden, :current_user

    prepend_before_action :authenticate!
  end

  def current_user
    warden.user
  end

  def warden
    request.env['warden']
  end

  def authenticate!
    warden.authenticate!
  end
end

By including the concern in the ApplicationController we assure that all the controllers that inherit from it will require authentication.

# file: app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include WardenHelper

  # ...
end

As you could see in the initializer, we delegate to a special UnauthenticatedController controller the handling of failed authentications. It will respond with a 401 status code and an error message to all the requests that don’t satisfy the authentication.

# file: app/controllers/unauthenticated_controller.rb
class UnauthenticatedController < ActionController::Metal

  def self.call(env)
    @respond ||= action(:respond)
    @respond.call(env)
  end

  def respond
    self.status        = :unauthorized
    self.content_type  = "application/json"
    self.response_body = { errors: ["Unauthorized Request"] }.to_json
  end

end

In order to test the authentication layer, we need to add some helpers to configure RSpec with Warden. I had some issues and found a solution in a StackOverflow post:

# file: spec/support/warden.rb
# Based on http://stackoverflow.com/questions/13420923/configuring-warden-for-use-in-rspec-controller-specs
module Warden
  # Warden::Test::ControllerHelpers provides a facility to test controllers in isolation
  # Most of the code was extracted from Devise's Devise::TestHelpers.
  module Test
    module ControllerHelpers
      def self.included(base)
        base.class_eval do
          setup :setup_controller_for_warden, :warden if respond_to?(:setup)
        end
      end

      # Override process to consider warden.
      def process(*)
        # Make sure we always return @response, a la ActionController::TestCase::Behavior#process, even if warden interrupts
        _catch_warden {super} || @response
      end

      # We need to setup the environment variables and the response in the controller
      def setup_controller_for_warden
        @request.env['action_controller.instance'] = @controller
      end

      # Quick access to Warden::Proxy.
      def warden
        @warden ||= begin
          manager = Warden::Manager.new(nil, &Rails.application.config.middleware.detect{|m| m.name == 'Warden::Manager'}.block)
          @request.env['warden'] = Warden::Proxy.new(@request.env, manager)
        end
      end

      protected

      # Catch warden continuations and handle like the middleware would.
      # Returns nil when interrupted, otherwise the normal result of the block.
      def _catch_warden(&block)
        result = catch(:warden, &block)

        if result.is_a?(Hash) && !warden.custom_failure? && !@controller.send(:performed?)
          result[:action] ||= :unauthenticated

          env = @controller.request.env
          env['PATH_INFO'] = "/#{result[:action]}"
          env['warden.options'] = result
          Warden::Manager._run_callbacks(:before_failure, env, result)

          status, headers, body = warden.config[:failure_app].call(env).to_a
          @controller.send :render, status: status, text: body,
            content_type: headers['Content-Type'], location: headers['Location']

          nil
        else
          result
        end
      end
    end
  end
end

RSpec.configure do |config|
  config.include Warden::Test::ControllerHelpers, type: :controller
end

Just add this code in a spec/support/warden.rb file and you will be fine.

Let’s then add another shared example to gather the common specs for an authenticated controller.

# file: spec/support/authenticated_api_controller.rb
require 'rails_helper'

RSpec.shared_examples "authenticated_api_controller" do

  describe "authentiation" do

    it "returns unauthorized request without email and token" do
      request.env["HTTP_X_USER_EMAIL"] = nil
      request.env["HTTP_X_AUTH_TOKEN"] = nil
      get :index, { format: :json }

      expect(response.status).to eq(401)
    end

    it "returns unauthorized request without token" do
      user = User.create(email: "user@example.com", password: "password")
      request.env["HTTP_X_USER_EMAIL"] = user.email
      request.env["HTTP_X_AUTH_TOKEN"] = nil
      get :index, { format: :json }

      expect(response.status).to eq(401)
    end

  end

end

By adding a before block where we create a user and its token and set them in the headers, we can now test that our customers controller specs are still passing.

Add the shared example as well to be sure that the controller respects the authentication strategy whenever it’s not valid.

# file: spec/controllers/customers_controller_spec.rb
require 'rails_helper'

RSpec.describe CustomersController, type: :controller do

  before do
    user = User.create(email: "user@example.com", password: "password")
    authentication_token = AuthenticationToken.create(user_id: user.id,
      body: "token", last_used_at: DateTime.current)
    request.env["HTTP_X_USER_EMAIL"] = user.email
    request.env["HTTP_X_AUTH_TOKEN"] = authentication_token.body
  end

  it_behaves_like "api_controller"
  it_behaves_like "authenticated_api_controller"

  # ...

end

Adding a sessions controller to login users

Our last step in order to see some real results for our API is adding a way for the user to log in and receive a valid token to authenticate subsequent requests to the endpoints.

Let’s add some routing with specs for the sessions controller:

# file: spec/routing/sessions_routing_spec.rb
require 'rails_helper'

RSpec.describe SessionsController, type: :routing do
  it { expect(post:   "/sessions").to route_to("sessions#create") }
  it { expect(delete: "/sessions").to route_to("sessions#destroy") }
end
# file: config/routes.rb
Rails.application.routes.draw do
  resource  :sessions,  only: [:create, :destroy]
  resources :customers, only: [:index, :show, :create, :update, :destroy]
end

And write the controller specs for the create (login) and destroy (logout) actions:

# file: spec/controllers/sessions_controller_spec.rb
require 'rails_helper'

RSpec.describe SessionsController, type: :controller do

  let!(:user) { User.create(email: "user@example.com", password: "password") }
  let!(:authentication_token) { AuthenticationToken.create(user_id: user.id,
      body: "token", last_used_at: DateTime.current) }

  let(:valid_attributes) {
    { user: { email: user.email, password: "password" } }
  }

  let(:invalid_attributes) {
    { user: { email: user.email, password: "not-the-right-password" } }
  }

  let(:parsed_response) { JSON.parse(response.body) }

  def set_auth_headers
    request.env["HTTP_X_USER_EMAIL"] = user.email
    request.env["HTTP_X_AUTH_TOKEN"] = authentication_token.body
  end

  before do
    allow(TokenIssuer).to receive(:create_and_return_token).and_return(authentication_token.body)
  end

  describe "POST #create" do
    context "with valid credentials" do
      before { post :create, valid_attributes, format: :json  }

      it { expect(response).to be_success }
      it { expect(parsed_response).to eq({ "user_email" => user.email, "auth_token" => authentication_token.body }) }
    end

    context "with invalid credentials" do
      before { post :create, invalid_attributes, format: :json }

      it { expect(response.status).to eq(401) }
    end

    context "with missing/invalid params" do
      before { post :create, { foo: { bar: "baz" }, format: :json } }

      it { expect(response.status).to eq(422) }
    end
  end

  describe "DELETE #destroy" do
    context "with valid credentials" do
      before do
        set_auth_headers
        delete :destroy, { format: :json }
      end

      it { expect(response).to be_success }
    end

    context "with invalid credentials" do
      before { delete :destroy, { format: :json } }

      it { expect(response.status).to eq(401) }
    end
  end

end

The implementation uses the TokenIssuer to create and return a new token for the user with valid credentials while logging in and again uses the service to get rid of the expired token on logout.

As you might notice, I skip the authenticate! before_action on the create action since we want to enable the user to make this request while not being authenticated for obvious reasons.

The destroy (logout) action instead would still require the user to provide the authentication credentials to be executed.

# file: app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :authenticate!, only: [:create]

  def create
    user = User.find_by(email: session_params[:email])
    if user && user.authenticate(session_params[:password])
      token = TokenIssuer.create_and_return_token(user, request)
      render status: :ok, json: { user_email: user.email, auth_token: token }
    else
      render status: :unauthorized, json: ""
    end
  end

  def destroy
    TokenIssuer.expire_token(current_user, request) if current_user
    render status: :ok, json: ""
  end

  private

    def session_params
      params.require(:user).permit(:email, :password)
    end

end

Let’s add an example user to our seeds and then test with curl the working login and an authenticated request to the customers endpoint.

# file: db/seeds.rb
# ...
User.create(email: "admin@example.com", password: "password")
curl -i -X POST -H "Content-Type:application/json" -d '{ "user": { "email": "admin@example.com", "password": "password" } }' http://localhost:3000/sessions.json
{"user_email":"admin@example.com","auth_token":"m5d2eADqgZ5pX7aE4daSkevg"}

If you received the valid token after logging in, you can now request the list of customers providing the user email and token through the custom headers:

curl -i -X GET -H "Content-Type:application/json" -H "X-User-Email:admin@example.com" -H "X-Auth-Token:m5d2eADqgZ5pX7aE4daSkevg" http://localhost:3000/customers.json

If everything is working properly, you should receive the payload we created some steps ago. Congratulations!

Cross-origin resource sharing (CORS)

The previous step marks the last piece of the standard functionalities that I would require from a simple API application. I would still add one bonus step in order to make sure that our API can be used in client web applications through AJAX calls. In order to do this, the browser expects the API to provide Cross-origin resource sharing (CORS) headers. You can find more information about them on Wikipedia.

The most important thing is that all the responses of our application will contain these additional headers and a special route that respond to OPTIONS HTTP requests is present.

Let’s start by adding the config.action_dispatch.default_headers to the config/application.rb file. I use constants to set the headers so we can reuse them in other places centralizing the setup.

# file: config/application.rb
# ...

CORS_ALLOW_ORIGIN  = "*"
CORS_ALLOW_METHODS = %w{GET POST PUT OPTIONS DELETE}.join(',')
CORS_ALLOW_HEADERS = %w{Content-Type Accept X-User-Email X-Auth-Token}.join(',')

module ExampleApi
  class Application < Rails::Application
    # ...

    config.action_dispatch.default_headers = {
      "Access-Control-Allow-Origin"  => CORS_ALLOW_ORIGIN,
      "Access-Control-Allow-Methods" => CORS_ALLOW_METHODS,
      "Access-Control-Allow-Headers" => CORS_ALLOW_HEADERS
    }
  end
end

I also added to the api_controller shared example in the support directory, some specs that check the presence of those headers in all the responses.

# file: spec/support/api_controller.rb
require 'rails_helper'

RSpec.shared_examples "api_controller" do

  # ...

  describe "responds to OPTIONS requests to return CORS headers" do

    before { process :index, 'OPTIONS' }

    context "CORS requests" do
      it "returns the Access-Control-Allow-Origin header to allow CORS from anywhere" do
        expect(response.headers['Access-Control-Allow-Origin']).to eq('*')
      end

      it "returns general HTTP methods through CORS (GET/POST/PUT/DELETE)" do
        %w{GET POST PUT DELETE}.each do |method|
          expect(response.headers['Access-Control-Allow-Methods']).to include(method)
        end
      end

      it "returns the allowed headers" do
        %w{Content-Type Accept X-User-Email X-Auth-Token}.each do |header|
          expect(response.headers['Access-Control-Allow-Headers']).to include(header)
        end
      end
    end

  end

end

Since the Warden fail_app is not using the default_headers set by our Rails application, we need to manually set again the CORS header in the UnauthenticatedController respond method.

# file: app/controllers/unauthenticated_controller.rb
class UnauthenticatedController < ActionController::Metal

  # ...

  def respond
    self.status        = :unauthorized
    self.content_type  = "application/json"
    self.response_body = { errors: ["Unauthorized Request"] }.to_json
    self.headers["Access-Control-Allow-Origin"]  = CORS_ALLOW_ORIGIN
    self.headers["Access-Control-Allow-Methods"] = CORS_ALLOW_METHODS
    self.headers["Access-Control-Allow-Headers"] = CORS_ALLOW_HEADERS
  end

end

Finally we create a “catch all” route (be sure it’s at bottom of your list) that will respond to every OPTIONS HTTP request with only the CORS headers.

In this way the browser pre-flight request to enable the usage of our API will be fulfilled and we will allow the requests as we like. Please customize the CORS header constants accordingly.

# file: config/routes.rb
Rails.application.routes.draw do
  # ...

  match "/*path",
    to: proc {
      [
        204,
        {
          "Content-Type"                 => "text/plain",
          "Access-Control-Allow-Origin"  => CORS_ALLOW_ORIGIN,
          "Access-Control-Allow-Methods" => CORS_ALLOW_METHODS,
          "Access-Control-Allow-Headers" => CORS_ALLOW_HEADERS
        },
        []
      ]
    }, via: [:options, :head]

end

Conclusion

It was a long tutorial and we just skimmed the surface of the topic. I will probably extend in the future the application with some more features and write other tutorials about them. For example I would like to use this API on an Angular.js web app to show how you can build a single-page application using these technologies.

You can find the complete repository for the tutorial on Github.

For any question or request, you can use the comments below or send an email to luca.tironi@gmail.com

Stay tuned and thanks for your time, I hope you will find this useful.

L

^Back to top