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.
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.
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.
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.
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:
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.
Run the specs with bin/rspec and see them fail once. Add the Customer model and relaunch the specs.
The output of the specs should be similar to this one:
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.
I will start by adding the routing specs to be sure that our controller is routable as expected.
To make these specs pass we need to add the customers resource to the routes.rb file.
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.
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.
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.
Run the rake command to insert the seeds in your database and start the rails application server.
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.
Add the gem to Gemfile and bundle it. Use the rails generator provided to add the CustomerSerializer model.
Add the attributes that we want to return as JSON:
The payload should be now like the following one, without the timestamps.
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.
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.
Add to the customer controller specs the shared example with the it_behaves_like method call.
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:
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.
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.
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.
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.
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.
User and AuthenticationToken models
Let’s create now the User and AuthenticationToken models.
I added an index on the email attribute since we will look for users through the email while doing the authentication.
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.
Let’s add an initializer to require and load our new strategy and setup the middleware.
Authentication Token Strategy
We now just need to create the authentication token strategy specs and class.
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.
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.
By including the concern in the ApplicationController we assure that all the controllers that inherit from it will require authentication.
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.
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:
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.
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.
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:
And write the controller specs for the create (login) and destroy (logout) actions:
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.
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.
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:
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.
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.
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.
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.
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.