Authenticating Microservice Requests in Rails
This lesson walks through how to authenticate requests in a Rails microservice application, including ensuring that a request is coming from a valid client.
Guide Tasks
  • Read Tutorial

There are a number of ways to authenticate an outside microservice request. Let's step back and list out the requirements for our application:

  1. Request comes in from outside service.
  2. App needs to ensure the request contains a valid source_app and api_key values.
  3. Notification will only go out if the request contains the correct authentication values.

Current Requests

Right now we're testing requests that contain data such as:

    post "/notifications",
    {
      notification: {
        phone: "5555555555",
        body: "My Message",
        source_app: "my_app_name"
      }
    }, headers

As you may have noticed, we don't have a spot in the request that takes in the API key value, so how can we check it? Technically you could place the API authentication code in the request itself, with code such as:

    post "/notifications",
    {
      notification: {
        phone: "5555555555",
        body: "My Message",
        source_app: "my_app_name"
      },
      client: {
        source_app: "my_app_name",
        api_key: "asdfasdf"
      }
    }, headers

However this would be a poor practice. I spoke with the developer Michal Kwiatkowski this morning and discussed the best implementation for the app. He pointed out that API authentication should reside inside of the HTTP header, not in the request itself. And since this course focuses on best practices let's follow that pattern.

Testing API Authentication in RSpec

As you may expect we're going to start off with our app tests. We don't have to create any new tests, but we do need to update the ones that we have. Opening up the request specs, let's update the first one to look like this:

# spec/requests/notification_spec.rb

  it "creates a Notification" do
    client = FactoryGirl.create(:client)

    headers = {
      "ACCEPT" => "application/json",
      "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials(client.source_app, client.api_key)
    }

    post "/notifications",
    {
      notification: {
        phone: "5555555555",
        body: "My Message",
        source_app: "my_app_name"
      }
    }, headers

    expect(response.content_type).to eq("application/json")
    expect(response).to have_http_status(:created)
  end

Let's walk through what we're doing here:

  1. Created a new FactoryGirl client.
  2. Passed the new client to the HTTP header on this line "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials(client.source_app, client.api_key)

Now when we run rspec this will all pass. However that's because right now the application doesn't require authentication at all. So ideally, when we've implemented the authentication feature the other tests should start to fail.

Designing the Auth Setup

Now that we have a functional test, let's implement the code for authenticating API requests. We're going to leverage the Basic authentication system provided by the Rails framework. I think this is a good choice because:

  1. It's straightforward to implement
  2. It's straightforward to test
  3. It works well with API requests
  4. It covers edge cases that would take a long time for us to implement manually

Since this is a microservice with a single request type we can place this code in the NotificationsController. In a larger app this code could be placed in the ApplicationController. However, given the fact that a microservice should only have a single feature, I think it makes to place this code in the NotificationsController.

I think this is the right design decision because if we place the code in the main app controller than it would limit our flexibility. For example, if for any reason we would ever wants to allow any service to pick up the index list of notifications, this implementation would let us open an index action up without having to re-configure the full auth system.

Implementing Basic Auth for a Rails Microservice

Now that we know the design, let's implement the code.

# app/controllers/notifications_controller.rb

class NotificationsController < ApplicationController
  include SmsTool

  before_filter :authenticate

  def create
    @notification = Notification.new(notification_params)

    respond_to do |format|
      if @notification.save
        SmsTool.send_sms(@notification.phone, @notification.body, @notification.source_app)
        format.json { render action: 'show', status: :created, location: @notification}
      else
        format.json { render json: @notification.errors, status: :unprocessable_entity }
      end
    end
  end

  def show
    @notification = Notification.find(params[:id])
  end

  private

    def notification_params
      params.require(:notification).permit(:phone, :body, :source_app)
    end

    def authenticate
      authenticate_or_request_with_http_basic do |source_app, api_key|
        client = Client.find_by_source_app(source_app)
        client && client.api_key == api_key
      end
    end
end

We're adding two items to the controller here:

  1. A before_action that will run before each of methods in the controller. This ensures that no outside applications will be able to create or show any notifications unless they're authorized.
  2. An authenticate method

Let's examine what the authenticate method is doing.

Authenticate Method

To begin with, I'm using the authenticate_or_request_with_http_basic method. This is a built in method that's available from Rails. The method takes two parameters. In our case the parameters are the source_app and api_key. However they could just as easily be named username and password.

The authenticate_or_request_with_http_basic method also takes a block that is looking for a true or false return value. This means that the code inside of the block will perform an action, such as checking the database to see if a request contains accurate login information.

In our case, the code inside of the block is running a database query. First it's looking for a client with the source_app value provided in the request. Next it's checking to see if the client returned from the database has a matching api_key. If the client passes both tests it's considered authenticated and can communicate with the app.

Technically this authenticate method is breaking the OOP single responsibility rule. However I haven't decided on if we need to refactor it yet, so I'm going to keep it like it is for now.

Running the Tests

If you run the tests now you'll see that our assumptions were correct and our test that contains the proper header auth data is passing, while the two tests with plain headers are now failing. Let's update the entire set of request specs and have a before block run and store the header in an instance variable.

# spec/requests/notification_spec.rb

require "rails_helper"

RSpec.describe Notification, type: :request do
  before do
    client = FactoryGirl.create(:client)

    @headers = {
      "ACCEPT" => "application/json",
      "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials(client.source_app, client.api_key)
    }
  end

  it "creates a Notification" do
    post "/notifications",
    {
      notification: {
        phone: "5555555555",
        body: "My Message",
        source_app: "my_app_name"
      }
    }, @headers

    expect(response.content_type).to eq("application/json")
    expect(response).to have_http_status(:created)
  end

  it 'renders an error status if the notification was not created' do
    post "/notifications",
    {
      notification: {
        phone: "5555555555",
        body: "My Message"
      }
    }, @headers

    expect(response.content_type).to eq("application/json")
    expect(response).to have_http_status(:unprocessable_entity)
  end

  it 'sends a text message via the Twilio API after a notification is created' do
    post "/notifications",
    {
      notification: {
        phone: "1234567890",
        body: "New Message",
        source_app: "my_app_name"
      }
    }, @headers

    expect(FakeSms.messages.last.num).to eq("1234567890")
  end
end

Notice here that we have a new before block that will manage the header creation process. This includes the creation of the client that will be used for the tests along with setting the header values. This allows us to remove the header calls throughout the rest of the tests and clean the code up a bit. If you run the tests again you'll see that they're all passing!

Testing in the browser

large

If you run this in the Rails server you’ll see that this works and the browser is asking for login credentials. This is essentially mimicking what the API will encounter when it tries to communicate with the app.

I ran a database query to find client and I found one that had the following credentials:

  • source_app: “app_name3”
  • api_key: “gJpCXE9u0IhAx9Rct3Hy1Att”

If I enter in the wrong credentials, the app won’t let me see the content and it will ask for my login credentials again, as shown here:

large

But if I enter the correct login credentials into the browser it will let me access the page.

large

This shows that everything is working properly. So just to ensure we have all our bases covered, let's test this using cURL.

Testing Microservice Auth Using cURL

Before authentication was implemented we were able to create a notification by sending a cURL request. This cURL request used to work:

curl -X POST -d "notification[phone]=4322386131" -d "notification[body]=mymessage" -d "notification[source_app]=myapp" http://localhost:3000/notifications

However if I try this now it fails, as it should. And it fails with the error message:

HTTP Basic: Access denied.

large

Including Auth Headers via cURL

So how can we include our login credentials via cURL? Thankfully cURL has a nice built in way of passing in parameters for HTTP basic auth requests like ours. We can prepend the code:

--user app_name3:gJpCXE9u0IhAx9Rct3Hy1Att

Which, if you remember are simply the login credentials we tested in browser, separated by a colon. The full cURL request would look like this:

curl --user app_name3:gJpCXE9u0IhAx9Rct3Hy1Att -X POST -d "notification[phone]=4322386131" -d "notification[body]=mymessage" -d "notification[source_app]=myapp" http://localhost:3000/notifications

Now if we run this command in the terminal it will process properly:

large

And it even sends out the text message!

medium

Summary

So you now know how to fully implement authentication into a Rails microservice, including how to test it properly with RSpec. This is the full set of features that we're going to build for this particular application. With this knowledge you now know how to build a fully functional Rails API application, with features including:

  • API Authentication.
  • TDD based development for API based features.
  • Analysis of API request/response lifecycle.
  • Connecting a Rails API application with another API.

Resources