- 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:
- Request comes in from outside service.
- App needs to ensure the request contains a valid
source_app
andapi_key
values. - 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:
- Created a new
FactoryGirl
client
. - Passed the new
client
to theHTTP 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:
- It's straightforward to implement
- It's straightforward to test
- It works well with API requests
- 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:
- A
before_action
that will run before each of methods in the controller. This ensures that no outside applications will be able tocreate
orshow
anynotifications
unless they're authorized. - 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
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:
But if I enter the correct login credentials into the browser it will let me access the page.
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.
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:
And it even sends out the text message!
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.