Loading...
New webinar: "The Remote Job Search: My Microverse Journey" with graduate Paul Rail
Watch Now

Topics

Previously, I shared how to build a RESTful API Authentication with JWT. In this article we will focus on how to secure some of the endpoints that require only logged in users to access, as well as perform manual tests on them using Postman. As usual, we will follow our test-driven-development approach while doing it.

Postman is an API development environment that makes it easier to test and manage HTTP REST APIs. It also lets you organize and document your API very easily.

Authenticate API Requests

We will simply continue from our last article, where we had finished building the login. The next step is to register endpoints alongside issuing JSON Web Tokens for each authentication request. To follow along, here's the code for the previous article.

To recap, when a user logs in using our current API, looking at the user_representer.rb file, it returns:

{% code-block language="js" %}
 def as_json
     {
       id: user.id,
       username: user.username,
       token: AuthenticationTokenService.call(user.id)
     }
   end
{% code-block-end %}

If we were to use React as our frontend, we would want to add the token to the headers of our request in order to verify each user request. However, what if the user can get a hold of our API directly? Then, they can make requests using external applications like Postman without the need to login. In our current API, one can add a book without any need to login.

Let’s start by adding a private method to our app/controllers/application_controller.rb. With this method we will be able to extract the token for each request headers and verify it with the logged in user. Here’s how:

{% code-block language="js" %}
class ApplicationController < ActionController::API 
include Response 
include ExceptionHandler 
private 
def payload 
  auth_header = request.headers['Authorization']  
  token = auth_header.split(' ').last   
AuthenticationTokenService.decode(token) 
rescue StandardError   
nil 
end
end
{% code-block-end %}

Let’s add another helper method that will render invalid authentication messages for invalid login requests.

{% code-block language="js" %}
class ApplicationController < ActionController::API
[...]
 private
[...]
 def invalid_authentication
   render json: { error: 'You will need to login first' }, status: :unauthorized
 end
end
{% code-block-end %}

Next, we will add the current_user method. Below you will notice that we are extracting the user_id from the payload method we added earlier.

{% code-block language="js" %}
def current_user!
   @current_user = User.find_by(id: payload[0]['user_id'])
 end
{% code-block-end %}

Finally, let's add the authenticate_request method which we will be using on any controller we wish to protect its endpoint.

The final code of our application controller should look like this now:

{% code-block language="js" %}
class ApplicationController < ActionController::API
include Response
 include ExceptionHandler
 rescue_from ActiveRecord::RecordNotDestroyed, with: :not_destroyed
 def authenticate_request!
   return invalid_authentication if !payload || !AuthenticationTokenService.valid_payload(payload.first)
   current_user!
   invalid_authentication unless @current_user
 end
 def current_user!
   @current_user = User.find_by(id: payload[0]['user_id'])
 end
 private
 def payload
   auth_header = request.headers['Authorization']
   token = auth_header.split(' ').last
   AuthenticationTokenService.decode(token)
 rescue StandardError
   nil
 end
 def invalid_authentication
   render json: { error: 'You will need to login first' }, status: :unauthorized
 end
end
{% code-block-end %}

You will notice, I added rescue_from ActiveRecord::RecordNotDestroyed, with : :not_destroyed. This exception will help us avoid any error thrown when we have a failed destroy method.

Now that our authenticate_request! method is ready, we can use the before_action filter in any controller we like to protect its endpoint. Just before that, let’s fire up our server (rails server) and make sure everything is working as expected before securing the books endpoints.

We are going to use Postman for our test, you can use Postman Chrome or download the native app to your system if you don’t have it. 

Now that you’re done with all necessary setups, your Postman screen should look like this:

Necessary Setups


My server is currently running on http://localhost:3000/, I hope yours runs on that too. For a quick reminder, the table below shows the list of our API Endpoints. 

API Endpoints

Let’s go ahead and test our GET api/v1/books endpoint using http://localhost:3000/api/v1/books. Don’t worry if you don’t have any data in your database, if yours returned an empty array ( [ ] ) that’s fine.

GET api/v1/books

Securing Our API Endpoints

With APIs becoming foundational to modern app development, the attack surface is continually increasing. 

The attack surface in this context refers to; all entry points through which an attacker could potentially gain unauthorized access to a network or system to extract or enter data or to carry out other malicious activities.

Debbie Walkowski in Securing APIs: 10 Best Practices for Keeping Your Data and Infrastructure Safe provides great insights on this topic.

We will begin by adding before_action :authenticate_request! to secure our books endpoints.

{% code-block language="js" %}
module Api
 module V1
   class BooksController < ApplicationController
     before_action :authenticate_request!     
     before_action :set_book, only: %i[update show destroy]
    [...]
   end
 end
end
{% code-block-end %}

If we try to fetch all books using our GET api/v1/books endpoint (http://localhost:3000/api/v1/books) with Postman again we get an "error": "You will need to login first". 

That is good, it means our authenticate_request! does exactly what it should do.

Now, if we try to run our test again, all books-related tests should fail (rspec spec/requests/books_request_spec.rb). We will try to fix it by providing a token in the authorization header for each of our requests to the books endpoint. 

It uses a bearer token, a cryptic string, usually generated by the server in response to a login request. The client must send this token in the Authorization header when making requests to protected resources.

First, let’s create a test user inside our books_request_spec.rb using 

{% code-block language="js" %}
 let(:user) { FactoryBot.create(:user, username: 'acushla', password: 'password') }
{% code-block-end %}

Then introduce authorization headers to each of our requests. The books_request_spec.rb should now look like the code below.

{% code-block language="js" %}
# spec/requests/books_request_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
 [...]
 let(:user) { FactoryBot.create(:user, username: 'acushla', password: 'password') }
 describe 'GET /books' do
   before { get '/api/v1/books', headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
   [...]
 end
 describe 'GET /books/:id' do
   before { get "/api/v1/books/#{book_id}", headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
   [...]
 end
 describe 'POST /books/:id' do
   [...]
   context 'when request attributes are valid' do
     before { post '/api/v1/books', params: valid_attributes, headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
     [...]
   end
   context 'when an invalid request' do
     before { post '/api/v1/books', params: {}, headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
     [...]
   end
 end
 describe 'PUT /books/:id' do
   [...]
   before { put "/api/v1/books/#{book_id}", params: valid_attributes, headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
   [...]
 end
 describe 'DELETE /books/:id' do
   before { delete "/api/v1/books/#{book_id}", headers: { 'Authorization' => AuthenticationTokenService.call(user.id) } }
   [...]
 end
end
{% code-block-end %}

Note, I used [...] to denote a block of code we didn’t modify.

Let’s run our test again with rspec spec/requests/books_request_spec.rb. All should be working just fine now. 

Before we go ahead and test this out with Postman, we need to update our books_controller, so that we use the current user for the create books operation.

{% code-block language="js" %}
def create
       @book = current_user!.books.create(book_params)
       if @book.save
         [...]
       end
     end
{% code-block-end %}

Thanks to this, we will no longer need the user_id attribute inside our spec/requests/books_request_spec.rb POST request.

{% code-block language="js" %}
describe 'POST /books/:id' do
   [...]
   let(:valid_attributes) do
     { title: 'Whispers of Time', author: 'Dr. Krishna Saksena',
       category_id: history.id }
   end
  [...]
end
{% code-block-end %}

After this step by step guide, you should feel more comfortable securing any of your endpoints. 

Also, if you will be using devise in future, you should take a look at the documentation.

Testing and Documenting our Endpoints

Here, we are going to build and organize all of our endpoints into a single collection using Postman. We will use it to generate documentation for our endpoints.

To add a collection, see the image below:

Testing and Documenting our Endpoints

'POST /register'

Let’s begin by performing our first request, by registering a user using the ‘POST /register’ endpoint.

Testing and Documenting

Upon successful registration, the response comes with a token which we can use to login automatically. For the sake of documentation, we will need to also perform the login request. So, let’s add our first successful request to our Books API collection.

Books API collection

If you look at the image carefully, I clicked on the save button to open the ‘Save Request’ dialog box. Then I changed the Request name to a more friendly name, added a request description - which is purely optional - and selected the collection Books API. We will repeat this process for the next few endpoints we will be testing.

POST /login'

Next, I will go ahead and login with the user we just created using the ‘POST /login’ endpoint and save it to our collection as well.

POST /login

POST /categories

Note that we did not secure our categories endpoint, so we can create one without requiring any authorization header in our request.

POST /categories‍

I will go ahead and add the GET request for categories as well.

POST /books

Here, we are going to need to take the token from our login request and add it to the header of our request. Navigate to the Headers tab and add Authorization key with prefix of Bearer + token to the value as shown below.

Navigate the Headers Tab

With this out of the way we can make a POST request successfully and add to our collection as well.

Make a POST request successfully

GET /books

For the GET /books request you want to make sure you have the same Authorization header as well.

POST register

Publish Your Documentation

Postman handles publishing your documentation very easily for you. Simply click on ‘Publish Docs’ and the rest will be history.

POST /categories‍

Great job! You should be proud of yourself, if you’ve followed this series of articles from the first and second through to this last article. Bravo!

I think you would now agree that spinning up a RESTful JSON API with Ruby-on-Rails is quite easy to get started with. In this article we’ve covered quite a lot while maintaining best practices. Although many programmers think that Test Driven Development adds extra work to the development process, it can save you a lot of time, and make the debugging process faster. It also helps you gain great confidence while refactoring existing code without a fear of breaking it.

You can find the full code for everything we went over here.

I would love to hear your feedback on this series, as well as know if you’re interested in a follow up article about a React application that consumes this API. Reach out to me on Twitter or LinkedIn.


Subscribe to our Newsletter

Get Our Insights in Your Inbox

Career advice, the latest coding trends and languages, and insights on how to land a remote job in tech, straight to your inbox.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
We use own and third party cookies to; provide essential functionality, analyze website usages, personalize content, improve website security, support third-party integrations and/or for marketing and advertising purposes.

By using our website, you consent to the use of these cookies as described above. You can get more information, or learn how to change the settings, in our Cookies Policy. However, please note that disabling certain cookies may impact the functionality and user experience of our website.

You can accept all cookies by clicking the "Accept" button or configure them or refuse their use by clicking HERE.