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