Introduction

This is part 3 in a series of articles that shows you how to build your own API. In part 2, we created the Client and ApiKey models. In this article we will set up authorization and show you how to make your first API request. Click the links below to read other parts of this series.

In this article I cover authentication, authorization, and making the first API request. Let's get to it!

Basic Authentication

Now it's time to build our authentication system, but how exactly does this work? To authenticate a user, we will look for the API key in an HTTP header called HTTP_AUTHORIZATION. The data stored in this header takes the format of Token=<api key> where <api key> represents the user's API key. To authenticate the request, we will have a before_action method in our application controller that uses a special Rails helper method called authenticate_with_http_token. This method takes a block and makes it easy for you to authenticate the API key. If the user is not valid, we will return a JSON error that states the request is unauthorized. You will see this in action in a moment, but first we need to write a test. Ceate a new file called application_controller_spec.rb inside the spec/controllers folder and modify it so that it looks like the code below.

spec/controllers/application_controller.rb:

require 'rails_helper'

RSpec.describe ApplicationController, type: :controller do
  let(:api_key) { create(:api_key) }
  
  controller do
    def index
      render json: { response: :success }
    end
  end
  
  it 'returns success with a valid http token' do
    request.env['HTTP_AUTHORIZATION'] = token(api_key.key)
    get :index, params: { format: :json }
    expect(response).to have_http_status(:success)
  end
  
  it 'returns unauthorized when the token is invalid' do
    request.env['HTTP_AUTHORIZATION'] = token('fake')
    get :index, params: { format: :json }
    expect(response).to have_http_status(:unauthorized)
  end
end

Let's break this down:

  • Line 4 creates an API key.
  • Line 6-10 creates a dummy index method. This allows us to easily test the functionality on our application controller.
  • The test at lines 12-16 checks whether get :index is successful or not. Line 13 adds the API key to the header, Line 14 makes the request, and Line 15 checks whether it was successful.
  • The test at lines 1-22 checks whether invalid keys receive an unauthorized error when an attempt is made to access index.

Pretty simply right? Now let's work on the application controller itself. Open up your application controller at app/controllers/application_controller.rb and modify it so that it looks like the code listed below.

app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate

  def authenticate
    authenticate_token || unauthorized
  end
  
  def authenticate_token
    authenticate_with_http_token do |token, options|
      @api_key = ApiKey.find_by(key: token)
    end
  end

  def unauthorized
    render json: {error: 'Unauthorized Request'}, status: :unauthorized
  end
end

  • Line 2 includes a few helper methods provided by Rails in our application controller. These helper methods allow us to easily perform the authentication of our users.
  • Line 4 calls the authenticate method before every request.
  • Lines 6-8 actually implement the authenticate method. The authenticate method calls authenticate_token and if that method fails it then calls unauthorized, which renders a JSON unauthorized error.
  • The authenticate_token at lines 10-14 method uses a Rails helper to retrieve the token and authenticate it against our application code.

That's all there is to authenticating the request, now let's move on to authorization.

Basic Authorization

For our first API request we will create an API request for getting a list of clients. If you recall in Part 1, our admin API keys should have the ability to access all clients, where users of a client only have access to their own clients. In order to facilitate this we must build an authorization system that determines what resources the user has access to. To make this easier we will utilize a library called Pundit.

What is Pundit? Pundit is a handy library that allows you to build an authorization system using regular ruby classes. Pundit creates the concept of policies for each model. Policies contain a bit of business logic that either authorizes or denies the request.

Let's get Pundit installed. Open up your gem file and add in the line listed below.

Gemfile:
  
gem 'pundit'

Now run a bundle install to install the gem.

Terminal Commands:

bundle install

Now it's time to configure Pundit. First, run the command below to the app/policies folder along with a default application policy.

Terminal Commands:

rails g pundit:install

Now let's add support for Pundit to our Application controller. Open up your application controller and modify it so that it looks like the code below.

app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
  include Pundit

  before_action :authenticate

  def authenticate
    authenticate_token || unauthorized
  end
  
  def authenticate_token
    authenticate_with_http_token do |token|
      @api_key = ApiKey.find_by(key: token)
    end
  end
  
  def pundit_user
    @api_key
  end

  def unauthorized
    render json: {error: 'Unauthorized Request'}, status: :unauthorized
  end
end

Let's break this down:

  • Line 3 includes support for Pundit in our application controller. This means that all of our controller inheriting from the application controller will have access to the Pundit helper methods.
  • Lines 17-19 define a special method called pundit_user. The pundit_user method returns the current user, or in this case, the current API key. Pundit uses this method to simplify some of the code we need to write.

Now that we have Pundit set up, we can start building our authorization system. However, there is one more thing we need to do. Pundit includes support for RSpec, but I'm not a fan of it because it's syntax doesn't really follow the current flow of RSpec's DSL. Because of this I like to use something a bit different different. Create a file in your spec/support folder called pundit.rb and add in the code below.

spec/support/pundit.rb:

RSpec::Matchers.define :permit do |action|
  match do |policy|
    policy.public_send("#{action}?")
  end

  failure_message do |policy|
    "Expected #{policy.class} to allow #{action} on #{policy.record} for #{policy.user.inspect}, but it was not allowed."
  end

  failure_message_when_negated do |policy|
    "Expected #{policy.class} to not allow #{action} on #{policy.record} for #{policy.user.inspect}, but it was allowed!"
  end
end

The code above adds in a function for RSpec called permit. The permit function allows you to write a simple RSpec expectation like expect(policy).to permit(:index). Now let's work on building a policy for our client model.

Pundit policies are made up of two different pieces. The first piece is called a scope. Scopes are classes that contain methods that filter data returned when we perform a query. The second piece that Pundit includes are methods that allow us to determine whether a user has access to specific controller methods. We will go into this in a moment. Let's create the client policy now. The command below will generate a policy class for the Client model in app/policies/client_policy.rb. In addition a spec in spec/policies/client_policy.rb will be created. Run it now.

Terminal Commands:

rails g pundit:policy Client

Now that we have created the policy, let's write some tests for it. Open up the client policy spec at spec/policies/client_policy_spec.rb and modify it so that it looks like the code below.

spec/policies/client_policy_spec.rb:

require 'rails_helper'

RSpec.describe ClientPolicy do
  let(:client_1) { create(:client) }
  let(:client_2) { create(:client) }
  let(:admin) { build_stubbed(:api_key, :admin) }
  let(:user) { build_stubbed(:api_key, client: client_1) }

  subject { described_class }

  describe 'scope' do
    it 'returns all clients when the user is an admin' do
      scope = described_class::Scope.new(admin, Client).resolve
      expect(scope.all).to eq([client_1, client_2])
    end

    it 'only returns the client assigned when the user is not an admin' do
      scope = described_class::Scope.new(user, Client).resolve
      expect(scope.all).to eq([client_1])
    end
  end
end

  • Lines 4 and 5 create 2 clients.
  • Lines 6 and 7 initialize 2 users.
  • The test at lines 10-13 checks that all clients are returned for an admin user. We will have more details on how this test works once we take a look at the policy code itself.
  • The test at lines 15-18 checks that only the current client is returned for users that aren't admins.

Now let's make this test pass. Open up the client policy at app/policies/client_policy.rb and modify it so that it looks like the code listed below.

app/policies/client_policy.rb:

class ClientPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(id: user.client_id)
      end
    end
  end
end

As mentioned earlier, there is a class called Scope embedded within the client policy class. This class contains a single method called resolve. The resolve method is responsible for handling the authorization. In this example, we check if the user is an admin at line 4. If they are an admin then we return all clients. Otherwise, we only return the clients that match the user's client.

If you recall the code in the client policy spec, we create a new instance of this class and call resolve. This executes the code within the resolve method, which basically adds a .where clause to any query that uses the policy scope. Pundit includes a handy helper method for this. The policy_scope method will automatically grab the API Key using either current_user or pundit_user and call .resolve for us. The end result is we can simply call policy_scope(Client) in our application code and the resolve code gets executed. We will see this in action in just a moment. Let's move on.

Now that we have a policy in place for our Client model, we can start working on our first API request. To do this, we first need to create a controller for our clients API call. Run the command below to create this controller now.

Terminal Commands:
  
rails g controller clients index

Great, now let's modify our routes file. Open up the routes file and modify it so that it looks like the code below.

config/routes.rb:

Rails.application.routes.draw do
  resources :clients, only: [:index]
end

You may notice that we only include the index path in the routes file. We will add onto this later, for now we will keep things simple. Now let's write a test for our clients controller. Open up the clients controller spec at spec/controllers/clients_controller_spec.rb and modify it so that it looks like the code listed below.

Now we are ready to write the spec for the clients controller, but first, let's create a few helpers that will allow us to more easily write our specs. Create a new file in spec/support called helpers.rb and modify it so that it looks like the code listed below.

spec/support/helpers.rb:
 
module Helpers
  def token(key)
    ActionController::HttpAuthentication::Token.encode_credentials(key)
  end

  def json
    JSON.parse response.body
  end
end

RSpec.configure do |c|
  c.include Helpers
end

  • The token method at lines 2-4 creates the value for our HTTP_AUTHORIZATION header from the API key that we pass in.
  • The json method at lines 6-8 just saves us a step in calling JSON.parse(response.body).
  • The code at lines 11-13 registers these helper methods with RSpec.

Let's move on. Now it's time to create our controller spec. Open up your spec/controllers/clients_controller_spec.rb file and modify it so that it looks like the code listed below.

spec/controllers/clients_controller_spec.rb:

require 'rails_helper'

RSpec.describe ClientsController, type: :controller do
  let!(:client_1) { create(:client) }
  let!(:client_2) { create(:client) }

  let(:user) { create(:api_key, client: client_1) }
  let(:admin) { create(:api_key, :admin) }

  def do_request(method, verb, key, params = {})
    request.env['HTTP_AUTHORIZATION'] = token(key)
    process method, method: verb, params: params
  end

  describe 'GET #index' do
    context 'as an admin' do
      before { do_request(:index, :get, admin.key) }
      
      it 'returns http success' do
        expect(response).to have_http_status(:success)
      end

      it 'returns a list of clients' do
        expect(json['clients'].size).to eq(2)
      end
    end

    context 'as a user' do
      before { do_request(:index, :get, user.key) }

      it 'returns http success' do
        expect(response).to have_http_status(:success)
      end

      it "returns the user's assigned client" do
        expect(json['clients'].size).to eq(1)
        expect(json['clients'][0]['id']).to eq(client_1.id)
      end
    end
  end
end

Let's break this down.

Lines 4-8 create 2 clients, 1 admin API key and 1 regular API key that is assigned to client_1.

The do_request function defined at lines 10-13 is a helper function that makes the request. Line 11 sets the appropriate HTTP header while line 12 actually performs the request. Normally we could do this in one line( example: get :index, headers: { 'HTTP_AUTHORIZATION' => token(admin.key) }. However, as of this writing, a bug in RSpec and/or Rails currently does not properly set the headers.

The first set of tests at lines 16-26 tests admin functionality:

  • Line 17 calls our helper function to perform the request before each test.
  • The test at lines 19-21 checks whether the request was successful.
  • The test at lines 2-25 checks whether a list of clients was returned.

The next set of tests at lines 28-39 tests regular API key functionality:

  • The test at lines 31-33 checks whether the request was successful.
  • The test at lines 3-38 checks that only the client assigned to the API key is returned.

Making A Request Using Curl

If we run our test suite, it passes. That's great, however how do we know it works? It's pretty easy to test actually. First we need to add a couple seeds to our db/seeds.rb file. Modify this file to look like the code listed below.

db/seeds.rb:

include FactoryGirl::Syntax::Methods
DatabaseCleaner.clean_with(:truncation)

client_1 = create(:client)
client_2 = create(:client)

admin = create(:api_key, :admin, key: 'admin')
user = create(:api_key, key: 'user', client: client_1)

Now run rails db:seed to seed the database:

Terminal Commands:

rails db:seed

Next, start your Rails development server using rails s

Terminal Commands:

rails s

Finally, make sure the curl command is installed and then run the following code:

Tip:  If you can't install curl or don't want to, skip to the next section and we'll show you an alternative way of testing.
Terminal Commands:

curl -w "\n" -H "Authorization: Token token=user" http://localhost:3000/clients.json

This should return:


{"clients":[{"id":1,"name":"Client 1"}]}

Replace userwith admin to check the admin functionality:

Terminal Commands:

curl -w "\n" -H "Authorization: Token token=user" http://localhost:3000/clients.json

This should return:


{"clients":[{"id":1,"name":"Client 1"},{"id":2,"name":"Client 2"}]}

Making a Request Using Postman

You can also make the request using Postman. Postman is a handy utility that let's you make and automate web requests. You can download it at https://www.getpostman.com/. Once installed, enter http://localhost:3000/clients.json in the url. Then click Headers and Type Authorization for the key and Token token=user for the value. Then click send and you'll get a response back. Replace user with admin to test admin functionality. See the screenshot below.

Postman Screenshot

Conclusion

In our next article we will continue building out our API. If you have any questions/comments feel free to leave them in the comment section below. As always, thanks for reading!