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.
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:
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.
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.
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.
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:
'POST /register'
Let’s begin by performing our first request, by registering a user using the ‘POST /register’ endpoint.
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.
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 /categories
Note that we did not secure our categories endpoint, so we can create one without requiring any authorization header in our request.
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.
With this out of the way we can make a POST request successfully and add to our collection as well.
GET /books
For the GET /books request you want to make sure you have the same Authorization header as well.
Postman handles publishing your documentation very easily for you. Simply click on ‘Publish Docs’ and the rest will be history.
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.
Career advice, the latest coding trends and languages, and insights on how to land a remote job in tech, straight to your inbox.